getprismo 0.1.50 → 0.1.52

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.
@@ -15,6 +15,8 @@ This page lists **exactly** what the connector sends after you connect, field by
15
15
 
16
16
  ## What session sync sends (`prismo sync`, and the connector on an interval)
17
17
 
18
+ The background connector captures recent agent sessions from **every repo** on this machine (each attributed to its own repo), so all your agent activity is visible in one place — not just the repo the connector was started in. A one-off `prismo sync` covers only the current repo unless you pass `--all-repos`. Either way, only the numbers and labels below are sent; never code or prompts.
19
+
18
20
  Per machine:
19
21
 
20
22
  | field | example | note |
@@ -8,3 +8,8 @@ Paper cuts, observations, and metric notes from the measurement window. Fix bugs
8
8
  - 2026-06-12: follow-up dogfood found backend-summary.md could churn backups because generated .prismo context files influenced load-bearing text-reference counts. Excluded .prismo generated context from the reference corpus and added a regression test; repeated optimize on the SaaS repo now produces 0 new .bak files after the one-time old-report replacement.
9
9
  - 2026-06-12: connector dogfood found scoped `optimize frontend` and unscoped `optimize` could alternate optimize-report.md's Generated Files list and create metadata-only backups. Changed optimize-report metadata-only changes to update without backups and added a scoped/unscoped regression test.
10
10
  - 2026-06-12: connector dogfood found legitimate generated-context changes still left timestamped `.prismo/*.bak` files, which made the repo look dirty during validation. Changed `prismo optimize` context files to update in place because they are reproducible generated artifacts; user-authored files still keep backups.
