getprismo 0.1.31 → 0.1.33

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,7 +30,7 @@ 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
@@ -38,8 +38,9 @@ agent-native npx getprismo mcp
38
38
  ```
39
39
 
40
40
  **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.
41
+ **guard** runs live guardrails, context throttle, rescue prompts, context firewall, and dashboard-ready prevention events.
42
+ **watch** monitors context pressure live and is the lower-level diagnostic view behind guard.
43
+ **receipt** explains what repeated, what output dominated, what artifacts leaked, what likely influenced the run, and a heuristic context-efficiency score.
43
44
  **replay** reconstructs why a session went sideways and prints a recovery prompt.
44
45
  **shield** runs noisy commands without dumping full output back into the agent context.
45
46
  **mcp** exposes PrismoDev as local tools so compatible agents can scan, search shield output, and request scoped context directly.
@@ -271,7 +272,24 @@ this is intentionally not magic interception yet. it is a safe local-first primi
271
272
 
272
273
  ## new: live guardrails mode
273
274
 
274
- the easiest proactive mode is:
275
+ the easiest proactive mode is guard:
276
+
277
+ ```bash
278
+ npx getprismo connect --token <your Prismo API key>
279
+ npx getprismo guard --watch
280
+ ```
281
+
282
+ `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.
283
+
284
+ run it once for a snapshot:
285
+
286
+ ```bash
287
+ npx getprismo guard
288
+ npx getprismo guard --json
289
+ npx getprismo guard --no-sync
290
+ ```
291
+
292
+ the lower-level watch mode is:
275
293
 
276
294
  ```bash
277
295
  npx getprismo watch --auto
@@ -600,7 +618,7 @@ npx getprismo receipt
600
618
  npx getprismo receipt codex --json
601
619
  ```
602
620
 
603
- it summarizes repeated reads, generated artifacts, tool-output floods, repeated commands, likely influence, and the next scoped action to take.
621
+ 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
622
 
605
623
  `replay` is the postmortem view:
606
624
 
@@ -622,9 +640,11 @@ it surfaces recurring waste patterns such as the same lockfile leaking into many
622
640
 
623
641
  ```bash
624
642
  npx getprismo instructions audit
643
+ npx getprismo instructions ablate --dry-run
644
+ npx getprismo instructions apply --dry-run
625
645
  ```
626
646
 
627
- it scores rules in `CLAUDE.md`, `AGENTS.md`, `.codex/AGENTS.md`, `.codex/instructions.md`, and `.openai/instructions.md`, then flags duplicated rules, low-signal rules, trim candidates, and rules that appear ineffective based on recent session evidence.
647
+ 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.
628
648
 
629
649
  `boundaries` checks parallel-agent isolation:
630
650
 
@@ -716,10 +736,12 @@ no install needed. npx runs it directly.
716
736
  | `cc` | claude code cost breakdown |
717
737
  | `cc timeline` | session reconstruction with events |
718
738
  | `cursor` | cursor session tracking and ai authorship |
719
- | `receipt` | run receipt for reads, repeats, output, artifacts, likely influence, and next-run scope |
739
+ | `receipt` | run receipt for reads, repeats, output, artifacts, context efficiency, likely influence, and next-run scope |
720
740
  | `replay` | incident replay with root cause and recovery prompt |
721
741
  | `timeline` | recurring context-waste patterns across recent sessions |
722
742
  | `instructions audit` | instruction ROI audit for CLAUDE.md / AGENTS.md violations, partial compliance, duplicates, and influence-unknown rules |
743
+ | `instructions ablate --dry-run` | conservative ablation plan for instruction candidates without editing files |
744
+ | `instructions apply` | safely dedupe exact duplicate instruction lines with backups |
723
745
  | `boundaries` | multi-agent boundary check for shared files/artifacts and worktree overlap |
724
746
  | `scan --usage` | full repo scan with local usage data |
725
747
  | `scan --optimizer-fit` | recommend which token-optimization path fits your repo/session |
