@runtimescope/collector 0.9.2 → 0.9.3

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.
@@ -50,6 +50,12 @@ var RingBuffer = class {
50
50
  };
51
51
 
52
52
  // src/store.ts
53
+ function matchesSessionFilter(eventSessionId, filterSessionId) {
54
+ if (filterSessionId.includes(",")) {
55
+ return filterSessionId.split(",").includes(eventSessionId);
56
+ }
57
+ return eventSessionId === filterSessionId;
58
+ }
53
59
  var EventStore = class {
54
60
  buffer;
55
61
  sessions = /* @__PURE__ */ new Map();
@@ -110,7 +116,7 @@ var EventStore = class {
110
116
  const since = filter.sinceSeconds ? Date.now() - filter.sinceSeconds * 1e3 : 0;
111
117
  return this.buffer.query((e) => {
112
118
  if (e.eventType !== "network") return false;
113
- if (filter.sessionId && e.sessionId !== filter.sessionId) return false;
119
+ if (filter.sessionId && !matchesSessionFilter(e.sessionId, filter.sessionId)) return false;
114
120
  if (filter.projectId && !this.matchesProjectId(e.sessionId, filter.projectId)) return false;
115
121
  const ne = e;
116
122
  if (ne.timestamp < since) return false;
@@ -125,7 +131,7 @@ var EventStore = class {
125
131
  const since = filter.sinceSeconds ? Date.now() - filter.sinceSeconds * 1e3 : 0;
126
132
  return this.buffer.query((e) => {
127
133
  if (e.eventType !== "console") return false;
128
- if (filter.sessionId && e.sessionId !== filter.sessionId) return false;
134
+ if (filter.sessionId && !matchesSessionFilter(e.sessionId, filter.sessionId)) return false;
129
135
  if (filter.projectId && !this.matchesProjectId(e.sessionId, filter.projectId)) return false;
130
136
  const ce = e;
131
137
  if (ce.timestamp < since) return false;
@@ -147,7 +153,7 @@ var EventStore = class {
147
153
  const typeSet = filter.eventTypes ? new Set(filter.eventTypes) : null;
148
154
  return this.buffer.toArray().filter((e) => {
149
155
  if (e.timestamp < since) return false;
150
- if (filter.sessionId && e.sessionId !== filter.sessionId) return false;
156
+ if (filter.sessionId && !matchesSessionFilter(e.sessionId, filter.sessionId)) return false;
151
157
  if (filter.projectId && !this.matchesProjectId(e.sessionId, filter.projectId)) return false;
152
158
  if (typeSet && !typeSet.has(e.eventType)) return false;
153
159
  return true;
@@ -168,16 +174,28 @@ var EventStore = class {
168
174
  getSessionIdsForProjectId(projectId) {
169
175
  return Array.from(this.sessions.values()).filter((s) => s.projectId === projectId).map((s) => s.sessionId);
170
176
  }
177
+ /** Re-tag all sessions with oldProjectId to use newProjectId. */
178
+ retagSessions(oldProjectId, newProjectId) {
179
+ for (const [, session] of this.sessions) {
180
+ if (session.projectId === oldProjectId) {
181
+ session.projectId = newProjectId;
182
+ }
183
+ }
184
+ }
171
185
  /** Check if an event belongs to the given projectId (via its session). */
172
186
  matchesProjectId(sessionId, projectId) {
173
187
  const session = this.sessions.get(sessionId);
174
- return session?.projectId === projectId;
188
+ if (!session?.projectId) return false;
189
+ if (projectId.includes(",")) {
190
+ return projectId.split(",").includes(session.projectId);
191
+ }
192
+ return session.projectId === projectId;
175
193
  }
176
194
  getStateEvents(filter = {}) {
177
195
  const since = filter.sinceSeconds ? Date.now() - filter.sinceSeconds * 1e3 : 0;
178
196
  return this.buffer.query((e) => {
179
197
  if (e.eventType !== "state") return false;
180
- if (filter.sessionId && e.sessionId !== filter.sessionId) return false;
198
+ if (filter.sessionId && !matchesSessionFilter(e.sessionId, filter.sessionId)) return false;
181
199
  if (filter.projectId && !this.matchesProjectId(e.sessionId, filter.projectId)) return false;
182
200
  const se = e;
183
201
  if (se.timestamp < since) return false;
@@ -189,7 +207,7 @@ var EventStore = class {
189
207
  const since = filter.sinceSeconds ? Date.now() - filter.sinceSeconds * 1e3 : 0;
190
208
  return this.buffer.query((e) => {
191
209
  if (e.eventType !== "render") return false;
192
- if (filter.sessionId && e.sessionId !== filter.sessionId) return false;
210
+ if (filter.sessionId && !matchesSessionFilter(e.sessionId, filter.sessionId)) return false;
193
211
  if (filter.projectId && !this.matchesProjectId(e.sessionId, filter.projectId)) return false;
194
212
  const re = e;
195
213
  if (re.timestamp < since) return false;
@@ -206,7 +224,7 @@ var EventStore = class {
206
224
  const since = filter.sinceSeconds ? Date.now() - filter.sinceSeconds * 1e3 : 0;
207
225
  return this.buffer.query((e) => {
208
226
  if (e.eventType !== "performance") return false;
209
- if (filter.sessionId && e.sessionId !== filter.sessionId) return false;
227
+ if (filter.sessionId && !matchesSessionFilter(e.sessionId, filter.sessionId)) return false;
210
228
  if (filter.projectId && !this.matchesProjectId(e.sessionId, filter.projectId)) return false;
211
229
  const pe = e;
212
230
  if (pe.timestamp < since) return false;
@@ -218,7 +236,7 @@ var EventStore = class {
218
236
  const since = filter.sinceSeconds ? Date.now() - filter.sinceSeconds * 1e3 : 0;
219
237
  return this.buffer.query((e) => {
220
238
  if (e.eventType !== "database") return false;
221
- if (filter.sessionId && e.sessionId !== filter.sessionId) return false;
239
+ if (filter.sessionId && !matchesSessionFilter(e.sessionId, filter.sessionId)) return false;
222
240
  if (filter.projectId && !this.matchesProjectId(e.sessionId, filter.projectId)) return false;
223
241
  const de = e;
224
242
  if (de.timestamp < since) return false;
@@ -240,7 +258,7 @@ var EventStore = class {
240
258
  const since = filter.sinceSeconds ? Date.now() - filter.sinceSeconds * 1e3 : 0;
241
259
  return this.buffer.query((e) => {
242
260
  if (e.eventType !== "custom") return false;
243
- if (filter.sessionId && e.sessionId !== filter.sessionId) return false;
261
+ if (filter.sessionId && !matchesSessionFilter(e.sessionId, filter.sessionId)) return false;
244
262
  if (filter.projectId && !this.matchesProjectId(e.sessionId, filter.projectId)) return false;
245
263
  const ce = e;
246
264
  if (ce.timestamp < since) return false;
@@ -252,7 +270,7 @@ var EventStore = class {
252
270
  const since = filter.sinceSeconds ? Date.now() - filter.sinceSeconds * 1e3 : 0;
253
271
  return this.buffer.query((e) => {
254
272
  if (e.eventType !== "ui") return false;
255
- if (filter.sessionId && e.sessionId !== filter.sessionId) return false;
273
+ if (filter.sessionId && !matchesSessionFilter(e.sessionId, filter.sessionId)) return false;
256
274
  if (filter.projectId && !this.matchesProjectId(e.sessionId, filter.projectId)) return false;
257
275
  const ue = e;
258
276
  if (ue.timestamp < since) return false;
@@ -267,7 +285,7 @@ var EventStore = class {
267
285
  const since = filter.sinceSeconds ? Date.now() - filter.sinceSeconds * 1e3 : 0;
268
286
  const results = this.buffer.query((e) => {
269
287
  if (e.eventType !== eventType) return false;
270
- if (filter.sessionId && e.sessionId !== filter.sessionId) return false;
288
+ if (filter.sessionId && !matchesSessionFilter(e.sessionId, filter.sessionId)) return false;
271
289
  if (filter.projectId && !this.matchesProjectId(e.sessionId, filter.projectId)) return false;
272
290
  if (e.timestamp < since) return false;
273
291
  if (filter.url) {
@@ -282,7 +300,7 @@ var EventStore = class {
282
300
  const since = filter.sinceSeconds ? Date.now() - filter.sinceSeconds * 1e3 : 0;
283
301
  return this.buffer.query((e) => {
284
302
  if (e.eventType !== eventType) return false;
285
- if (filter.sessionId && e.sessionId !== filter.sessionId) return false;
303
+ if (filter.sessionId && !matchesSessionFilter(e.sessionId, filter.sessionId)) return false;
286
304
  if (filter.projectId && !this.matchesProjectId(e.sessionId, filter.projectId)) return false;
287
305
  if (e.timestamp < since) return false;
288
306
  if (filter.url) {
@@ -349,6 +367,18 @@ function getOrCreateProjectId(projectManager, appName) {
349
367
  projectManager.setProjectIdForApp(appName, projectId);
350
368
  return projectId;
351
369
  }
370
+ function resolveProjectId(projectManager, appName, pmStore) {
371
+ const fromIndex = projectManager.resolveAppProjectId(appName);
372
+ if (fromIndex) return fromIndex;
373
+ if (pmStore) {
374
+ const fromPm = pmStore.findProjectIdByApp(appName);
375
+ if (fromPm) {
376
+ projectManager.setProjectIdForApp(appName, fromPm);
377
+ return fromPm;
378
+ }
379
+ }
380
+ return getOrCreateProjectId(projectManager, appName);
381
+ }
352
382
 
353
383
  // src/sqlite-store.ts
354
384
  import { renameSync, existsSync } from "fs";
@@ -852,6 +882,7 @@ var CollectorServer = class {
852
882
  pruneTimer = null;
853
883
  heartbeatTimer = null;
854
884
  tlsConfig = null;
885
+ pmStore = null;
855
886
  constructor(options = {}) {
856
887
  this.store = new EventStore(options.bufferSize ?? 1e4);
857
888
  this.projectManager = options.projectManager ?? null;
@@ -887,6 +918,10 @@ var CollectorServer = class {
887
918
  getRateLimiter() {
888
919
  return this.rateLimiter;
889
920
  }
921
+ /** Set the PmStore for project ID resolution (called after construction when PmStore is available). */
922
+ setPmStore(pmStore) {
923
+ this.pmStore = pmStore;
924
+ }
890
925
  onConnect(cb) {
891
926
  this.connectCallbacks.push(cb);
892
927
  }
@@ -1071,7 +1106,7 @@ var CollectorServer = class {
1071
1106
  this.pendingHandshakes.delete(ws);
1072
1107
  }
1073
1108
  const projectName = payload.appName;
1074
- const projectId = payload.projectId ?? (this.projectManager ? getOrCreateProjectId(this.projectManager, projectName) : void 0);
1109
+ const projectId = payload.projectId ?? (this.projectManager ? resolveProjectId(this.projectManager, projectName, this.pmStore) : void 0);
1075
1110
  this.clients.set(ws, {
1076
1111
  sessionId: payload.sessionId,
1077
1112
  projectName,
@@ -1244,6 +1279,7 @@ var DEFAULT_GLOBAL_CONFIG = {
1244
1279
  };
1245
1280
  var ProjectManager = class {
1246
1281
  baseDir;
1282
+ appProjectIndex = /* @__PURE__ */ new Map();
1247
1283
  constructor(baseDir) {
1248
1284
  this.baseDir = baseDir ?? join(homedir(), ".runtimescope");
1249
1285
  }
@@ -1356,6 +1392,60 @@ var ProjectManager = class {
1356
1392
  }
1357
1393
  return null;
1358
1394
  }
1395
+ // --- Reverse index: appName → projectId ---
1396
+ /**
1397
+ * Build reverse index: appName -> projectId.
1398
+ * Scans all project configs, PM projects with runtimeApps + runtimeProjectId,
1399
+ * and project-level .runtimescope/config.json files from PM project paths.
1400
+ */
1401
+ rebuildAppIndex(pmStore) {
1402
+ this.appProjectIndex.clear();
1403
+ for (const name of this.listProjects()) {
1404
+ const config = this.getProjectConfig(name);
1405
+ if (config?.projectId) {
1406
+ this.appProjectIndex.set(name.toLowerCase(), config.projectId);
1407
+ }
1408
+ }
1409
+ if (pmStore) {
1410
+ for (const p of pmStore.listProjects()) {
1411
+ if (p.runtimeProjectId && p.runtimeApps) {
1412
+ for (const app of p.runtimeApps) {
1413
+ this.appProjectIndex.set(app.toLowerCase(), p.runtimeProjectId);
1414
+ }
1415
+ }
1416
+ }
1417
+ }
1418
+ if (pmStore) {
1419
+ for (const p of pmStore.listProjects()) {
1420
+ if (p.path) {
1421
+ try {
1422
+ const configPath = join(p.path, ".runtimescope", "config.json");
1423
+ if (existsSync2(configPath)) {
1424
+ const content = readFileSync2(configPath, "utf-8");
1425
+ const config = JSON.parse(content);
1426
+ if (config.projectId) {
1427
+ if (config.appName) {
1428
+ this.appProjectIndex.set(config.appName.toLowerCase(), config.projectId);
1429
+ }
1430
+ if (Array.isArray(config.sdks)) {
1431
+ for (const sdk of config.sdks) {
1432
+ if (sdk.appName) {
1433
+ this.appProjectIndex.set(sdk.appName.toLowerCase(), config.projectId);
1434
+ }
1435
+ }
1436
+ }
1437
+ }
1438
+ }
1439
+ } catch {
1440
+ }
1441
+ }
1442
+ }
1443
+ }
1444
+ }
1445
+ /** O(1) lookup from the cached index. */
1446
+ resolveAppProjectId(appName) {
1447
+ return this.appProjectIndex.get(appName.toLowerCase()) ?? null;
1448
+ }
1359
1449
  // --- Environment variable resolution ---
1360
1450
  resolveEnvVars(value) {
1361
1451
  return value.replace(/\$\{([^}]+)\}/g, (_match, varName) => {
@@ -1409,6 +1499,129 @@ var ProjectManager = class {
1409
1499
  }
1410
1500
  };
1411
1501
 
1502
+ // src/project-config.ts
1503
+ import { readFileSync as readFileSync3, existsSync as existsSync3, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2 } from "fs";
1504
+ import { join as join2 } from "path";
1505
+ var DEFAULT_CAPTURE = {
1506
+ network: true,
1507
+ console: true,
1508
+ xhr: true,
1509
+ body: false,
1510
+ performance: true,
1511
+ renders: true,
1512
+ navigation: true,
1513
+ clicks: false,
1514
+ http: false,
1515
+ errors: true,
1516
+ stackTraces: false
1517
+ };
1518
+ function readProjectConfig(projectDir) {
1519
+ const configPath = join2(projectDir, ".runtimescope", "config.json");
1520
+ if (!existsSync3(configPath)) return null;
1521
+ try {
1522
+ const content = readFileSync3(configPath, "utf-8");
1523
+ return JSON.parse(content);
1524
+ } catch {
1525
+ return null;
1526
+ }
1527
+ }
1528
+ function writeProjectConfig(projectDir, config) {
1529
+ const dir = join2(projectDir, ".runtimescope");
1530
+ if (!existsSync3(dir)) mkdirSync2(dir, { recursive: true });
1531
+ writeFileSync2(join2(dir, "config.json"), JSON.stringify(config, null, 2) + "\n", "utf-8");
1532
+ }
1533
+ function scaffoldProjectConfig(projectDir, opts) {
1534
+ const existing = readProjectConfig(projectDir);
1535
+ if (existing) {
1536
+ if (opts.sdkType) {
1537
+ const alreadyHas = existing.sdks.some((s) => s.type === opts.sdkType);
1538
+ if (!alreadyHas) {
1539
+ existing.sdks.push({
1540
+ type: opts.sdkType,
1541
+ framework: opts.framework,
1542
+ appName: opts.appName !== existing.appName ? opts.appName : void 0
1543
+ });
1544
+ writeProjectConfig(projectDir, existing);
1545
+ }
1546
+ }
1547
+ return existing;
1548
+ }
1549
+ const projectId = generateProjectId();
1550
+ const httpPort = process.env.RUNTIMESCOPE_HTTP_PORT ?? "9091";
1551
+ const dsn = `runtimescope://${projectId}@localhost:${httpPort}/${opts.appName}`;
1552
+ const config = {
1553
+ projectId,
1554
+ dsn,
1555
+ appName: opts.appName,
1556
+ description: opts.description,
1557
+ sdks: opts.sdkType ? [{ type: opts.sdkType, framework: opts.framework }] : [],
1558
+ capture: { ...DEFAULT_CAPTURE },
1559
+ category: opts.category
1560
+ };
1561
+ writeProjectConfig(projectDir, config);
1562
+ const gitignorePath = join2(projectDir, ".runtimescope", ".gitignore");
1563
+ if (!existsSync3(gitignorePath)) {
1564
+ writeFileSync2(gitignorePath, "# Keep config.json committed, ignore local state\n*.log\n*.db\n.env\n", "utf-8");
1565
+ }
1566
+ return config;
1567
+ }
1568
+ function resolveProjectAppNames(config) {
1569
+ const names = /* @__PURE__ */ new Set([config.appName]);
1570
+ for (const sdk of config.sdks) {
1571
+ if (sdk.appName) names.add(sdk.appName);
1572
+ }
1573
+ return Array.from(names);
1574
+ }
1575
+ function migrateProjectIds(projectManager, pmStore) {
1576
+ const result = { unified: 0, skipped: 0, details: [] };
1577
+ if (!pmStore) return result;
1578
+ for (const project of pmStore.listProjects()) {
1579
+ if (!project.runtimeApps || project.runtimeApps.length < 2) continue;
1580
+ if (!project.path) {
1581
+ result.skipped++;
1582
+ continue;
1583
+ }
1584
+ let canonicalId = null;
1585
+ try {
1586
+ const configPath = join2(project.path, ".runtimescope", "config.json");
1587
+ if (existsSync3(configPath)) {
1588
+ const config = JSON.parse(readFileSync3(configPath, "utf-8"));
1589
+ if (config.projectId) canonicalId = config.projectId;
1590
+ }
1591
+ } catch {
1592
+ }
1593
+ if (!canonicalId && project.runtimeProjectId) {
1594
+ canonicalId = project.runtimeProjectId;
1595
+ }
1596
+ if (!canonicalId) {
1597
+ for (const appName of project.runtimeApps) {
1598
+ const existing = projectManager.getProjectIdForApp(appName);
1599
+ if (existing) {
1600
+ canonicalId = existing;
1601
+ break;
1602
+ }
1603
+ }
1604
+ }
1605
+ if (!canonicalId) {
1606
+ result.skipped++;
1607
+ continue;
1608
+ }
1609
+ for (const appName of project.runtimeApps) {
1610
+ const current = projectManager.getProjectIdForApp(appName);
1611
+ if (current !== canonicalId) {
1612
+ projectManager.setProjectIdForApp(appName, canonicalId);
1613
+ result.details.push(`Unified ${appName}: ${current ?? "none"} \u2192 ${canonicalId}`);
1614
+ result.unified++;
1615
+ }
1616
+ }
1617
+ if (project.runtimeProjectId !== canonicalId) {
1618
+ pmStore.updateProject(project.id, { runtimeProjectId: canonicalId });
1619
+ result.details.push(`PM project ${project.name}: runtimeProjectId \u2192 ${canonicalId}`);
1620
+ }
1621
+ }
1622
+ return result;
1623
+ }
1624
+
1412
1625
  // src/auth.ts
1413
1626
  import { randomBytes as randomBytes2, timingSafeEqual } from "crypto";
1414
1627
  var AuthManager = class {
@@ -1573,7 +1786,7 @@ var Redactor = class {
1573
1786
  // src/platform.ts
1574
1787
  import { execFileSync, execSync } from "child_process";
1575
1788
  import { readlinkSync, readdirSync as readdirSync2 } from "fs";
1576
- import { join as join2 } from "path";
1789
+ import { join as join3 } from "path";
1577
1790
  var IS_WIN = process.platform === "win32";
1578
1791
  var IS_LINUX = process.platform === "linux";
1579
1792
  function runFile(cmd, args, timeoutMs = 5e3) {
@@ -1692,7 +1905,7 @@ function findPidsInDir_linux(dir) {
1692
1905
  const pid = parseInt(entry, 10);
1693
1906
  if (isNaN(pid) || pid <= 1) continue;
1694
1907
  try {
1695
- const cwd = readlinkSync(join2("/proc", entry, "cwd"));
1908
+ const cwd = readlinkSync(join3("/proc", entry, "cwd"));
1696
1909
  if (cwd.startsWith(dir)) pids.push(pid);
1697
1910
  } catch {
1698
1911
  }
@@ -1896,15 +2109,15 @@ var SessionManager = class {
1896
2109
  // src/http-server.ts
1897
2110
  import { createServer } from "http";
1898
2111
  import { createServer as createHttpsServer2 } from "https";
1899
- import { readFileSync as readFileSync3, existsSync as existsSync4 } from "fs";
2112
+ import { readFileSync as readFileSync4, existsSync as existsSync5 } from "fs";
1900
2113
  import { resolve, dirname } from "path";
1901
2114
  import { fileURLToPath } from "url";
1902
2115
  import { WebSocketServer as WebSocketServer2 } from "ws";
1903
2116
 
1904
2117
  // src/pm/pm-routes.ts
1905
2118
  import { readdir, readFile, writeFile, unlink, mkdir } from "fs/promises";
1906
- import { existsSync as existsSync3 } from "fs";
1907
- import { join as join3 } from "path";
2119
+ import { existsSync as existsSync4 } from "fs";
2120
+ import { join as join4 } from "path";
1908
2121
  import { homedir as homedir2 } from "os";
1909
2122
  import { spawn, execFileSync as execFileSync2 } from "child_process";
1910
2123
  var LOG_RING_SIZE = 500;
@@ -2041,6 +2254,16 @@ function createPmRouter(pmStore, discovery, helpers, broadcastDevServer) {
2041
2254
  helpers.json(res, { error: err.message }, 400);
2042
2255
  }
2043
2256
  });
2257
+ route("DELETE", "/api/pm/projects/:id", (_req, res, params) => {
2258
+ const id = params.get("id");
2259
+ const project = pmStore.getProject(id);
2260
+ if (!project) {
2261
+ helpers.json(res, { error: "Project not found" }, 404);
2262
+ return;
2263
+ }
2264
+ pmStore.deleteProject(id);
2265
+ helpers.json(res, { ok: true, deleted: project.name });
2266
+ });
2044
2267
  route("GET", "/api/pm/tasks", (_req, res, params) => {
2045
2268
  const projectId = params.get("project_id") ?? void 0;
2046
2269
  const status = params.get("status") ?? void 0;
@@ -2216,13 +2439,13 @@ function createPmRouter(pmStore, discovery, helpers, broadcastDevServer) {
2216
2439
  helpers.json(res, { data: [], count: 0 });
2217
2440
  return;
2218
2441
  }
2219
- const memoryDir = join3(homedir2(), ".claude", "projects", project.claudeProjectKey, "memory");
2442
+ const memoryDir = join4(homedir2(), ".claude", "projects", project.claudeProjectKey, "memory");
2220
2443
  try {
2221
2444
  const files = await readdir(memoryDir);
2222
2445
  const mdFiles = files.filter((f) => f.endsWith(".md"));
2223
2446
  const result = await Promise.all(
2224
2447
  mdFiles.map(async (filename) => {
2225
- const content = await readFile(join3(memoryDir, filename), "utf-8");
2448
+ const content = await readFile(join4(memoryDir, filename), "utf-8");
2226
2449
  return { filename, content, sizeBytes: Buffer.byteLength(content) };
2227
2450
  })
2228
2451
  );
@@ -2239,7 +2462,7 @@ function createPmRouter(pmStore, discovery, helpers, broadcastDevServer) {
2239
2462
  helpers.json(res, { error: "Project not found" }, 404);
2240
2463
  return;
2241
2464
  }
2242
- const filePath = join3(homedir2(), ".claude", "projects", project.claudeProjectKey, "memory", filename);
2465
+ const filePath = join4(homedir2(), ".claude", "projects", project.claudeProjectKey, "memory", filename);
2243
2466
  try {
2244
2467
  const content = await readFile(filePath, "utf-8");
2245
2468
  helpers.json(res, { filename, content, sizeBytes: Buffer.byteLength(content) });
@@ -2262,9 +2485,9 @@ function createPmRouter(pmStore, discovery, helpers, broadcastDevServer) {
2262
2485
  }
2263
2486
  try {
2264
2487
  const { content } = JSON.parse(body);
2265
- const memoryDir = join3(homedir2(), ".claude", "projects", project.claudeProjectKey, "memory");
2488
+ const memoryDir = join4(homedir2(), ".claude", "projects", project.claudeProjectKey, "memory");
2266
2489
  await mkdir(memoryDir, { recursive: true });
2267
- await writeFile(join3(memoryDir, filename), content, "utf-8");
2490
+ await writeFile(join4(memoryDir, filename), content, "utf-8");
2268
2491
  helpers.json(res, { ok: true });
2269
2492
  } catch (err) {
2270
2493
  helpers.json(res, { error: err.message }, 500);
@@ -2278,7 +2501,7 @@ function createPmRouter(pmStore, discovery, helpers, broadcastDevServer) {
2278
2501
  helpers.json(res, { error: "Project not found" }, 404);
2279
2502
  return;
2280
2503
  }
2281
- const filePath = join3(homedir2(), ".claude", "projects", project.claudeProjectKey, "memory", filename);
2504
+ const filePath = join4(homedir2(), ".claude", "projects", project.claudeProjectKey, "memory", filename);
2282
2505
  try {
2283
2506
  await unlink(filePath);
2284
2507
  helpers.json(res, { ok: true });
@@ -2338,7 +2561,7 @@ function createPmRouter(pmStore, discovery, helpers, broadcastDevServer) {
2338
2561
  const { content } = JSON.parse(body);
2339
2562
  const paths = getRulesPaths(project.claudeProjectKey, project.path);
2340
2563
  const filePath = paths[scope];
2341
- const dir = join3(filePath, "..");
2564
+ const dir = join4(filePath, "..");
2342
2565
  await mkdir(dir, { recursive: true });
2343
2566
  await writeFile(filePath, content, "utf-8");
2344
2567
  helpers.json(res, { ok: true });
@@ -2358,7 +2581,7 @@ function createPmRouter(pmStore, discovery, helpers, broadcastDevServer) {
2358
2581
  return;
2359
2582
  }
2360
2583
  try {
2361
- const pkgPath = join3(project.path, "package.json");
2584
+ const pkgPath = join4(project.path, "package.json");
2362
2585
  const pkg = JSON.parse(await readFile(pkgPath, "utf-8"));
2363
2586
  const scripts = pkg.scripts ?? {};
2364
2587
  const recommended = ["dev", "start", "serve"].find((s) => s in scripts) ?? null;
@@ -2774,6 +2997,71 @@ function createPmRouter(pmStore, discovery, helpers, broadcastDevServer) {
2774
2997
  res.end(csv);
2775
2998
  }
2776
2999
  });
3000
+ route("GET", "/api/pm/capex-all", (_req, res, params) => {
3001
+ const category = params.get("category") ?? void 0;
3002
+ const projects = pmStore.listProjects();
3003
+ const filteredProjects = category ? projects.filter((p) => p.category === category) : projects;
3004
+ const allEntries = [];
3005
+ let totalCost = 0;
3006
+ let totalCapitalizable = 0;
3007
+ let totalExpensed = 0;
3008
+ let totalMinutes = 0;
3009
+ let totalConfirmed = 0;
3010
+ let totalUnconfirmed = 0;
3011
+ const byProject = [];
3012
+ for (const project of filteredProjects) {
3013
+ const entries = pmStore.listCapexEntries(project.id);
3014
+ let projCost = 0;
3015
+ let projCap = 0;
3016
+ let projExp = 0;
3017
+ let projMins = 0;
3018
+ let projConfirmed = 0;
3019
+ for (const e of entries) {
3020
+ allEntries.push({ ...e, projectName: project.name });
3021
+ projCost += e.adjustedCostMicrodollars;
3022
+ projMins += e.activeMinutes;
3023
+ if (e.classification === "capitalizable") projCap += e.adjustedCostMicrodollars;
3024
+ else projExp += e.adjustedCostMicrodollars;
3025
+ if (e.confirmed) projConfirmed++;
3026
+ }
3027
+ if (entries.length > 0) {
3028
+ byProject.push({
3029
+ projectId: project.id,
3030
+ projectName: project.name,
3031
+ category: project.category,
3032
+ totalCost: projCost,
3033
+ capitalizable: projCap,
3034
+ expensed: projExp,
3035
+ activeMinutes: projMins,
3036
+ activeHours: +(projMins / 60).toFixed(2),
3037
+ confirmed: projConfirmed,
3038
+ total: entries.length
3039
+ });
3040
+ }
3041
+ totalCost += projCost;
3042
+ totalCapitalizable += projCap;
3043
+ totalExpensed += projExp;
3044
+ totalMinutes += projMins;
3045
+ totalConfirmed += projConfirmed;
3046
+ totalUnconfirmed += entries.length - projConfirmed;
3047
+ }
3048
+ helpers.json(res, {
3049
+ data: {
3050
+ summary: {
3051
+ totalCost,
3052
+ capitalizable: totalCapitalizable,
3053
+ expensed: totalExpensed,
3054
+ activeMinutes: totalMinutes,
3055
+ activeHours: +(totalMinutes / 60).toFixed(2),
3056
+ confirmed: totalConfirmed,
3057
+ unconfirmed: totalUnconfirmed,
3058
+ projectCount: byProject.length
3059
+ },
3060
+ byProject,
3061
+ entries: allEntries.sort((a, b) => b.createdAt - a.createdAt)
3062
+ }
3063
+ });
3064
+ });
2777
3065
  route("GET", "/api/pm/capex-report-all", async (_req, res, params) => {
2778
3066
  const startDate = params.get("start_date") ?? void 0;
2779
3067
  const endDate = params.get("end_date") ?? void 0;
@@ -2819,9 +3107,9 @@ function sanitizeFilename(name) {
2819
3107
  function getRulesPaths(claudeProjectKey, projectPath) {
2820
3108
  const home = homedir2();
2821
3109
  return {
2822
- global: join3(home, ".claude", "CLAUDE.md"),
2823
- project: claudeProjectKey ? join3(home, ".claude", "projects", claudeProjectKey, "CLAUDE.md") : join3(projectPath ?? "", ".claude", "CLAUDE.md"),
2824
- local: projectPath ? join3(projectPath, "CLAUDE.md") : join3(home, "CLAUDE.md")
3110
+ global: join4(home, ".claude", "CLAUDE.md"),
3111
+ project: claudeProjectKey ? join4(home, ".claude", "projects", claudeProjectKey, "CLAUDE.md") : join4(projectPath ?? "", ".claude", "CLAUDE.md"),
3112
+ local: projectPath ? join4(projectPath, "CLAUDE.md") : join4(home, "CLAUDE.md")
2825
3113
  };
2826
3114
  }
2827
3115
  function execGit(args, cwd) {
@@ -2866,7 +3154,7 @@ function parseGitStatus(porcelain) {
2866
3154
  }
2867
3155
  async function readRuleFile(filePath) {
2868
3156
  try {
2869
- if (existsSync3(filePath)) {
3157
+ if (existsSync4(filePath)) {
2870
3158
  const content = await readFile(filePath, "utf-8");
2871
3159
  return { path: filePath, content, exists: true };
2872
3160
  }
@@ -3111,7 +3399,7 @@ var HttpServer = class {
3111
3399
  return;
3112
3400
  }
3113
3401
  const payload = parsed;
3114
- const projectId = typeof payload.projectId === "string" ? payload.projectId : payload.appName && this.projectManager ? getOrCreateProjectId(this.projectManager, payload.appName) : void 0;
3402
+ const projectId = typeof payload.projectId === "string" ? payload.projectId : payload.appName && this.projectManager ? resolveProjectId(this.projectManager, payload.appName, this.pmStore) : void 0;
3115
3403
  if (!payload.sessionId || !Array.isArray(payload.events) || payload.events.length === 0) {
3116
3404
  this.json(res, {
3117
3405
  error: "Required: sessionId (string), events (non-empty array)",
@@ -3196,7 +3484,7 @@ var HttpServer = class {
3196
3484
  // npm installed
3197
3485
  ];
3198
3486
  for (const p of candidates) {
3199
- if (existsSync4(p)) {
3487
+ if (existsSync5(p)) {
3200
3488
  this.sdkBundlePath = p;
3201
3489
  return p;
3202
3490
  }
@@ -3344,7 +3632,7 @@ var HttpServer = class {
3344
3632
  if (req.method === "GET" && url.pathname === "/runtimescope.js") {
3345
3633
  const sdkPath = this.resolveSdkPath();
3346
3634
  if (sdkPath) {
3347
- const bundle = readFileSync3(sdkPath, "utf-8");
3635
+ const bundle = readFileSync4(sdkPath, "utf-8");
3348
3636
  res.writeHead(200, {
3349
3637
  "Content-Type": "application/javascript",
3350
3638
  "Cache-Control": "no-cache"
@@ -3358,14 +3646,12 @@ var HttpServer = class {
3358
3646
  }
3359
3647
  if (req.method === "GET" && url.pathname === "/snippet") {
3360
3648
  const appName = (url.searchParams.get("app") || "my-app").replace(/[^a-zA-Z0-9_-]/g, "");
3361
- const wsPort = process.env.RUNTIMESCOPE_PORT ?? "9090";
3649
+ const projectId = url.searchParams.get("project_id") || "proj_xxx";
3650
+ const dsn = `runtimescope://${projectId}@localhost:${this.activePort}/${appName}`;
3362
3651
  const snippet = `<!-- RuntimeScope SDK \u2014 paste before </body> -->
3363
3652
  <script src="http://localhost:${this.activePort}/runtimescope.js"></script>
3364
3653
  <script>
3365
- RuntimeScope.init({
3366
- appName: '${appName}',
3367
- endpoint: 'ws://localhost:${wsPort}',
3368
- });
3654
+ RuntimeScope.init({ dsn: '${dsn}' });
3369
3655
  </script>`;
3370
3656
  res.writeHead(200, {
3371
3657
  "Content-Type": "text/plain"
@@ -3595,6 +3881,13 @@ var PmStore = class {
3595
3881
  CREATE INDEX IF NOT EXISTS idx_pm_capex_project ON pm_capex_entries(project_id);
3596
3882
  CREATE INDEX IF NOT EXISTS idx_pm_capex_period ON pm_capex_entries(period);
3597
3883
  CREATE INDEX IF NOT EXISTS idx_pm_capex_confirmed ON pm_capex_entries(confirmed);
3884
+
3885
+ CREATE TABLE IF NOT EXISTS pm_deleted_projects (
3886
+ path TEXT PRIMARY KEY,
3887
+ name TEXT,
3888
+ deleted_at INTEGER NOT NULL
3889
+ );
3890
+ CREATE INDEX IF NOT EXISTS idx_deleted_path ON pm_deleted_projects(path);
3598
3891
  `);
3599
3892
  }
3600
3893
  runMigrations() {
@@ -3762,6 +4055,11 @@ var PmStore = class {
3762
4055
  this.updateProject(match.id, updates);
3763
4056
  return match.id;
3764
4057
  }
4058
+ /** Find a project's runtimeProjectId by checking if appName appears in any project's runtimeApps. */
4059
+ findProjectIdByApp(appName) {
4060
+ const row = this.db.prepare(`SELECT runtime_project_id FROM pm_projects WHERE runtime_apps LIKE ? AND runtime_project_id IS NOT NULL LIMIT 1`).get(`%"${appName}"%`);
4061
+ return row?.runtime_project_id ?? null;
4062
+ }
3765
4063
  listCategories() {
3766
4064
  const rows = this.db.prepare("SELECT DISTINCT category FROM pm_projects WHERE category IS NOT NULL ORDER BY category ASC").all();
3767
4065
  return rows.map((r) => r.category);
@@ -4521,6 +4819,38 @@ var PmStore = class {
4521
4819
  };
4522
4820
  }
4523
4821
  // ============================================================
4822
+ // Deleted Projects Blocklist
4823
+ // ============================================================
4824
+ deleteProject(id) {
4825
+ const project = this.getProject(id);
4826
+ if (!project) return;
4827
+ if (project.path) {
4828
+ this.db.prepare(
4829
+ "INSERT OR REPLACE INTO pm_deleted_projects (path, name, deleted_at) VALUES (?, ?, ?)"
4830
+ ).run(project.path, project.name, Date.now());
4831
+ }
4832
+ if (project.claudeProjectKey) {
4833
+ this.db.prepare(
4834
+ "INSERT OR REPLACE INTO pm_deleted_projects (path, name, deleted_at) VALUES (?, ?, ?)"
4835
+ ).run(project.claudeProjectKey, project.name, Date.now());
4836
+ }
4837
+ this.db.prepare("DELETE FROM pm_capex_entries WHERE project_id = ?").run(id);
4838
+ this.db.prepare("DELETE FROM pm_notes WHERE project_id = ?").run(id);
4839
+ this.db.prepare("DELETE FROM pm_tasks WHERE project_id = ?").run(id);
4840
+ this.db.prepare("DELETE FROM pm_sessions WHERE project_id = ?").run(id);
4841
+ this.db.prepare("DELETE FROM pm_projects WHERE id = ?").run(id);
4842
+ }
4843
+ isDeletedPath(path) {
4844
+ const row = this.db.prepare("SELECT 1 FROM pm_deleted_projects WHERE path = ?").get(path);
4845
+ return !!row;
4846
+ }
4847
+ recoverProject(path) {
4848
+ this.db.prepare("DELETE FROM pm_deleted_projects WHERE path = ?").run(path);
4849
+ }
4850
+ listDeletedProjects() {
4851
+ return this.db.prepare("SELECT path, name, deleted_at as deletedAt FROM pm_deleted_projects ORDER BY deleted_at DESC").all();
4852
+ }
4853
+ // ============================================================
4524
4854
  // Cleanup
4525
4855
  // ============================================================
4526
4856
  close() {
@@ -4733,13 +5063,13 @@ async function parseSessionJsonl(jsonlPath, sessionId, projectId) {
4733
5063
 
4734
5064
  // src/pm/project-discovery.ts
4735
5065
  import { readdir as readdir2, readFile as readFile2, stat as stat2 } from "fs/promises";
4736
- import { join as join4, basename as basename2 } from "path";
4737
- import { existsSync as existsSync5 } from "fs";
5066
+ import { join as join5, basename as basename2 } from "path";
5067
+ import { existsSync as existsSync6 } from "fs";
4738
5068
  import { homedir as homedir3 } from "os";
4739
5069
  var LOG_PREFIX = "[RuntimeScope PM]";
4740
5070
  async function detectSdkInstalled(projectPath) {
4741
5071
  try {
4742
- const pkgPath = join4(projectPath, "package.json");
5072
+ const pkgPath = join5(projectPath, "package.json");
4743
5073
  const pkg = JSON.parse(await readFile2(pkgPath, "utf-8"));
4744
5074
  const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
4745
5075
  if ("@runtimescope/sdk" in allDeps || "@runtimescope/server-sdk" in allDeps) {
@@ -4749,13 +5079,13 @@ async function detectSdkInstalled(projectPath) {
4749
5079
  const workspaces = Array.isArray(pkg.workspaces) ? pkg.workspaces : pkg.workspaces.packages ?? [];
4750
5080
  for (const ws of workspaces) {
4751
5081
  const wsBase = ws.replace(/\/?\*$/, "");
4752
- const wsDir = join4(projectPath, wsBase);
5082
+ const wsDir = join5(projectPath, wsBase);
4753
5083
  try {
4754
5084
  const entries = await readdir2(wsDir, { withFileTypes: true });
4755
5085
  for (const entry of entries) {
4756
5086
  if (!entry.isDirectory()) continue;
4757
5087
  try {
4758
- const wsPkg = JSON.parse(await readFile2(join4(wsDir, entry.name, "package.json"), "utf-8"));
5088
+ const wsPkg = JSON.parse(await readFile2(join5(wsDir, entry.name, "package.json"), "utf-8"));
4759
5089
  const wsDeps = { ...wsPkg.dependencies, ...wsPkg.devDependencies };
4760
5090
  if ("@runtimescope/sdk" in wsDeps || "@runtimescope/server-sdk" in wsDeps) {
4761
5091
  return true;
@@ -4770,7 +5100,7 @@ async function detectSdkInstalled(projectPath) {
4770
5100
  } catch {
4771
5101
  }
4772
5102
  try {
4773
- await stat2(join4(projectPath, "node_modules", "@runtimescope"));
5103
+ await stat2(join5(projectPath, "node_modules", "@runtimescope"));
4774
5104
  return true;
4775
5105
  } catch {
4776
5106
  return false;
@@ -4801,7 +5131,7 @@ function slugifyPath(fsPath) {
4801
5131
  }
4802
5132
  function decodeClaudeKey(key) {
4803
5133
  const naive = "/" + key.slice(1).replace(/-/g, "/");
4804
- if (existsSync5(naive)) return naive;
5134
+ if (existsSync6(naive)) return naive;
4805
5135
  const parts = key.slice(1).split("-");
4806
5136
  return resolvePathSegments(parts);
4807
5137
  }
@@ -4809,16 +5139,16 @@ function resolvePathSegments(parts) {
4809
5139
  if (parts.length === 0) return null;
4810
5140
  function tryResolve(prefix, remaining) {
4811
5141
  if (remaining.length === 0) {
4812
- return existsSync5(prefix) ? prefix : null;
5142
+ return existsSync6(prefix) ? prefix : null;
4813
5143
  }
4814
5144
  for (let count = remaining.length; count >= 1; count--) {
4815
5145
  const segment = remaining.slice(0, count).join("-");
4816
- const candidate = join4(prefix, segment);
5146
+ const candidate = join5(prefix, segment);
4817
5147
  if (count === remaining.length) {
4818
- if (existsSync5(candidate)) return candidate;
5148
+ if (existsSync6(candidate)) return candidate;
4819
5149
  } else {
4820
5150
  try {
4821
- if (existsSync5(candidate)) {
5151
+ if (existsSync6(candidate)) {
4822
5152
  const result = tryResolve(candidate, remaining.slice(count));
4823
5153
  if (result) return result;
4824
5154
  }
@@ -4834,13 +5164,14 @@ function toPeriod(timestampMs) {
4834
5164
  const d = new Date(timestampMs);
4835
5165
  const year = d.getFullYear();
4836
5166
  const month = String(d.getMonth() + 1).padStart(2, "0");
4837
- return `${year}-${month}`;
5167
+ const day = String(d.getDate()).padStart(2, "0");
5168
+ return `${year}-${month}-${day}`;
4838
5169
  }
4839
5170
  var ProjectDiscovery = class {
4840
5171
  constructor(pmStore, projectManager, claudeBaseDir) {
4841
5172
  this.pmStore = pmStore;
4842
5173
  this.projectManager = projectManager;
4843
- this.claudeBaseDir = claudeBaseDir ?? join4(homedir3(), ".claude");
5174
+ this.claudeBaseDir = claudeBaseDir ?? join5(homedir3(), ".claude");
4844
5175
  }
4845
5176
  claudeBaseDir;
4846
5177
  /**
@@ -4873,7 +5204,7 @@ var ProjectDiscovery = class {
4873
5204
  sessionsUpdated: 0,
4874
5205
  errors: []
4875
5206
  };
4876
- const projectsDir = join4(this.claudeBaseDir, "projects");
5207
+ const projectsDir = join5(this.claudeBaseDir, "projects");
4877
5208
  try {
4878
5209
  await stat2(projectsDir);
4879
5210
  } catch {
@@ -4940,6 +5271,7 @@ var ProjectDiscovery = class {
4940
5271
  await this.pmStore.upsertProject(updated);
4941
5272
  result.projectsUpdated = (result.projectsUpdated ?? 0) + 1;
4942
5273
  } else {
5274
+ if (this.pmStore.isDeletedPath(sourcePath)) continue;
4943
5275
  const fsPath = projectDir;
4944
5276
  const project = {
4945
5277
  id,
@@ -4985,10 +5317,10 @@ var ProjectDiscovery = class {
4985
5317
  if (!project.claudeProjectKey) {
4986
5318
  return 0;
4987
5319
  }
4988
- const projectDir = join4(this.claudeBaseDir, "projects", project.claudeProjectKey);
5320
+ const projectDir = join5(this.claudeBaseDir, "projects", project.claudeProjectKey);
4989
5321
  let sessionsIndexed = 0;
4990
5322
  try {
4991
- const indexPath = join4(projectDir, "sessions-index.json");
5323
+ const indexPath = join5(projectDir, "sessions-index.json");
4992
5324
  let indexEntries = null;
4993
5325
  try {
4994
5326
  const indexContent = await readFile2(indexPath, "utf-8");
@@ -5001,7 +5333,7 @@ var ProjectDiscovery = class {
5001
5333
  for (const jsonlFile of jsonlFiles) {
5002
5334
  try {
5003
5335
  const sessionId = jsonlFile.replace(".jsonl", "");
5004
- const jsonlPath = join4(projectDir, jsonlFile);
5336
+ const jsonlPath = join5(projectDir, jsonlFile);
5005
5337
  const fileStat = await stat2(jsonlPath);
5006
5338
  const fileSize = fileStat.size;
5007
5339
  const existingSession = await this.pmStore.getSession(sessionId);
@@ -5036,7 +5368,7 @@ var ProjectDiscovery = class {
5036
5368
  * Process a single Claude project directory key.
5037
5369
  */
5038
5370
  async processClaudeProject(key, result) {
5039
- const projectDir = join4(this.claudeBaseDir, "projects", key);
5371
+ const projectDir = join5(this.claudeBaseDir, "projects", key);
5040
5372
  let fsPath = decodeClaudeKey(key);
5041
5373
  if (!fsPath) {
5042
5374
  fsPath = await this.resolvePathFromIndex(projectDir);
@@ -5062,6 +5394,8 @@ var ProjectDiscovery = class {
5062
5394
  await this.pmStore.upsertProject(updated);
5063
5395
  result.projectsUpdated = (result.projectsUpdated ?? 0) + 1;
5064
5396
  } else {
5397
+ if (fsPath && this.pmStore.isDeletedPath(fsPath)) return;
5398
+ if (this.pmStore.isDeletedPath(key)) return;
5065
5399
  const project = {
5066
5400
  id,
5067
5401
  name,
@@ -5088,7 +5422,7 @@ var ProjectDiscovery = class {
5088
5422
  */
5089
5423
  async resolvePathFromIndex(projectDir) {
5090
5424
  try {
5091
- const indexPath = join4(projectDir, "sessions-index.json");
5425
+ const indexPath = join5(projectDir, "sessions-index.json");
5092
5426
  const content = await readFile2(indexPath, "utf-8");
5093
5427
  const index = JSON.parse(content);
5094
5428
  const entry = index.entries?.find((e) => e.projectPath);
@@ -5103,11 +5437,11 @@ var ProjectDiscovery = class {
5103
5437
  */
5104
5438
  async indexSessionsForClaudeProject(projectId, claudeKey) {
5105
5439
  const counts = { discovered: 0, updated: 0 };
5106
- const projectDir = join4(this.claudeBaseDir, "projects", claudeKey);
5440
+ const projectDir = join5(this.claudeBaseDir, "projects", claudeKey);
5107
5441
  try {
5108
5442
  let indexEntries = null;
5109
5443
  try {
5110
- const indexPath = join4(projectDir, "sessions-index.json");
5444
+ const indexPath = join5(projectDir, "sessions-index.json");
5111
5445
  const indexContent = await readFile2(indexPath, "utf-8");
5112
5446
  const index = JSON.parse(indexContent);
5113
5447
  indexEntries = index.entries ?? [];
@@ -5118,7 +5452,7 @@ var ProjectDiscovery = class {
5118
5452
  for (const jsonlFile of jsonlFiles) {
5119
5453
  try {
5120
5454
  const sessionId = jsonlFile.replace(".jsonl", "");
5121
- const jsonlPath = join4(projectDir, jsonlFile);
5455
+ const jsonlPath = join5(projectDir, jsonlFile);
5122
5456
  const fileStat = await stat2(jsonlPath);
5123
5457
  const fileSize = fileStat.size;
5124
5458
  const existingSession = await this.pmStore.getSession(sessionId);
@@ -5308,6 +5642,11 @@ export {
5308
5642
  resolveTlsConfig,
5309
5643
  CollectorServer,
5310
5644
  ProjectManager,
5645
+ readProjectConfig,
5646
+ writeProjectConfig,
5647
+ scaffoldProjectConfig,
5648
+ resolveProjectAppNames,
5649
+ migrateProjectIds,
5311
5650
  AuthManager,
5312
5651
  generateApiKey,
5313
5652
  BUILT_IN_RULES,
@@ -5326,4 +5665,4 @@ export {
5326
5665
  parseSessionJsonl,
5327
5666
  ProjectDiscovery
5328
5667
  };
5329
- //# sourceMappingURL=chunk-GENCCHYK.js.map
5668
+ //# sourceMappingURL=chunk-MM44DN7Y.js.map