11
+ - 2026-06-14: THREE real bugs found via dogfooding, all fixed + shipped (v0.1.47-0.1.51):
12
+ 1. Digest counted auto-detect health scans as interventions (186 fake vs 3 real).
13
+ 2. Verification baseline poisoned by Cursor's blind 0-signals -> now compares like-for-like across tools.
14
+ 3. CRITICAL: Claude Code telemetry silently dropped when repo path has a space/dot ("Code Projects") -> path encoding now matches Claude's. Was hiding the strongest signal for any user with a spaced path.
15
+ - 2026-06-14: KNOWN BUG (not yet fixed): `prismo protect` reinstalls/repoints the single connector even when one is already running for another repo, silently hijacking which repo syncs. Connector is single-repo; working across multiple repos means only the watched one syncs to the dashboard. Post-freeze: make protect not move a running connector, and consider syncing all recent repos.
@@ -752,7 +752,10 @@ module.exports = function createAgent(deps) {
752
752
  let syncResult = null;
753
753
  if (options.syncTelemetry) {
754
754
  try {
755
- const result = await runSync(rootDir, { limit: options.syncLimit || 20 });
755
+ // The connector is the always-on capturer: observe every repo's agent
756
+ // activity, not just the one it was started in (each session is
757
+ // attributed to its own repo). Opt out with allRepos: false.
758
+ const result = await runSync(rootDir, { limit: options.syncLimit || 20, allRepos: options.allRepos !== false });
756
759
  syncResult = {
757
760
  synced: Boolean(result.synced),
758
761
  sessions: Number(result.aggregate?.sessions || 0),
@@ -417,6 +417,7 @@ function createCli(deps) {
417
417
  preview: rest.includes("--preview"),
418
418
  limit: parsePositiveInt(limitIndex >= 0 ? rest[limitIndex + 1] : null, 20),
419
419
  tool: toolIndex >= 0 ? rest[toolIndex + 1] : "all",
420
+ allRepos: rest.includes("--all-repos"),
420
421
  endpoint: apiUrlIndex >= 0 ? `${String(rest[apiUrlIndex + 1] || "").replace(/\/$/, "")}/v1/dev/sessions/sync` : null,
421
422
  };
422
423
  if (rest.includes("--watch")) {
@@ -859,8 +860,15 @@ function createCli(deps) {
859
860
  const config = loadConfig();
860
861
  if (config?.token) {
861
862
  const connector = runConnectorStatus();
862
- if (connector.running) {
863
- steps.push({ step: "connector", ok: true, detail: "Background connector already running." });
863
+ if (connector.installed) {
864
+ // The connector already runs and now observes every repo's agent
865
+ // activity (all-repos sync), so do NOT repoint it to this repo —
866
+ // that would hijack which repo the dashboard treats as primary.
867
+ const watchedRoot = connector.state && connector.state.root;
868
+ const watched = watchedRoot && path.resolve(watchedRoot) !== path.resolve(target)
869
+ ? ` (watching ${path.basename(watchedRoot)}; it captures this repo too)`
870
+ : "";
871
+ steps.push({ step: "connector", ok: true, detail: `Background connector already installed${watched}.` });
864
872
  } else {
865
873
  runConnectorInstall(target, { mode: "autopilot" });
866
874
  steps.push({ step: "connector", ok: true, detail: "Background connector installed and started in autopilot." });
@@ -86,7 +86,18 @@ module.exports = function createCloudSync(deps) {
86
86
  }
87
87
  }
88
88
 
89
- function repoIdentity(root) {
89
+ function sameResolvedPath(a, b) {
90
+ if (!a || !b) return false;
91
+ try {
92
+ const ra = fs.existsSync(a) ? fs.realpathSync(a) : path.resolve(a);
93
+ const rb = fs.existsSync(b) ? fs.realpathSync(b) : path.resolve(b);
94
+ return ra === rb;
95
+ } catch {
96
+ return path.resolve(a) === path.resolve(b);
97
+ }
98
+ }
99
+
100
+ function repoIdentity(root, idOptions = {}) {
90
101
  const resolved = path.resolve(root || process.cwd());
91
102
  const remote = runGit(resolved, ["config", "--get", "remote.origin.url"]);
92
103
  const branch = runGit(resolved, ["branch", "--show-current"]);
@@ -96,7 +107,9 @@ module.exports = function createCloudSync(deps) {
96
107
  remote: redactRemote(remote),
97
108
  branch: branch || null,
98
109
  commit: commit || null,
99
- pr: detectPullRequest(resolved),
110
+ // PR detection shells out to `gh` per repo; skip it for secondary repos
111
+ // in all-repos sync so a 60s poll doesn't fan out gh calls everywhere.
112
+ pr: idOptions.detectPr === false ? null : detectPullRequest(resolved),
100
113
  };
101
114
  }
102
115
 
@@ -176,11 +189,13 @@ module.exports = function createCloudSync(deps) {
176
189
 
177
190
  function buildSyncPayload(rootDir = process.cwd(), options = {}) {
178
191
  const root = path.resolve(rootDir);
192
+ const allRepos = Boolean(options.allRepos);
179
193
  const repo = repoIdentity(root);
180
194
  const usage = getUsageSummary({
181
195
  cwd: root,
182
196
  tool: options.tool || "all",
183
197
  limit: options.limit || 20,
198
+ allRepos,
184
199
  });
185
200
  let scan = null;
186
201
  try {
@@ -188,7 +203,28 @@ module.exports = function createCloudSync(deps) {
188
203
  } catch {
189
204
  scan = null;
190
205
  }
191
- const sessions = (usage.sessions || []).map((session) => sanitizeSession(session, repo));
206
+
207
+ // In all-repos mode each session is attributed to its own repo, derived
208
+ // from the session's recorded cwd (git lookups cached per directory), so
209
+ // the dashboard groups activity by the repo it actually happened in.
210
+ const repoCache = new Map();
211
+ function repoForSession(session) {
212
+ if (!allRepos) return repo;
213
+ const sessionCwd = session && session.cwd ? path.resolve(session.cwd) : null;
214
+ if (!sessionCwd) return repo;
215
+ if (sameResolvedPath(sessionCwd, root)) return repo;
216
+ if (repoCache.has(sessionCwd)) return repoCache.get(sessionCwd);
217
+ let id;
218
+ try {
219
+ id = repoIdentity(sessionCwd, { detectPr: false });
220
+ } catch {
221
+ id = repo;
222
+ }
223
+ repoCache.set(sessionCwd, id);
224
+ return id;
225
+ }
226
+
227
+ const sessions = (usage.sessions || []).map((session) => sanitizeSession(session, repoForSession(session)));
192
228
  const aggregate = sessions.reduce((acc, session) => {
193
229
  acc.sessions += 1;
194
230
  acc.displayTokens += session.tokens.display;
@@ -347,6 +383,7 @@ module.exports = function createCloudSync(deps) {
347
383
  const payload = buildSyncPayload(rootDir, {
348
384
  limit: options.limit || config?.sync?.defaultLimit || 20,
349
385
  tool: options.tool || "all",
386
+ allRepos: Boolean(options.allRepos),
350
387
  });
351
388
  if (options.dryRun || options.preview) {
352
389
  return {
@@ -15,7 +15,7 @@ Usage:
15
15
  prismo connect [--json] [--token TOKEN] [--api-url URL] [--org ORG] [--user USER] [--device NAME]
16
16
  prismo connector [status|install|start|stop|uninstall] [--json] [--interval N] [--sync-interval N] [--mode observe|suggest|autopilot] [path]
17
17
  prismo bridge [--json] [path]
18
- prismo sync [--json] [--dry-run] [--watch] [--interval N] [--limit N] [--tool all|codex|claude|cursor] [path]
18
+ prismo sync [--json] [--dry-run] [--watch] [--all-repos] [--interval N] [--limit N] [--tool all|codex|claude|cursor] [path]
19
19
  prismo status [--json]
20
20
  prismo digest [--json] [--days N]
21
21
  prismo disconnect [--json]
@@ -485,7 +485,7 @@ Output:
485
485
  Usage:
486
486
  prismo connect [--json] [--token TOKEN] [--api-url URL] [--org ORG] [--user USER] [--device NAME]
487
487
  prismo connector [status|install|start|stop|uninstall] [--json] [--interval N] [--sync-interval N] [--mode observe|suggest|autopilot] [path]
488
- prismo sync [--json] [--dry-run] [--watch] [--interval N] [--limit N] [--tool all|codex|claude|cursor] [path]
488
+ prismo sync [--json] [--dry-run] [--watch] [--all-repos] [--interval N] [--limit N] [--tool all|codex|claude|cursor] [path]
489
489
  prismo status [--json]
490
490
  prismo disconnect [--json]
491
491
 
@@ -556,7 +556,7 @@ Privacy:
556
556
  sync: `PrismoDev Sync
557
557
 
558
558
  Usage:
559
- prismo sync [--json] [--dry-run] [--watch] [--interval N] [--limit N] [--tool all|codex|claude|cursor] [path]
559
+ prismo sync [--json] [--dry-run] [--watch] [--all-repos] [--interval N] [--limit N] [--tool all|codex|claude|cursor] [path]
560
560
 
561
561
  Examples:
562
562
  prismo sync --dry-run
@@ -228,9 +228,18 @@ function getClaudeSessionFiles(cwd = process.cwd()) {
228
228
  }
229
229
  const files = [];
230
230
  for (const candidate of Array.from(new Set(candidates))) {
231
- const safeProject = candidate.replace(/[\/\\:]/g, "-").replace(/^-/, "-");
232
- const projectDir = path.join(claudeHome, "projects", safeProject);
233
- files.push(...listFilesRecursive(projectDir, (file) => file.endsWith(".jsonl"), 200));
231
+ // Claude Code names a project folder by replacing every path separator,
232
+ // whitespace, and dot in the cwd with "-" (e.g. "/Users/me/Code Projects/app"
233
+ // -> "-Users-me-Code-Projects-app"). Matching only "/\:" missed any path
234
+ // containing a space or dot, silently dropping all Claude Code telemetry.
235
+ const encodings = new Set([
236
+ candidate.replace(/[\/\\:.\s]/g, "-"),
237
+ candidate.replace(/[\/\\:]/g, "-"), // legacy fallback for older folders
238
+ ]);
239
+ for (const safeProject of encodings) {
240
+ const projectDir = path.join(claudeHome, "projects", safeProject);
241
+ files.push(...listFilesRecursive(projectDir, (file) => file.endsWith(".jsonl"), 200));
242
+ }
234
243
  }
235
244
  return Array.from(new Set(files));
236
245
  }
@@ -248,24 +257,53 @@ function sameResolvedPath(a, b) {
248
257
  return false;
249
258
  }
250
259
  }
260
+ function safeMtimeMs(filePath) {
261
+ try {
262
+ return fs.statSync(filePath).mtimeMs;
263
+ } catch {
264
+ return 0;
265
+ }
266
+ }
267
+
251
268
  function getUsageSummary(options = {}) {
252
269
  const tool = options.tool || "all";
253
270
  const limit = options.limit || 5;
254
271
  const cwd = options.cwd || process.cwd();
272
+ // allRepos: capture the most recent sessions across every repo on this
273
+ // machine (each retains its own cwd for per-repo attribution), so a single
274
+ // always-on connector observes all of the user's agent activity rather than
275
+ // only the one repo it was started in. Cursor is skipped here because it
276
+ // exposes no per-session signal and cannot be attributed per repo.
277
+ const allRepos = Boolean(options.allRepos);
255
278
  const sessions = [];
256
279
  if (tool === "all" || tool === "codex") {
257
- for (const file of getCodexSessionFiles().slice(0, Math.max(limit * 8, 20))) {
258
- const session = analyzeSessionFile(file, "codex");
259
- if (!session.cwd || sameResolvedPath(session.cwd, cwd)) sessions.push(session);
260
- if (sessions.filter((item) => item.tool === "codex").length >= limit) break;
280
+ if (allRepos) {
281
+ const recent = getCodexSessionFiles()
282
+ .map((file) => ({ file, m: safeMtimeMs(file) }))
283
+ .sort((a, b) => b.m - a.m)
284
+ .slice(0, Math.max(limit, 30));
285
+ for (const { file } of recent) sessions.push(analyzeSessionFile(file, "codex"));
286
+ } else {
287
+ for (const file of getCodexSessionFiles().slice(0, Math.max(limit * 8, 20))) {
288
+ const session = analyzeSessionFile(file, "codex");
289
+ if (!session.cwd || sameResolvedPath(session.cwd, cwd)) sessions.push(session);
290
+ if (sessions.filter((item) => item.tool === "codex").length >= limit) break;
291
+ }
261
292
  }
262
293
  }
263
294
  if (tool === "all" || tool === "claude") {
264
- for (const file of getClaudeSessionFiles(cwd).slice(0, limit)) {
295
+ const claudeFiles = allRepos
296
+ ? getAllClaudeSessionFiles()
297
+ .map((file) => ({ file, m: safeMtimeMs(file) }))
298
+ .sort((a, b) => b.m - a.m)
299
+ .slice(0, Math.max(limit, 30))
300
+ .map((x) => x.file)
301
+ : getClaudeSessionFiles(cwd).slice(0, limit);
302
+ for (const file of claudeFiles) {
265
303
  sessions.push(analyzeSessionFile(file, "claude-code"));
266
304
  }
267
305
  }
268
- if (tool === "all" || tool === "cursor") {
306
+ if (!allRepos && (tool === "all" || tool === "cursor")) {
269
307
  try {
270
308
  const cursorData = analyzeCursorSessions({ limit, cwd });
271
309
  if (cursorData.sessions.length) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "getprismo",
3
- "version": "0.1.50",
3
+ "version": "0.1.52",
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",