@@ -757,6 +779,14 @@ npx getprismo doctor --json # machine-readable output
757
779
 
758
780
  ## watch modes
759
781
 
782
+ ```bash
783
+ npx getprismo guard # proactive local guard snapshot
784
+ npx getprismo guard --watch # keep guardrails active and sync prevention events
785
+ npx getprismo guard --no-sync # keep all guard events local
786
+ npx getprismo guard --dry-run # preview guard actions without writing state
787
+ npx getprismo guard --json # dashboard-ready guard payload
788
+ ```
789
+
760
790
  ```bash
761
791
  npx getprismo watch # live refresh
762
792
  npx getprismo watch --once # single snapshot
@@ -810,6 +840,7 @@ npx getprismo mcp /path/to/repo
810
840
  - `prismo_cursor_sessions`
811
841
  - `prismo_receipt`
812
842
  - `prismo_instructions_audit`
843
+ - `prismo_instructions_ablate`
813
844
  - `prismo_timeline`
814
845
  - `prismo_replay`
815
846
  - `prismo_boundaries`
@@ -990,7 +1021,7 @@ lib/prismo-dev/context-optimize.js context packs, scoped prompts
990
1021
  lib/prismo-dev/boundaries.js multi-agent boundary and worktree overlap checks
991
1022
  lib/prismo-dev/doctor.js doctor/dev/init orchestration
992
1023
  lib/prismo-dev/fixes.js safe ignore/template generation
993
- lib/prismo-dev/instructions.js instruction ROI and dead-rule analysis
1024
+ lib/prismo-dev/instructions.js instruction ROI, partial-compliance, and ablation planning
994
1025
  lib/prismo-dev/mcp.js local MCP server and Prismo tool bindings
995
1026
  lib/prismo-dev/receipt.js run receipts for reads, output, artifacts, and next scope
996
1027
  lib/prismo-dev/report.js terminal, markdown, ci reports
@@ -0,0 +1,492 @@
1
+ module.exports = function createCloudSync(deps) {
2
+ const {
3
+ fs,
4
+ http,
5
+ https,
6
+ os,
7
+ path,
8
+ PACKAGE_VERSION,
9
+ NPX_COMMAND,
10
+ getUsageSummary,
11
+ scanRepo,
12
+ } = deps;
13
+
14
+ const DEFAULT_API_URL = "https://api.getprismo.dev";
15
+ const CONFIG_VERSION = 1;
16
+
17
+ function prismoHome() {
18
+ return process.env.PRISMO_HOME || path.join(os.homedir(), ".prismo");
19
+ }
20
+
21
+ function configPath() {
22
+ return path.join(prismoHome(), "config.json");
23
+ }
24
+
25
+ function statePath() {
26
+ return path.join(prismoHome(), "sync-state.json");
27
+ }
28
+
29
+ function readJson(filePath) {
30
+ try {
31
+ if (!fs.existsSync(filePath)) return null;
32
+ return JSON.parse(fs.readFileSync(filePath, "utf8"));
33
+ } catch {
34
+ return null;
35
+ }
36
+ }
37
+
38
+ function writeJson(filePath, payload) {
39
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
40
+ fs.writeFileSync(filePath, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
41
+ }
42
+
43
+ function redactRemote(remote) {
44
+ const value = String(remote || "").trim();
45
+ if (!value) return null;
46
+ const githubSsh = value.match(/^git@github\.com:([^/]+)\/(.+?)(?:\.git)?$/i);
47
+ if (githubSsh) return `github.com/${githubSsh[1]}/${githubSsh[2].replace(/\.git$/i, "")}`;
48
+ try {
49
+ const parsed = new URL(value);
50
+ const host = parsed.hostname.replace(/^www\./, "");
51
+ const repo = parsed.pathname.replace(/^\/+/, "").replace(/\.git$/i, "");
52
+ return repo ? `${host}/${repo}` : host;
53
+ } catch {
54
+ return value.replace(/https?:\/\/[^@]+@/i, "https://").replace(/\.git$/i, "");
55
+ }
56
+ }
57
+
58
+ function runGit(root, args) {
59
+ try {
60
+ const { spawnSync } = require("child_process");
61
+ const result = spawnSync("git", args, { cwd: root, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
62
+ return result.status === 0 ? String(result.stdout || "").trim() : "";
63
+ } catch {
64
+ return "";
65
+ }
66
+ }
67
+
68
+ function repoIdentity(root) {
69
+ const resolved = path.resolve(root || process.cwd());
70
+ const remote = runGit(resolved, ["config", "--get", "remote.origin.url"]);
71
+ const branch = runGit(resolved, ["branch", "--show-current"]);
72
+ const commit = runGit(resolved, ["rev-parse", "--short=12", "HEAD"]);
73
+ return {
74
+ pathBasename: path.basename(resolved),
75
+ remote: redactRemote(remote),
76
+ branch: branch || null,
77
+ commit: commit || null,
78
+ };
79
+ }
80
+
81
+ function sumCounts(items) {
82
+ return (items || []).reduce((sum, item) => sum + Number(item.count || 0), 0);
83
+ }
84
+
85
+ function topCauseForSession(session) {
86
+ const toolTokens = Number(session.estimatedToolTokens || 0);
87
+ const repeatedReads = sumCounts(session.actionableRepeatedPaths && session.actionableRepeatedPaths.length ? session.actionableRepeatedPaths : session.repeatedPathMentions);
88
+ const artifacts = sumCounts(session.generatedArtifacts) + sumCounts(session.generatedArtifactGroups);
89
+ const repeatedCommands = sumCounts(session.repeatedCommands);
90
+ const candidates = [
91
+ { cause: "tool-output-flood", score: toolTokens / 25000 },
92
+ { cause: "repeated-file-reads", score: repeatedReads * 2000 },
93
+ { cause: "generated-artifacts", score: artifacts * 2500 },
94
+ { cause: "context-loop", score: (session.loopSuspicion ? 12000 : 0) + repeatedCommands * 3000 },
95
+ { cause: "long-session-buildup", score: session.contextRisk === "High" ? 20000 : session.contextRisk === "Medium" ? 8000 : 0 },
96
+ ].sort((a, b) => b.score - a.score);
97
+ return candidates[0] && candidates[0].score > 0 ? candidates[0].cause : "low-signal";
98
+ }
99
+
100
+ function estimateWaste(session) {
101
+ const tokens = Number(session.displayTokens || session.contextTokens || session.tokens || 0);
102
+ const toolTokens = Number(session.estimatedToolTokens || 0);
103
+ const repeatedReads = sumCounts(session.actionableRepeatedPaths && session.actionableRepeatedPaths.length ? session.actionableRepeatedPaths : session.repeatedPathMentions);
104
+ const artifacts = sumCounts(session.generatedArtifacts) + sumCounts(session.generatedArtifactGroups);
105
+ const repeatedCommands = sumCounts(session.repeatedCommands);
106
+ const riskDrag = session.contextRisk === "High" ? tokens * 0.18 : session.contextRisk === "Medium" ? tokens * 0.08 : 0;
107
+ const loopDrag = session.loopSuspicion ? tokens * 0.12 : 0;
108
+ const wasted = Math.min(tokens, Math.round(
109
+ toolTokens * 0.65 +
110
+ repeatedReads * 1800 +
111
+ artifacts * 2200 +
112
+ repeatedCommands * 2800 +
113
+ riskDrag +
114
+ loopDrag
115
+ ));
116
+ return {
117
+ tokens,
118
+ wastedTokens: Math.max(0, wasted),
119
+ wastePercent: tokens > 0 ? Math.round((Math.max(0, wasted) / tokens) * 100) : 0,
120
+ topCause: topCauseForSession(session),
121
+ };
122
+ }
123
+
124
+ function sanitizeSession(session, repo) {
125
+ const waste = estimateWaste(session);
126
+ return {
127
+ sessionId: session.sessionId || null,
128
+ title: session.title || null,
129
+ tool: session.tool || "unknown",
130
+ model: session.model || null,
131
+ repo,
132
+ startedAt: session.startedAt || null,
133
+ updatedAt: session.updatedAt || null,
134
+ turns: Number(session.turns || 0),
135
+ toolCalls: Number(session.toolCalls || 0),
136
+ toolResults: Number(session.toolResults || 0),
137
+ contextRisk: session.contextRisk || "Unknown",
138
+ confidence: session.confidence || "unknown",
139
+ tokens: {
140
+ display: Number(session.displayTokens || 0),
141
+ context: Number(session.contextTokens || 0),
142
+ exact: Number(session.exactTotalTokens || 0),
143
+ toolOutput: Number(session.estimatedToolTokens || 0),
144
+ },
145
+ waste,
146
+ signals: {
147
+ repeatedFileReads: sumCounts(session.actionableRepeatedPaths && session.actionableRepeatedPaths.length ? session.actionableRepeatedPaths : session.repeatedPathMentions),
148
+ generatedArtifactMentions: sumCounts(session.generatedArtifacts) + sumCounts(session.generatedArtifactGroups),
149
+ repeatedCommands: sumCounts(session.repeatedCommands),
150
+ loopSuspicion: Boolean(session.loopSuspicion),
151
+ },
152
+ };
153
+ }
154
+
155
+ function buildSyncPayload(rootDir = process.cwd(), options = {}) {
156
+ const root = path.resolve(rootDir);
157
+ const repo = repoIdentity(root);
158
+ const usage = getUsageSummary({
159
+ cwd: root,
160
+ tool: options.tool || "all",
161
+ limit: options.limit || 20,
162
+ });
163
+ let scan = null;
164
+ try {
165
+ scan = scanRepo(root, { includeUsage: false });
166
+ } catch {
167
+ scan = null;
168
+ }
169
+ const sessions = (usage.sessions || []).map((session) => sanitizeSession(session, repo));
170
+ const aggregate = sessions.reduce((acc, session) => {
171
+ acc.sessions += 1;
172
+ acc.displayTokens += session.tokens.display;
173
+ acc.contextTokens += session.tokens.context;
174
+ acc.exactTokens += session.tokens.exact;
175
+ acc.toolOutputTokens += session.tokens.toolOutput;
176
+ acc.estimatedWastedTokens += session.waste.wastedTokens;
177
+ return acc;
178
+ }, {
179
+ sessions: 0,
180
+ displayTokens: 0,
181
+ contextTokens: 0,
182
+ exactTokens: 0,
183
+ toolOutputTokens: 0,
184
+ estimatedWastedTokens: 0,
185
+ });
186
+ aggregate.wastePercent = aggregate.displayTokens > 0 ? Math.round((aggregate.estimatedWastedTokens / aggregate.displayTokens) * 100) : 0;
187
+
188
+ return {
189
+ schemaVersion: 1,
190
+ command: "sync",
191
+ generatedAt: new Date().toISOString(),
192
+ client: {
193
+ name: "prismodev",
194
+ version: PACKAGE_VERSION,
195
+ platform: `${os.platform()} ${os.arch()}`,
196
+ hostname: os.hostname(),
197
+ },
198
+ repo,
199
+ usage: {
200
+ confidence: usage.confidence,
201
+ sources: usage.sources || [],
202
+ totals: usage.totals || {},
203
+ },
204
+ scan: scan ? {
205
+ score: scan.score,
206
+ riskLevel: scan.risk,
207
+ tokenLeaks: scan.issues.length,
208
+ topTokenLeaks: scan.topTokenLeaks || [],
209
+ toolOutputRisk: scan.toolOutputRisk,
210
+ agentReadiness: scan.agentReadiness,
211
+ } : null,
212
+ aggregate,
213
+ sessions,
214
+ privacy: {
215
+ rawPrompts: false,
216
+ rawCode: false,
217
+ rawStdout: false,
218
+ rawStderr: false,
219
+ fileContents: false,
220
+ note: "Payload contains aggregate local telemetry only. It does not include prompts, source code, file contents, or command output.",
221
+ },
222
+ };
223
+ }
224
+
225
+ function requestJson(method, urlValue, token, payload, timeoutMs = 8000) {
226
+ return new Promise((resolve, reject) => {
227
+ let parsed;
228
+ try {
229
+ parsed = new URL(urlValue);
230
+ } catch {
231
+ reject(new Error(`Invalid URL: ${urlValue}`));
232
+ return;
233
+ }
234
+ const body = payload ? JSON.stringify(payload) : null;
235
+ const client = parsed.protocol === "https:" ? https : http;
236
+ const request = client.request({
237
+ method,
238
+ hostname: parsed.hostname,
239
+ port: parsed.port,
240
+ path: `${parsed.pathname}${parsed.search}`,
241
+ timeout: timeoutMs,
242
+ headers: {
243
+ "content-type": "application/json",
244
+ "user-agent": `prismodev/${PACKAGE_VERSION}`,
245
+ ...(token ? { authorization: `Bearer ${token}` } : {}),
246
+ ...(body ? { "content-length": Buffer.byteLength(body) } : {}),
247
+ },
248
+ }, (response) => {
249
+ const chunks = [];
250
+ response.on("data", (chunk) => chunks.push(chunk));
251
+ response.on("end", () => {
252
+ const text = Buffer.concat(chunks).toString("utf8");
253
+ let data = null;
254
+ try {
255
+ data = text ? JSON.parse(text) : null;
256
+ } catch {
257
+ data = { text };
258
+ }
259
+ if (response.statusCode >= 200 && response.statusCode < 300) {
260
+ resolve({ statusCode: response.statusCode, data });
261
+ } else {
262
+ reject(new Error(`HTTP ${response.statusCode}: ${text || response.statusMessage}`));
263
+ }
264
+ });
265
+ });
266
+ request.on("timeout", () => {
267
+ request.destroy();
268
+ reject(new Error("Request timed out"));
269
+ });
270
+ request.on("error", reject);
271
+ if (body) request.write(body);
272
+ request.end();
273
+ });
274
+ }
275
+
276
+ function loadConfig() {
277
+ return readJson(configPath());
278
+ }
279
+
280
+ function runConnect(options = {}) {
281
+ const apiUrl = String(options.apiUrl || process.env.PRISMO_API_URL || DEFAULT_API_URL).replace(/\/$/, "");
282
+ const token = options.token || process.env.PRISMO_API_KEY || process.env.PRISMO_DEV_TOKEN || null;
283
+ const existing = loadConfig();
284
+ const deviceName = options.device || existing?.device?.name || os.hostname();
285
+ const config = {
286
+ schemaVersion: CONFIG_VERSION,
287
+ connectedAt: new Date().toISOString(),
288
+ apiUrl,
289
+ org: options.org || existing?.org || null,
290
+ user: options.user || existing?.user || null,
291
+ device: {
292
+ id: existing?.device?.id || `${os.hostname()}-${Date.now().toString(36)}`,
293
+ name: deviceName,
294
+ platform: `${os.platform()} ${os.arch()}`,
295
+ },
296
+ token,
297
+ sync: {
298
+ enabled: Boolean(token),
299
+ defaultLimit: Number(options.limit || existing?.sync?.defaultLimit || 20),
300
+ },
301
+ };
302
+ writeJson(configPath(), config);
303
+ return {
304
+ schemaVersion: 1,
305
+ command: "connect",
306
+ connected: Boolean(token),
307
+ configPath: configPath(),
308
+ apiUrl,
309
+ org: config.org,
310
+ user: config.user,
311
+ device: config.device,
312
+ tokenStored: Boolean(token),
313
+ next: token
314
+ ? [`${NPX_COMMAND} sync`, `${NPX_COMMAND} status`]
315
+ : [
316
+ "Open the Prismo dashboard and create a PrismoDev device token.",
317
+ `${NPX_COMMAND} connect --token <token>`,
318
+ `${NPX_COMMAND} sync`,
319
+ ],
320
+ };
321
+ }
322
+
323
+ async function runSync(rootDir = process.cwd(), options = {}) {
324
+ const config = loadConfig();
325
+ const payload = buildSyncPayload(rootDir, {
326
+ limit: options.limit || config?.sync?.defaultLimit || 20,
327
+ tool: options.tool || "all",
328
+ });
329
+ if (options.dryRun || options.preview) {
330
+ return {
331
+ schemaVersion: 1,
332
+ command: "sync",
333
+ dryRun: true,
334
+ connected: Boolean(config?.token),
335
+ configPath: configPath(),
336
+ apiUrl: config?.apiUrl || null,
337
+ payload,
338
+ next: config?.token ? [`${NPX_COMMAND} sync`] : [`${NPX_COMMAND} connect --token <token>`],
339
+ };
340
+ }
341
+ if (!config || !config.token) {
342
+ return {
343
+ schemaVersion: 1,
344
+ command: "sync",
345
+ synced: false,
346
+ error: "not-connected",
347
+ configPath: configPath(),
348
+ next: [`${NPX_COMMAND} connect --token <token>`],
349
+ };
350
+ }
351
+ const endpoint = options.endpoint || `${String(config.apiUrl || DEFAULT_API_URL).replace(/\/$/, "")}/v1/dev/sessions/sync`;
352
+ const response = await requestJson("POST", endpoint, config.token, payload, options.timeoutMs || 8000);
353
+ const state = {
354
+ schemaVersion: 1,
355
+ lastSyncAt: new Date().toISOString(),
356
+ endpoint,
357
+ repo: payload.repo,
358
+ aggregate: payload.aggregate,
359
+ response: response.data,
360
+ };
361
+ writeJson(statePath(), state);
362
+ return {
363
+ schemaVersion: 1,
364
+ command: "sync",
365
+ synced: true,
366
+ endpoint,
367
+ statusCode: response.statusCode,
368
+ statePath: statePath(),
369
+ aggregate: payload.aggregate,
370
+ response: response.data,
371
+ };
372
+ }
373
+
374
+ function runStatus() {
375
+ const config = loadConfig();
376
+ const state = readJson(statePath());
377
+ return {
378
+ schemaVersion: 1,
379
+ command: "status",
380
+ connected: Boolean(config?.token),
381
+ configPath: configPath(),
382
+ apiUrl: config?.apiUrl || null,
383
+ org: config?.org || null,
384
+ user: config?.user || null,
385
+ device: config?.device || null,
386
+ syncEnabled: Boolean(config?.sync?.enabled),
387
+ lastSync: state || null,
388
+ next: config?.token ? [`${NPX_COMMAND} sync`] : [`${NPX_COMMAND} connect --token <token>`],
389
+ };
390
+ }
391
+
392
+ function runDisconnect() {
393
+ const existed = fs.existsSync(configPath());
394
+ const stateExisted = fs.existsSync(statePath());
395
+ if (existed) fs.rmSync(configPath(), { force: true });
396
+ if (stateExisted) fs.rmSync(statePath(), { force: true });
397
+ return {
398
+ schemaVersion: 1,
399
+ command: "disconnect",
400
+ disconnected: existed || stateExisted,
401
+ removed: [existed ? configPath() : null, stateExisted ? statePath() : null].filter(Boolean),
402
+ };
403
+ }
404
+
405
+ function renderConnectTerminal(result) {
406
+ const lines = [];
407
+ lines.push("");
408
+ lines.push("PrismoDev Connect");
409
+ lines.push("");
410
+ lines.push(`Status: ${result.connected ? "connected" : "token needed"}`);
411
+ lines.push(`Config: ${result.configPath}`);
412
+ lines.push(`API: ${result.apiUrl}`);
413
+ lines.push(`Device: ${result.device.name}`);
414
+ lines.push("");
415
+ lines.push("Next");
416
+ result.next.forEach((item, index) => lines.push(`${index + 1}. ${item}`));
417
+ return lines.join("\n");
418
+ }
419
+
420
+ function renderSyncTerminal(result) {
421
+ const lines = [];
422
+ lines.push("");
423
+ lines.push("PrismoDev Sync");
424
+ lines.push("");
425
+ if (result.dryRun) {
426
+ lines.push("Mode: dry run");
427
+ lines.push(`Connected: ${result.connected ? "yes" : "no"}`);
428
+ lines.push(`Sessions: ${result.payload.aggregate.sessions}`);
429
+ lines.push(`Observed tokens: ${result.payload.aggregate.displayTokens.toLocaleString()}`);
430
+ lines.push(`Likely wasted: ${result.payload.aggregate.estimatedWastedTokens.toLocaleString()} (${result.payload.aggregate.wastePercent}%)`);
431
+ } else if (result.synced) {
432
+ lines.push("Status: synced");
433
+ lines.push(`Endpoint: ${result.endpoint}`);
434
+ lines.push(`Sessions: ${result.aggregate.sessions}`);
435
+ lines.push(`Likely wasted: ${result.aggregate.estimatedWastedTokens.toLocaleString()} (${result.aggregate.wastePercent}%)`);
436
+ } else {
437
+ lines.push("Status: not connected");
438
+ lines.push(`Config: ${result.configPath}`);
439
+ }
440
+ if (result.next && result.next.length) {
441
+ lines.push("");
442
+ lines.push("Next");
443
+ result.next.forEach((item, index) => lines.push(`${index + 1}. ${item}`));
444
+ }
445
+ return lines.join("\n");
446
+ }
447
+
448
+ function renderStatusTerminal(result) {
449
+ const lines = [];
450
+ lines.push("");
451
+ lines.push("PrismoDev Status");
452
+ lines.push("");
453
+ lines.push(`Connected: ${result.connected ? "yes" : "no"}`);
454
+ lines.push(`Config: ${result.configPath}`);
455
+ if (result.apiUrl) lines.push(`API: ${result.apiUrl}`);
456
+ if (result.device) lines.push(`Device: ${result.device.name}`);
457
+ if (result.lastSync) {
458
+ lines.push(`Last sync: ${result.lastSync.lastSyncAt}`);
459
+ lines.push(`Sessions: ${result.lastSync.aggregate.sessions}`);
460
+ lines.push(`Likely wasted: ${result.lastSync.aggregate.estimatedWastedTokens.toLocaleString()} (${result.lastSync.aggregate.wastePercent}%)`);
461
+ } else {
462
+ lines.push("Last sync: never");
463
+ }
464
+ lines.push("");
465
+ lines.push("Next");
466
+ result.next.forEach((item, index) => lines.push(`${index + 1}. ${item}`));
467
+ return lines.join("\n");
468
+ }
469
+
470
+ function renderDisconnectTerminal(result) {
471
+ const lines = [];
472
+ lines.push("");
473
+ lines.push("PrismoDev Disconnect");
474
+ lines.push("");
475
+ lines.push(result.disconnected ? "Local PrismoDev connection removed." : "No local PrismoDev connection was found.");
476
+ return lines.join("\n");
477
+ }
478
+
479
+ return {
480
+ buildSyncPayload,
481
+ configPath,
482
+ loadConfig,
483
+ renderConnectTerminal,
484
+ renderDisconnectTerminal,
485
+ renderStatusTerminal,
486
+ renderSyncTerminal,
487
+ runConnect,
488
+ runDisconnect,
489
+ runStatus,
490
+ runSync,
491
+ };
492
+ };