@runtimescope/collector 0.9.2 → 0.10.0

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
  }
@@ -894,7 +929,7 @@ var CollectorServer = class {
894
929
  this.disconnectCallbacks.push(cb);
895
930
  }
896
931
  start(options = {}) {
897
- const port = options.port ?? 9090;
932
+ const port = options.port ?? 6767;
898
933
  const host = options.host ?? "127.0.0.1";
899
934
  const maxRetries = options.maxRetries ?? 5;
900
935
  const retryDelayMs = options.retryDelayMs ?? 1e3;
@@ -1055,7 +1090,15 @@ var CollectorServer = class {
1055
1090
  switch (msg.type) {
1056
1091
  case "handshake": {
1057
1092
  const payload = msg.payload;
1058
- if (this.authManager?.isEnabled()) {
1093
+ let workspaceFromKey = null;
1094
+ if (payload.authToken && this.pmStore?.getWorkspaceByApiKey) {
1095
+ try {
1096
+ const ws2 = this.pmStore.getWorkspaceByApiKey(payload.authToken);
1097
+ if (ws2) workspaceFromKey = { id: ws2.id, slug: ws2.slug };
1098
+ } catch {
1099
+ }
1100
+ }
1101
+ if (this.authManager?.isEnabled() && !workspaceFromKey) {
1059
1102
  if (!this.authManager.isAuthorized(payload.authToken)) {
1060
1103
  try {
1061
1104
  ws.send(JSON.stringify({
@@ -1068,15 +1111,27 @@ var CollectorServer = class {
1068
1111
  ws.close(4001, "Authentication failed");
1069
1112
  return;
1070
1113
  }
1071
- this.pendingHandshakes.delete(ws);
1072
1114
  }
1115
+ this.pendingHandshakes.delete(ws);
1073
1116
  const projectName = payload.appName;
1074
- const projectId = payload.projectId ?? (this.projectManager ? getOrCreateProjectId(this.projectManager, projectName) : void 0);
1117
+ const projectId = payload.projectId ?? (this.projectManager ? resolveProjectId(this.projectManager, projectName, this.pmStore) : void 0);
1075
1118
  this.clients.set(ws, {
1076
1119
  sessionId: payload.sessionId,
1077
1120
  projectName,
1078
- projectId
1121
+ projectId,
1122
+ workspaceId: workspaceFromKey?.id
1079
1123
  });
1124
+ if (workspaceFromKey && projectId && this.pmStore?.listProjects && this.pmStore.setProjectWorkspace) {
1125
+ try {
1126
+ const existing = this.pmStore.listProjects().find(
1127
+ (p) => p.runtimeProjectId === projectId
1128
+ );
1129
+ if (existing && !existing.workspaceId) {
1130
+ this.pmStore.setProjectWorkspace(existing.id, workspaceFromKey.id);
1131
+ }
1132
+ } catch {
1133
+ }
1134
+ }
1080
1135
  const sqliteStore = this.ensureSqliteStore(projectName);
1081
1136
  if (sqliteStore) {
1082
1137
  const sessionInfo = {
@@ -1238,12 +1293,13 @@ import { mkdirSync, readFileSync as readFileSync2, writeFileSync, existsSync as
1238
1293
  import { join } from "path";
1239
1294
  import { homedir } from "os";
1240
1295
  var DEFAULT_GLOBAL_CONFIG = {
1241
- defaultPort: 9090,
1296
+ defaultPort: 6767,
1242
1297
  bufferSize: 1e4,
1243
- httpPort: 9091
1298
+ httpPort: 6768
1244
1299
  };
1245
1300
  var ProjectManager = class {
1246
1301
  baseDir;
1302
+ appProjectIndex = /* @__PURE__ */ new Map();
1247
1303
  constructor(baseDir) {
1248
1304
  this.baseDir = baseDir ?? join(homedir(), ".runtimescope");
1249
1305
  }
@@ -1356,6 +1412,60 @@ var ProjectManager = class {
1356
1412
  }
1357
1413
  return null;
1358
1414
  }
1415
+ // --- Reverse index: appName → projectId ---
1416
+ /**
1417
+ * Build reverse index: appName -> projectId.
1418
+ * Scans all project configs, PM projects with runtimeApps + runtimeProjectId,
1419
+ * and project-level .runtimescope/config.json files from PM project paths.
1420
+ */
1421
+ rebuildAppIndex(pmStore) {
1422
+ this.appProjectIndex.clear();
1423
+ for (const name of this.listProjects()) {
1424
+ const config = this.getProjectConfig(name);
1425
+ if (config?.projectId) {
1426
+ this.appProjectIndex.set(name.toLowerCase(), config.projectId);
1427
+ }
1428
+ }
1429
+ if (pmStore) {
1430
+ for (const p of pmStore.listProjects()) {
1431
+ if (p.runtimeProjectId && p.runtimeApps) {
1432
+ for (const app of p.runtimeApps) {
1433
+ this.appProjectIndex.set(app.toLowerCase(), p.runtimeProjectId);
1434
+ }
1435
+ }
1436
+ }
1437
+ }
1438
+ if (pmStore) {
1439
+ for (const p of pmStore.listProjects()) {
1440
+ if (p.path) {
1441
+ try {
1442
+ const configPath = join(p.path, ".runtimescope", "config.json");
1443
+ if (existsSync2(configPath)) {
1444
+ const content = readFileSync2(configPath, "utf-8");
1445
+ const config = JSON.parse(content);
1446
+ if (config.projectId) {
1447
+ if (config.appName) {
1448
+ this.appProjectIndex.set(config.appName.toLowerCase(), config.projectId);
1449
+ }
1450
+ if (Array.isArray(config.sdks)) {
1451
+ for (const sdk of config.sdks) {
1452
+ if (sdk.appName) {
1453
+ this.appProjectIndex.set(sdk.appName.toLowerCase(), config.projectId);
1454
+ }
1455
+ }
1456
+ }
1457
+ }
1458
+ }
1459
+ } catch {
1460
+ }
1461
+ }
1462
+ }
1463
+ }
1464
+ }
1465
+ /** O(1) lookup from the cached index. */
1466
+ resolveAppProjectId(appName) {
1467
+ return this.appProjectIndex.get(appName.toLowerCase()) ?? null;
1468
+ }
1359
1469
  // --- Environment variable resolution ---
1360
1470
  resolveEnvVars(value) {
1361
1471
  return value.replace(/\$\{([^}]+)\}/g, (_match, varName) => {
@@ -1409,6 +1519,129 @@ var ProjectManager = class {
1409
1519
  }
1410
1520
  };
1411
1521
 
1522
+ // src/project-config.ts
1523
+ import { readFileSync as readFileSync3, existsSync as existsSync3, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2 } from "fs";
1524
+ import { join as join2 } from "path";
1525
+ var DEFAULT_CAPTURE = {
1526
+ network: true,
1527
+ console: true,
1528
+ xhr: true,
1529
+ body: false,
1530
+ performance: true,
1531
+ renders: true,
1532
+ navigation: true,
1533
+ clicks: false,
1534
+ http: false,
1535
+ errors: true,
1536
+ stackTraces: false
1537
+ };
1538
+ function readProjectConfig(projectDir) {
1539
+ const configPath = join2(projectDir, ".runtimescope", "config.json");
1540
+ if (!existsSync3(configPath)) return null;
1541
+ try {
1542
+ const content = readFileSync3(configPath, "utf-8");
1543
+ return JSON.parse(content);
1544
+ } catch {
1545
+ return null;
1546
+ }
1547
+ }
1548
+ function writeProjectConfig(projectDir, config) {
1549
+ const dir = join2(projectDir, ".runtimescope");
1550
+ if (!existsSync3(dir)) mkdirSync2(dir, { recursive: true });
1551
+ writeFileSync2(join2(dir, "config.json"), JSON.stringify(config, null, 2) + "\n", "utf-8");
1552
+ }
1553
+ function scaffoldProjectConfig(projectDir, opts) {
1554
+ const existing = readProjectConfig(projectDir);
1555
+ if (existing) {
1556
+ if (opts.sdkType) {
1557
+ const alreadyHas = existing.sdks.some((s) => s.type === opts.sdkType);
1558
+ if (!alreadyHas) {
1559
+ existing.sdks.push({
1560
+ type: opts.sdkType,
1561
+ framework: opts.framework,
1562
+ appName: opts.appName !== existing.appName ? opts.appName : void 0
1563
+ });
1564
+ writeProjectConfig(projectDir, existing);
1565
+ }
1566
+ }
1567
+ return existing;
1568
+ }
1569
+ const projectId = generateProjectId();
1570
+ const httpPort = process.env.RUNTIMESCOPE_HTTP_PORT ?? "6768";
1571
+ const dsn = `runtimescope://${projectId}@localhost:${httpPort}/${opts.appName}`;
1572
+ const config = {
1573
+ projectId,
1574
+ dsn,
1575
+ appName: opts.appName,
1576
+ description: opts.description,
1577
+ sdks: opts.sdkType ? [{ type: opts.sdkType, framework: opts.framework }] : [],
1578
+ capture: { ...DEFAULT_CAPTURE },
1579
+ category: opts.category
1580
+ };
1581
+ writeProjectConfig(projectDir, config);
1582
+ const gitignorePath = join2(projectDir, ".runtimescope", ".gitignore");
1583
+ if (!existsSync3(gitignorePath)) {
1584
+ writeFileSync2(gitignorePath, "# Keep config.json committed, ignore local state\n*.log\n*.db\n.env\n", "utf-8");
1585
+ }
1586
+ return config;
1587
+ }
1588
+ function resolveProjectAppNames(config) {
1589
+ const names = /* @__PURE__ */ new Set([config.appName]);
1590
+ for (const sdk of config.sdks) {
1591
+ if (sdk.appName) names.add(sdk.appName);
1592
+ }
1593
+ return Array.from(names);
1594
+ }
1595
+ function migrateProjectIds(projectManager, pmStore) {
1596
+ const result = { unified: 0, skipped: 0, details: [] };
1597
+ if (!pmStore) return result;
1598
+ for (const project of pmStore.listProjects()) {
1599
+ if (!project.runtimeApps || project.runtimeApps.length < 2) continue;
1600
+ if (!project.path) {
1601
+ result.skipped++;
1602
+ continue;
1603
+ }
1604
+ let canonicalId = null;
1605
+ try {
1606
+ const configPath = join2(project.path, ".runtimescope", "config.json");
1607
+ if (existsSync3(configPath)) {
1608
+ const config = JSON.parse(readFileSync3(configPath, "utf-8"));
1609
+ if (config.projectId) canonicalId = config.projectId;
1610
+ }
1611
+ } catch {
1612
+ }
1613
+ if (!canonicalId && project.runtimeProjectId) {
1614
+ canonicalId = project.runtimeProjectId;
1615
+ }
1616
+ if (!canonicalId) {
1617
+ for (const appName of project.runtimeApps) {
1618
+ const existing = projectManager.getProjectIdForApp(appName);
1619
+ if (existing) {
1620
+ canonicalId = existing;
1621
+ break;
1622
+ }
1623
+ }
1624
+ }
1625
+ if (!canonicalId) {
1626
+ result.skipped++;
1627
+ continue;
1628
+ }
1629
+ for (const appName of project.runtimeApps) {
1630
+ const current = projectManager.getProjectIdForApp(appName);
1631
+ if (current !== canonicalId) {
1632
+ projectManager.setProjectIdForApp(appName, canonicalId);
1633
+ result.details.push(`Unified ${appName}: ${current ?? "none"} \u2192 ${canonicalId}`);
1634
+ result.unified++;
1635
+ }
1636
+ }
1637
+ if (project.runtimeProjectId !== canonicalId) {
1638
+ pmStore.updateProject(project.id, { runtimeProjectId: canonicalId });
1639
+ result.details.push(`PM project ${project.name}: runtimeProjectId \u2192 ${canonicalId}`);
1640
+ }
1641
+ }
1642
+ return result;
1643
+ }
1644
+
1412
1645
  // src/auth.ts
1413
1646
  import { randomBytes as randomBytes2, timingSafeEqual } from "crypto";
1414
1647
  var AuthManager = class {
@@ -1573,7 +1806,7 @@ var Redactor = class {
1573
1806
  // src/platform.ts
1574
1807
  import { execFileSync, execSync } from "child_process";
1575
1808
  import { readlinkSync, readdirSync as readdirSync2 } from "fs";
1576
- import { join as join2 } from "path";
1809
+ import { join as join3 } from "path";
1577
1810
  var IS_WIN = process.platform === "win32";
1578
1811
  var IS_LINUX = process.platform === "linux";
1579
1812
  function runFile(cmd, args, timeoutMs = 5e3) {
@@ -1692,7 +1925,7 @@ function findPidsInDir_linux(dir) {
1692
1925
  const pid = parseInt(entry, 10);
1693
1926
  if (isNaN(pid) || pid <= 1) continue;
1694
1927
  try {
1695
- const cwd = readlinkSync(join2("/proc", entry, "cwd"));
1928
+ const cwd = readlinkSync(join3("/proc", entry, "cwd"));
1696
1929
  if (cwd.startsWith(dir)) pids.push(pid);
1697
1930
  } catch {
1698
1931
  }
@@ -1896,15 +2129,15 @@ var SessionManager = class {
1896
2129
  // src/http-server.ts
1897
2130
  import { createServer } from "http";
1898
2131
  import { createServer as createHttpsServer2 } from "https";
1899
- import { readFileSync as readFileSync3, existsSync as existsSync4 } from "fs";
2132
+ import { readFileSync as readFileSync4, existsSync as existsSync5 } from "fs";
1900
2133
  import { resolve, dirname } from "path";
1901
2134
  import { fileURLToPath } from "url";
1902
2135
  import { WebSocketServer as WebSocketServer2 } from "ws";
1903
2136
 
1904
2137
  // src/pm/pm-routes.ts
1905
2138
  import { readdir, readFile, writeFile, unlink, mkdir } from "fs/promises";
1906
- import { existsSync as existsSync3 } from "fs";
1907
- import { join as join3 } from "path";
2139
+ import { existsSync as existsSync4 } from "fs";
2140
+ import { join as join4 } from "path";
1908
2141
  import { homedir as homedir2 } from "os";
1909
2142
  import { spawn, execFileSync as execFileSync2 } from "child_process";
1910
2143
  var LOG_RING_SIZE = 500;
@@ -1943,7 +2176,11 @@ function createPmRouter(pmStore, discovery, helpers, broadcastDevServer) {
1943
2176
  helpers.json(res, { ...project, stats });
1944
2177
  return;
1945
2178
  }
1946
- const projects = pmStore.listProjects();
2179
+ let projects = pmStore.listProjects();
2180
+ const workspaceId = params.get("workspace_id");
2181
+ if (workspaceId) {
2182
+ projects = projects.filter((p) => p.workspaceId === workspaceId);
2183
+ }
1947
2184
  helpers.json(res, { data: projects, count: projects.length });
1948
2185
  });
1949
2186
  route("GET", "/api/pm/projects/export-csv", (_req, res, params) => {
@@ -2041,6 +2278,152 @@ function createPmRouter(pmStore, discovery, helpers, broadcastDevServer) {
2041
2278
  helpers.json(res, { error: err.message }, 400);
2042
2279
  }
2043
2280
  });
2281
+ route("DELETE", "/api/pm/projects/:id", (_req, res, params) => {
2282
+ const id = params.get("id");
2283
+ const project = pmStore.getProject(id);
2284
+ if (!project) {
2285
+ helpers.json(res, { error: "Project not found" }, 404);
2286
+ return;
2287
+ }
2288
+ pmStore.deleteProject(id);
2289
+ helpers.json(res, { ok: true, deleted: project.name });
2290
+ });
2291
+ route("PUT", "/api/pm/projects/:id/workspace", async (req, res, params) => {
2292
+ const id = params.get("id");
2293
+ const body = await helpers.readBody(req, 4096);
2294
+ if (!body) {
2295
+ helpers.json(res, { error: "Missing body" }, 400);
2296
+ return;
2297
+ }
2298
+ let parsed;
2299
+ try {
2300
+ parsed = JSON.parse(body);
2301
+ } catch {
2302
+ helpers.json(res, { error: "Invalid JSON" }, 400);
2303
+ return;
2304
+ }
2305
+ if (!parsed.workspace_id) {
2306
+ helpers.json(res, { error: "Missing workspace_id" }, 400);
2307
+ return;
2308
+ }
2309
+ try {
2310
+ pmStore.setProjectWorkspace(id, parsed.workspace_id);
2311
+ helpers.json(res, { ok: true });
2312
+ } catch (err) {
2313
+ helpers.json(res, { error: err.message }, 400);
2314
+ }
2315
+ });
2316
+ route("GET", "/api/pm/workspaces", (_req, res) => {
2317
+ helpers.json(res, { data: pmStore.listWorkspaces() });
2318
+ });
2319
+ route("POST", "/api/pm/workspaces", async (req, res) => {
2320
+ const body = await helpers.readBody(req, 4096);
2321
+ if (!body) {
2322
+ helpers.json(res, { error: "Missing body" }, 400);
2323
+ return;
2324
+ }
2325
+ let parsed;
2326
+ try {
2327
+ parsed = JSON.parse(body);
2328
+ } catch {
2329
+ helpers.json(res, { error: "Invalid JSON" }, 400);
2330
+ return;
2331
+ }
2332
+ if (!parsed.name || typeof parsed.name !== "string") {
2333
+ helpers.json(res, { error: "Missing name" }, 400);
2334
+ return;
2335
+ }
2336
+ try {
2337
+ const ws = pmStore.createWorkspace({ name: parsed.name, slug: parsed.slug, description: parsed.description });
2338
+ helpers.json(res, ws, 201);
2339
+ } catch (err) {
2340
+ helpers.json(res, { error: err.message }, 400);
2341
+ }
2342
+ });
2343
+ route("GET", "/api/pm/workspaces/:id", (_req, res, params) => {
2344
+ const id = params.get("id");
2345
+ const ws = pmStore.getWorkspace(id);
2346
+ if (!ws) {
2347
+ helpers.json(res, { error: "Workspace not found" }, 404);
2348
+ return;
2349
+ }
2350
+ helpers.json(res, ws);
2351
+ });
2352
+ route("PUT", "/api/pm/workspaces/:id", async (req, res, params) => {
2353
+ const id = params.get("id");
2354
+ if (!pmStore.getWorkspace(id)) {
2355
+ helpers.json(res, { error: "Workspace not found" }, 404);
2356
+ return;
2357
+ }
2358
+ const body = await helpers.readBody(req, 4096);
2359
+ if (!body) {
2360
+ helpers.json(res, { error: "Missing body" }, 400);
2361
+ return;
2362
+ }
2363
+ let parsed;
2364
+ try {
2365
+ parsed = JSON.parse(body);
2366
+ } catch {
2367
+ helpers.json(res, { error: "Invalid JSON" }, 400);
2368
+ return;
2369
+ }
2370
+ try {
2371
+ pmStore.updateWorkspace(id, parsed);
2372
+ helpers.json(res, pmStore.getWorkspace(id));
2373
+ } catch (err) {
2374
+ helpers.json(res, { error: err.message }, 400);
2375
+ }
2376
+ });
2377
+ route("DELETE", "/api/pm/workspaces/:id", (_req, res, params) => {
2378
+ const id = params.get("id");
2379
+ try {
2380
+ pmStore.deleteWorkspace(id);
2381
+ helpers.json(res, { ok: true });
2382
+ } catch (err) {
2383
+ helpers.json(res, { error: err.message }, 400);
2384
+ }
2385
+ });
2386
+ route("GET", "/api/pm/workspaces/:id/api-keys", (_req, res, params) => {
2387
+ const id = params.get("id");
2388
+ if (!pmStore.getWorkspace(id)) {
2389
+ helpers.json(res, { error: "Workspace not found" }, 404);
2390
+ return;
2391
+ }
2392
+ helpers.json(res, { data: pmStore.listApiKeys(id) });
2393
+ });
2394
+ route("POST", "/api/pm/workspaces/:id/api-keys", async (req, res, params) => {
2395
+ const id = params.get("id");
2396
+ if (!pmStore.getWorkspace(id)) {
2397
+ helpers.json(res, { error: "Workspace not found" }, 404);
2398
+ return;
2399
+ }
2400
+ const body = await helpers.readBody(req, 4096);
2401
+ if (!body) {
2402
+ helpers.json(res, { error: "Missing body" }, 400);
2403
+ return;
2404
+ }
2405
+ let parsed;
2406
+ try {
2407
+ parsed = JSON.parse(body);
2408
+ } catch {
2409
+ helpers.json(res, { error: "Invalid JSON" }, 400);
2410
+ return;
2411
+ }
2412
+ if (!parsed.label) {
2413
+ helpers.json(res, { error: "Missing label" }, 400);
2414
+ return;
2415
+ }
2416
+ try {
2417
+ const key = pmStore.createApiKey(id, parsed.label, parsed.expires_at);
2418
+ helpers.json(res, key, 201);
2419
+ } catch (err) {
2420
+ helpers.json(res, { error: err.message }, 400);
2421
+ }
2422
+ });
2423
+ route("DELETE", "/api/pm/api-keys/:key", (_req, res, params) => {
2424
+ pmStore.revokeApiKey(params.get("key"));
2425
+ helpers.json(res, { ok: true });
2426
+ });
2044
2427
  route("GET", "/api/pm/tasks", (_req, res, params) => {
2045
2428
  const projectId = params.get("project_id") ?? void 0;
2046
2429
  const status = params.get("status") ?? void 0;
@@ -2216,13 +2599,13 @@ function createPmRouter(pmStore, discovery, helpers, broadcastDevServer) {
2216
2599
  helpers.json(res, { data: [], count: 0 });
2217
2600
  return;
2218
2601
  }
2219
- const memoryDir = join3(homedir2(), ".claude", "projects", project.claudeProjectKey, "memory");
2602
+ const memoryDir = join4(homedir2(), ".claude", "projects", project.claudeProjectKey, "memory");
2220
2603
  try {
2221
2604
  const files = await readdir(memoryDir);
2222
2605
  const mdFiles = files.filter((f) => f.endsWith(".md"));
2223
2606
  const result = await Promise.all(
2224
2607
  mdFiles.map(async (filename) => {
2225
- const content = await readFile(join3(memoryDir, filename), "utf-8");
2608
+ const content = await readFile(join4(memoryDir, filename), "utf-8");
2226
2609
  return { filename, content, sizeBytes: Buffer.byteLength(content) };
2227
2610
  })
2228
2611
  );
@@ -2239,7 +2622,7 @@ function createPmRouter(pmStore, discovery, helpers, broadcastDevServer) {
2239
2622
  helpers.json(res, { error: "Project not found" }, 404);
2240
2623
  return;
2241
2624
  }
2242
- const filePath = join3(homedir2(), ".claude", "projects", project.claudeProjectKey, "memory", filename);
2625
+ const filePath = join4(homedir2(), ".claude", "projects", project.claudeProjectKey, "memory", filename);
2243
2626
  try {
2244
2627
  const content = await readFile(filePath, "utf-8");
2245
2628
  helpers.json(res, { filename, content, sizeBytes: Buffer.byteLength(content) });
@@ -2262,9 +2645,9 @@ function createPmRouter(pmStore, discovery, helpers, broadcastDevServer) {
2262
2645
  }
2263
2646
  try {
2264
2647
  const { content } = JSON.parse(body);
2265
- const memoryDir = join3(homedir2(), ".claude", "projects", project.claudeProjectKey, "memory");
2648
+ const memoryDir = join4(homedir2(), ".claude", "projects", project.claudeProjectKey, "memory");
2266
2649
  await mkdir(memoryDir, { recursive: true });
2267
- await writeFile(join3(memoryDir, filename), content, "utf-8");
2650
+ await writeFile(join4(memoryDir, filename), content, "utf-8");
2268
2651
  helpers.json(res, { ok: true });
2269
2652
  } catch (err) {
2270
2653
  helpers.json(res, { error: err.message }, 500);
@@ -2278,7 +2661,7 @@ function createPmRouter(pmStore, discovery, helpers, broadcastDevServer) {
2278
2661
  helpers.json(res, { error: "Project not found" }, 404);
2279
2662
  return;
2280
2663
  }
2281
- const filePath = join3(homedir2(), ".claude", "projects", project.claudeProjectKey, "memory", filename);
2664
+ const filePath = join4(homedir2(), ".claude", "projects", project.claudeProjectKey, "memory", filename);
2282
2665
  try {
2283
2666
  await unlink(filePath);
2284
2667
  helpers.json(res, { ok: true });
@@ -2338,7 +2721,7 @@ function createPmRouter(pmStore, discovery, helpers, broadcastDevServer) {
2338
2721
  const { content } = JSON.parse(body);
2339
2722
  const paths = getRulesPaths(project.claudeProjectKey, project.path);
2340
2723
  const filePath = paths[scope];
2341
- const dir = join3(filePath, "..");
2724
+ const dir = join4(filePath, "..");
2342
2725
  await mkdir(dir, { recursive: true });
2343
2726
  await writeFile(filePath, content, "utf-8");
2344
2727
  helpers.json(res, { ok: true });
@@ -2358,7 +2741,7 @@ function createPmRouter(pmStore, discovery, helpers, broadcastDevServer) {
2358
2741
  return;
2359
2742
  }
2360
2743
  try {
2361
- const pkgPath = join3(project.path, "package.json");
2744
+ const pkgPath = join4(project.path, "package.json");
2362
2745
  const pkg = JSON.parse(await readFile(pkgPath, "utf-8"));
2363
2746
  const scripts = pkg.scripts ?? {};
2364
2747
  const recommended = ["dev", "start", "serve"].find((s) => s in scripts) ?? null;
@@ -2774,6 +3157,71 @@ function createPmRouter(pmStore, discovery, helpers, broadcastDevServer) {
2774
3157
  res.end(csv);
2775
3158
  }
2776
3159
  });
3160
+ route("GET", "/api/pm/capex-all", (_req, res, params) => {
3161
+ const category = params.get("category") ?? void 0;
3162
+ const projects = pmStore.listProjects();
3163
+ const filteredProjects = category ? projects.filter((p) => p.category === category) : projects;
3164
+ const allEntries = [];
3165
+ let totalCost = 0;
3166
+ let totalCapitalizable = 0;
3167
+ let totalExpensed = 0;
3168
+ let totalMinutes = 0;
3169
+ let totalConfirmed = 0;
3170
+ let totalUnconfirmed = 0;
3171
+ const byProject = [];
3172
+ for (const project of filteredProjects) {
3173
+ const entries = pmStore.listCapexEntries(project.id);
3174
+ let projCost = 0;
3175
+ let projCap = 0;
3176
+ let projExp = 0;
3177
+ let projMins = 0;
3178
+ let projConfirmed = 0;
3179
+ for (const e of entries) {
3180
+ allEntries.push({ ...e, projectName: project.name });
3181
+ projCost += e.adjustedCostMicrodollars;
3182
+ projMins += e.activeMinutes;
3183
+ if (e.classification === "capitalizable") projCap += e.adjustedCostMicrodollars;
3184
+ else projExp += e.adjustedCostMicrodollars;
3185
+ if (e.confirmed) projConfirmed++;
3186
+ }
3187
+ if (entries.length > 0) {
3188
+ byProject.push({
3189
+ projectId: project.id,
3190
+ projectName: project.name,
3191
+ category: project.category,
3192
+ totalCost: projCost,
3193
+ capitalizable: projCap,
3194
+ expensed: projExp,
3195
+ activeMinutes: projMins,
3196
+ activeHours: +(projMins / 60).toFixed(2),
3197
+ confirmed: projConfirmed,
3198
+ total: entries.length
3199
+ });
3200
+ }
3201
+ totalCost += projCost;
3202
+ totalCapitalizable += projCap;
3203
+ totalExpensed += projExp;
3204
+ totalMinutes += projMins;
3205
+ totalConfirmed += projConfirmed;
3206
+ totalUnconfirmed += entries.length - projConfirmed;
3207
+ }
3208
+ helpers.json(res, {
3209
+ data: {
3210
+ summary: {
3211
+ totalCost,
3212
+ capitalizable: totalCapitalizable,
3213
+ expensed: totalExpensed,
3214
+ activeMinutes: totalMinutes,
3215
+ activeHours: +(totalMinutes / 60).toFixed(2),
3216
+ confirmed: totalConfirmed,
3217
+ unconfirmed: totalUnconfirmed,
3218
+ projectCount: byProject.length
3219
+ },
3220
+ byProject,
3221
+ entries: allEntries.sort((a, b) => b.createdAt - a.createdAt)
3222
+ }
3223
+ });
3224
+ });
2777
3225
  route("GET", "/api/pm/capex-report-all", async (_req, res, params) => {
2778
3226
  const startDate = params.get("start_date") ?? void 0;
2779
3227
  const endDate = params.get("end_date") ?? void 0;
@@ -2819,9 +3267,9 @@ function sanitizeFilename(name) {
2819
3267
  function getRulesPaths(claudeProjectKey, projectPath) {
2820
3268
  const home = homedir2();
2821
3269
  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")
3270
+ global: join4(home, ".claude", "CLAUDE.md"),
3271
+ project: claudeProjectKey ? join4(home, ".claude", "projects", claudeProjectKey, "CLAUDE.md") : join4(projectPath ?? "", ".claude", "CLAUDE.md"),
3272
+ local: projectPath ? join4(projectPath, "CLAUDE.md") : join4(home, "CLAUDE.md")
2825
3273
  };
2826
3274
  }
2827
3275
  function execGit(args, cwd) {
@@ -2866,7 +3314,7 @@ function parseGitStatus(porcelain) {
2866
3314
  }
2867
3315
  async function readRuleFile(filePath) {
2868
3316
  try {
2869
- if (existsSync3(filePath)) {
3317
+ if (existsSync4(filePath)) {
2870
3318
  const content = await readFile(filePath, "utf-8");
2871
3319
  return { path: filePath, content, exists: true };
2872
3320
  }
@@ -2876,6 +3324,16 @@ async function readRuleFile(filePath) {
2876
3324
  }
2877
3325
 
2878
3326
  // src/http-server.ts
3327
+ var COLLECTOR_VERSION = (() => {
3328
+ try {
3329
+ const here = dirname(fileURLToPath(import.meta.url));
3330
+ const pkgJson = readFileSync4(resolve(here, "..", "package.json"), "utf-8");
3331
+ const pkg = JSON.parse(pkgJson);
3332
+ return pkg.version ?? "unknown";
3333
+ } catch {
3334
+ return "unknown";
3335
+ }
3336
+ })();
2879
3337
  var HttpServer = class {
2880
3338
  server = null;
2881
3339
  wss = null;
@@ -2889,7 +3347,7 @@ var HttpServer = class {
2889
3347
  routes = /* @__PURE__ */ new Map();
2890
3348
  pmRouter = null;
2891
3349
  sdkBundlePath = null;
2892
- activePort = 9091;
3350
+ activePort = 6768;
2893
3351
  startedAt = Date.now();
2894
3352
  connectedSessionsGetter = null;
2895
3353
  pmStore = null;
@@ -2915,6 +3373,7 @@ var HttpServer = class {
2915
3373
  this.routes.set("GET /api/health", (_req, res) => {
2916
3374
  this.json(res, {
2917
3375
  status: "ok",
3376
+ version: COLLECTOR_VERSION,
2918
3377
  timestamp: Date.now(),
2919
3378
  uptime: Math.floor((Date.now() - this.startedAt) / 1e3),
2920
3379
  sessions: this.store.getSessionInfo().filter((s) => s.isConnected).length,
@@ -3111,7 +3570,7 @@ var HttpServer = class {
3111
3570
  return;
3112
3571
  }
3113
3572
  const payload = parsed;
3114
- const projectId = typeof payload.projectId === "string" ? payload.projectId : payload.appName && this.projectManager ? getOrCreateProjectId(this.projectManager, payload.appName) : void 0;
3573
+ const projectId = typeof payload.projectId === "string" ? payload.projectId : payload.appName && this.projectManager ? resolveProjectId(this.projectManager, payload.appName, this.pmStore) : void 0;
3115
3574
  if (!payload.sessionId || !Array.isArray(payload.events) || payload.events.length === 0) {
3116
3575
  this.json(res, {
3117
3576
  error: "Required: sessionId (string), events (non-empty array)",
@@ -3138,6 +3597,21 @@ var HttpServer = class {
3138
3597
  } catch {
3139
3598
  }
3140
3599
  }
3600
+ if (this.pmStore) {
3601
+ try {
3602
+ const token = AuthManager.extractBearer(req.headers.authorization);
3603
+ if (token) {
3604
+ const ws = this.pmStore.getWorkspaceByApiKey(token);
3605
+ if (ws && projectId) {
3606
+ const existing = this.pmStore.listProjects().find((p) => p.runtimeProjectId === projectId);
3607
+ if (existing && !existing.workspaceId) {
3608
+ this.pmStore.setProjectWorkspace(existing.id, ws.id);
3609
+ }
3610
+ }
3611
+ }
3612
+ } catch {
3613
+ }
3614
+ }
3141
3615
  }
3142
3616
  const VALID_EVENT_TYPES = /* @__PURE__ */ new Set([
3143
3617
  "network",
@@ -3196,7 +3670,7 @@ var HttpServer = class {
3196
3670
  // npm installed
3197
3671
  ];
3198
3672
  for (const p of candidates) {
3199
- if (existsSync4(p)) {
3673
+ if (existsSync5(p)) {
3200
3674
  this.sdkBundlePath = p;
3201
3675
  return p;
3202
3676
  }
@@ -3204,7 +3678,7 @@ var HttpServer = class {
3204
3678
  return null;
3205
3679
  }
3206
3680
  async start(options = {}) {
3207
- const basePort = options.port ?? parseInt(process.env.RUNTIMESCOPE_HTTP_PORT ?? "9091", 10);
3681
+ const basePort = options.port ?? parseInt(process.env.RUNTIMESCOPE_HTTP_PORT ?? "6768", 10);
3208
3682
  const host = options.host ?? "127.0.0.1";
3209
3683
  const tls = options.tls;
3210
3684
  const maxRetries = 5;
@@ -3336,7 +3810,9 @@ var HttpServer = class {
3336
3810
  const isPublic = url.pathname === "/api/health" || url.pathname === "/runtimescope.js" || url.pathname === "/snippet";
3337
3811
  if (!isPublic && this.authManager?.isEnabled()) {
3338
3812
  const token = AuthManager.extractBearer(req.headers.authorization);
3339
- if (!this.authManager.isAuthorized(token)) {
3813
+ const isGlobal = this.authManager.isAuthorized(token);
3814
+ const isWorkspaceToken = !!(token && this.pmStore?.getWorkspaceByApiKey(token));
3815
+ if (!isGlobal && !isWorkspaceToken) {
3340
3816
  this.json(res, { error: "Unauthorized", code: "AUTH_FAILED" }, 401);
3341
3817
  return;
3342
3818
  }
@@ -3344,7 +3820,7 @@ var HttpServer = class {
3344
3820
  if (req.method === "GET" && url.pathname === "/runtimescope.js") {
3345
3821
  const sdkPath = this.resolveSdkPath();
3346
3822
  if (sdkPath) {
3347
- const bundle = readFileSync3(sdkPath, "utf-8");
3823
+ const bundle = readFileSync4(sdkPath, "utf-8");
3348
3824
  res.writeHead(200, {
3349
3825
  "Content-Type": "application/javascript",
3350
3826
  "Cache-Control": "no-cache"
@@ -3358,14 +3834,12 @@ var HttpServer = class {
3358
3834
  }
3359
3835
  if (req.method === "GET" && url.pathname === "/snippet") {
3360
3836
  const appName = (url.searchParams.get("app") || "my-app").replace(/[^a-zA-Z0-9_-]/g, "");
3361
- const wsPort = process.env.RUNTIMESCOPE_PORT ?? "9090";
3837
+ const projectId = url.searchParams.get("project_id") || "proj_xxx";
3838
+ const dsn = `runtimescope://${projectId}@localhost:${this.activePort}/${appName}`;
3362
3839
  const snippet = `<!-- RuntimeScope SDK \u2014 paste before </body> -->
3363
3840
  <script src="http://localhost:${this.activePort}/runtimescope.js"></script>
3364
3841
  <script>
3365
- RuntimeScope.init({
3366
- appName: '${appName}',
3367
- endpoint: 'ws://localhost:${wsPort}',
3368
- });
3842
+ RuntimeScope.init({ dsn: '${dsn}' });
3369
3843
  </script>`;
3370
3844
  res.writeHead(200, {
3371
3845
  "Content-Type": "text/plain"
@@ -3470,6 +3944,7 @@ function numParam(params, key) {
3470
3944
 
3471
3945
  // src/pm/pm-store.ts
3472
3946
  import Database from "better-sqlite3";
3947
+ import { randomBytes as randomBytes3 } from "crypto";
3473
3948
  var PmStore = class {
3474
3949
  db;
3475
3950
  constructor(options) {
@@ -3595,6 +4070,37 @@ var PmStore = class {
3595
4070
  CREATE INDEX IF NOT EXISTS idx_pm_capex_project ON pm_capex_entries(project_id);
3596
4071
  CREATE INDEX IF NOT EXISTS idx_pm_capex_period ON pm_capex_entries(period);
3597
4072
  CREATE INDEX IF NOT EXISTS idx_pm_capex_confirmed ON pm_capex_entries(confirmed);
4073
+
4074
+ CREATE TABLE IF NOT EXISTS pm_deleted_projects (
4075
+ path TEXT PRIMARY KEY,
4076
+ name TEXT,
4077
+ deleted_at INTEGER NOT NULL
4078
+ );
4079
+ CREATE INDEX IF NOT EXISTS idx_deleted_path ON pm_deleted_projects(path);
4080
+
4081
+ -- Multi-tenant workspaces (Phase 1) --
4082
+ CREATE TABLE IF NOT EXISTS pm_workspaces (
4083
+ id TEXT PRIMARY KEY,
4084
+ name TEXT NOT NULL,
4085
+ slug TEXT UNIQUE NOT NULL,
4086
+ description TEXT,
4087
+ is_default INTEGER NOT NULL DEFAULT 0,
4088
+ created_at INTEGER NOT NULL,
4089
+ updated_at INTEGER NOT NULL
4090
+ );
4091
+ CREATE INDEX IF NOT EXISTS idx_workspaces_slug ON pm_workspaces(slug);
4092
+
4093
+ CREATE TABLE IF NOT EXISTS pm_api_keys (
4094
+ key TEXT PRIMARY KEY,
4095
+ workspace_id TEXT NOT NULL,
4096
+ label TEXT NOT NULL,
4097
+ created_at INTEGER NOT NULL,
4098
+ last_used_at INTEGER,
4099
+ expires_at INTEGER,
4100
+ revoked_at INTEGER,
4101
+ FOREIGN KEY (workspace_id) REFERENCES pm_workspaces(id) ON DELETE CASCADE
4102
+ );
4103
+ CREATE INDEX IF NOT EXISTS idx_api_keys_workspace ON pm_api_keys(workspace_id);
3598
4104
  `);
3599
4105
  }
3600
4106
  runMigrations() {
@@ -3614,18 +4120,45 @@ var PmStore = class {
3614
4120
  this.db.exec("ALTER TABLE pm_projects ADD COLUMN runtime_project_id TEXT DEFAULT NULL");
3615
4121
  } catch {
3616
4122
  }
4123
+ try {
4124
+ this.db.exec("ALTER TABLE pm_projects ADD COLUMN workspace_id TEXT DEFAULT NULL");
4125
+ } catch {
4126
+ }
4127
+ this.ensureDefaultWorkspace();
4128
+ }
4129
+ /**
4130
+ * Ensure a default "personal" workspace exists and every project has a
4131
+ * workspace_id. Runs on every startup — idempotent.
4132
+ */
4133
+ ensureDefaultWorkspace() {
4134
+ const existing = this.db.prepare("SELECT id FROM pm_workspaces WHERE is_default = 1").get();
4135
+ let defaultId;
4136
+ if (existing) {
4137
+ defaultId = existing.id;
4138
+ } else {
4139
+ defaultId = generateWorkspaceId();
4140
+ const now = Date.now();
4141
+ this.db.prepare(
4142
+ `INSERT INTO pm_workspaces (id, name, slug, description, is_default, created_at, updated_at)
4143
+ VALUES (?, ?, ?, ?, 1, ?, ?)`
4144
+ ).run(defaultId, "Personal", "personal", "Your personal workspace", now, now);
4145
+ }
4146
+ this.db.prepare("UPDATE pm_projects SET workspace_id = ? WHERE workspace_id IS NULL").run(defaultId);
3617
4147
  }
3618
4148
  // ============================================================
3619
4149
  // Projects
3620
4150
  // ============================================================
3621
4151
  upsertProject(project) {
4152
+ const workspaceId = project.workspaceId ?? this.db.prepare("SELECT id FROM pm_workspaces WHERE is_default = 1 LIMIT 1").get();
4153
+ const resolvedWorkspaceId = typeof workspaceId === "string" ? workspaceId : workspaceId?.id ?? null;
3622
4154
  this.db.prepare(`
3623
- INSERT INTO pm_projects (id, name, path, claude_project_key, runtimescope_project,
4155
+ INSERT INTO pm_projects (id, workspace_id, name, path, claude_project_key, runtimescope_project,
3624
4156
  phase, management_authorized, probable_to_complete, project_status,
3625
4157
  category, sdk_installed, runtime_apps,
3626
4158
  created_at, updated_at, metadata)
3627
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
4159
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
3628
4160
  ON CONFLICT(id) DO UPDATE SET
4161
+ workspace_id = COALESCE(pm_projects.workspace_id, excluded.workspace_id),
3629
4162
  name = excluded.name,
3630
4163
  path = COALESCE(excluded.path, pm_projects.path),
3631
4164
  claude_project_key = COALESCE(excluded.claude_project_key, pm_projects.claude_project_key),
@@ -3636,6 +4169,7 @@ var PmStore = class {
3636
4169
  metadata = COALESCE(excluded.metadata, pm_projects.metadata)
3637
4170
  `).run(
3638
4171
  project.id,
4172
+ resolvedWorkspaceId,
3639
4173
  project.name,
3640
4174
  project.path ?? null,
3641
4175
  project.claudeProjectKey ?? null,
@@ -3762,13 +4296,132 @@ var PmStore = class {
3762
4296
  this.updateProject(match.id, updates);
3763
4297
  return match.id;
3764
4298
  }
4299
+ /** Find a project's runtimeProjectId by checking if appName appears in any project's runtimeApps. */
4300
+ findProjectIdByApp(appName) {
4301
+ 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}"%`);
4302
+ return row?.runtime_project_id ?? null;
4303
+ }
3765
4304
  listCategories() {
3766
4305
  const rows = this.db.prepare("SELECT DISTINCT category FROM pm_projects WHERE category IS NOT NULL ORDER BY category ASC").all();
3767
4306
  return rows.map((r) => r.category);
3768
4307
  }
4308
+ // ============================================================
4309
+ // Workspaces (multi-tenant)
4310
+ // ============================================================
4311
+ listWorkspaces() {
4312
+ const rows = this.db.prepare("SELECT * FROM pm_workspaces ORDER BY is_default DESC, name ASC").all();
4313
+ return rows.map(mapWorkspaceRow);
4314
+ }
4315
+ getWorkspace(id) {
4316
+ const row = this.db.prepare("SELECT * FROM pm_workspaces WHERE id = ?").get(id);
4317
+ return row ? mapWorkspaceRow(row) : null;
4318
+ }
4319
+ getWorkspaceBySlug(slug) {
4320
+ const row = this.db.prepare("SELECT * FROM pm_workspaces WHERE slug = ?").get(slug);
4321
+ return row ? mapWorkspaceRow(row) : null;
4322
+ }
4323
+ getDefaultWorkspace() {
4324
+ const row = this.db.prepare("SELECT * FROM pm_workspaces WHERE is_default = 1 LIMIT 1").get();
4325
+ if (!row) {
4326
+ throw new Error("Default workspace missing \u2014 ensureDefaultWorkspace() must run first");
4327
+ }
4328
+ return mapWorkspaceRow(row);
4329
+ }
4330
+ createWorkspace(input) {
4331
+ const id = generateWorkspaceId();
4332
+ const slug = (input.slug ?? input.name).toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
4333
+ if (!slug) {
4334
+ throw new Error("Workspace slug cannot be empty");
4335
+ }
4336
+ if (this.getWorkspaceBySlug(slug)) {
4337
+ throw new Error(`Workspace with slug "${slug}" already exists`);
4338
+ }
4339
+ const now = Date.now();
4340
+ this.db.prepare(
4341
+ `INSERT INTO pm_workspaces (id, name, slug, description, is_default, created_at, updated_at)
4342
+ VALUES (?, ?, ?, ?, 0, ?, ?)`
4343
+ ).run(id, input.name, slug, input.description ?? null, now, now);
4344
+ return { id, name: input.name, slug, description: input.description, createdAt: now, updatedAt: now, isDefault: false };
4345
+ }
4346
+ updateWorkspace(id, updates) {
4347
+ const sets = [];
4348
+ const params = [];
4349
+ if (updates.name !== void 0) {
4350
+ sets.push("name = ?");
4351
+ params.push(updates.name);
4352
+ }
4353
+ if (updates.slug !== void 0) {
4354
+ sets.push("slug = ?");
4355
+ params.push(updates.slug);
4356
+ }
4357
+ if (updates.description !== void 0) {
4358
+ sets.push("description = ?");
4359
+ params.push(updates.description);
4360
+ }
4361
+ if (!sets.length) return;
4362
+ sets.push("updated_at = ?");
4363
+ params.push(Date.now());
4364
+ params.push(id);
4365
+ this.db.prepare(`UPDATE pm_workspaces SET ${sets.join(", ")} WHERE id = ?`).run(...params);
4366
+ }
4367
+ deleteWorkspace(id) {
4368
+ const ws = this.getWorkspace(id);
4369
+ if (!ws) return;
4370
+ if (ws.isDefault) {
4371
+ throw new Error("Cannot delete the default workspace");
4372
+ }
4373
+ const def = this.getDefaultWorkspace();
4374
+ this.db.prepare("UPDATE pm_projects SET workspace_id = ? WHERE workspace_id = ?").run(def.id, id);
4375
+ this.db.prepare("DELETE FROM pm_api_keys WHERE workspace_id = ?").run(id);
4376
+ this.db.prepare("DELETE FROM pm_workspaces WHERE id = ?").run(id);
4377
+ }
4378
+ /** Move a project between workspaces. */
4379
+ setProjectWorkspace(projectId, workspaceId) {
4380
+ const ws = this.getWorkspace(workspaceId);
4381
+ if (!ws) throw new Error(`Workspace ${workspaceId} does not exist`);
4382
+ this.db.prepare("UPDATE pm_projects SET workspace_id = ?, updated_at = ? WHERE id = ?").run(workspaceId, Date.now(), projectId);
4383
+ }
4384
+ // ============================================================
4385
+ // API Keys (workspace-scoped)
4386
+ // ============================================================
4387
+ createApiKey(workspaceId, label, expiresAt) {
4388
+ const ws = this.getWorkspace(workspaceId);
4389
+ if (!ws) throw new Error(`Workspace ${workspaceId} does not exist`);
4390
+ const key = `tk_${randomBytes3(24).toString("hex")}`;
4391
+ const now = Date.now();
4392
+ this.db.prepare(
4393
+ `INSERT INTO pm_api_keys (key, workspace_id, label, created_at, expires_at)
4394
+ VALUES (?, ?, ?, ?, ?)`
4395
+ ).run(key, workspaceId, label, now, expiresAt ?? null);
4396
+ return { key, workspaceId, label, createdAt: now, expiresAt };
4397
+ }
4398
+ listApiKeys(workspaceId) {
4399
+ const rows = this.db.prepare(
4400
+ `SELECT * FROM pm_api_keys WHERE workspace_id = ? AND revoked_at IS NULL ORDER BY created_at DESC`
4401
+ ).all(workspaceId);
4402
+ return rows.map(mapApiKeyRow);
4403
+ }
4404
+ revokeApiKey(key) {
4405
+ this.db.prepare("UPDATE pm_api_keys SET revoked_at = ? WHERE key = ?").run(Date.now(), key);
4406
+ }
4407
+ getWorkspaceByApiKey(key) {
4408
+ const row = this.db.prepare(
4409
+ `SELECT w.* FROM pm_api_keys k
4410
+ JOIN pm_workspaces w ON w.id = k.workspace_id
4411
+ WHERE k.key = ? AND k.revoked_at IS NULL
4412
+ AND (k.expires_at IS NULL OR k.expires_at > ?)`
4413
+ ).get(key, Date.now());
4414
+ if (!row) return null;
4415
+ try {
4416
+ this.db.prepare("UPDATE pm_api_keys SET last_used_at = ? WHERE key = ?").run(Date.now(), key);
4417
+ } catch {
4418
+ }
4419
+ return mapWorkspaceRow(row);
4420
+ }
3769
4421
  mapProjectRow(row) {
3770
4422
  return {
3771
4423
  id: row.id,
4424
+ workspaceId: row.workspace_id ?? void 0,
3772
4425
  name: row.name,
3773
4426
  path: row.path ?? void 0,
3774
4427
  claudeProjectKey: row.claude_project_key ?? void 0,
@@ -4521,12 +5174,69 @@ var PmStore = class {
4521
5174
  };
4522
5175
  }
4523
5176
  // ============================================================
5177
+ // Deleted Projects Blocklist
5178
+ // ============================================================
5179
+ deleteProject(id) {
5180
+ const project = this.getProject(id);
5181
+ if (!project) return;
5182
+ if (project.path) {
5183
+ this.db.prepare(
5184
+ "INSERT OR REPLACE INTO pm_deleted_projects (path, name, deleted_at) VALUES (?, ?, ?)"
5185
+ ).run(project.path, project.name, Date.now());
5186
+ }
5187
+ if (project.claudeProjectKey) {
5188
+ this.db.prepare(
5189
+ "INSERT OR REPLACE INTO pm_deleted_projects (path, name, deleted_at) VALUES (?, ?, ?)"
5190
+ ).run(project.claudeProjectKey, project.name, Date.now());
5191
+ }
5192
+ this.db.prepare("DELETE FROM pm_capex_entries WHERE project_id = ?").run(id);
5193
+ this.db.prepare("DELETE FROM pm_notes WHERE project_id = ?").run(id);
5194
+ this.db.prepare("DELETE FROM pm_tasks WHERE project_id = ?").run(id);
5195
+ this.db.prepare("DELETE FROM pm_sessions WHERE project_id = ?").run(id);
5196
+ this.db.prepare("DELETE FROM pm_projects WHERE id = ?").run(id);
5197
+ }
5198
+ isDeletedPath(path) {
5199
+ const row = this.db.prepare("SELECT 1 FROM pm_deleted_projects WHERE path = ?").get(path);
5200
+ return !!row;
5201
+ }
5202
+ recoverProject(path) {
5203
+ this.db.prepare("DELETE FROM pm_deleted_projects WHERE path = ?").run(path);
5204
+ }
5205
+ listDeletedProjects() {
5206
+ return this.db.prepare("SELECT path, name, deleted_at as deletedAt FROM pm_deleted_projects ORDER BY deleted_at DESC").all();
5207
+ }
5208
+ // ============================================================
4524
5209
  // Cleanup
4525
5210
  // ============================================================
4526
5211
  close() {
4527
5212
  this.db.close();
4528
5213
  }
4529
5214
  };
5215
+ function generateWorkspaceId() {
5216
+ return `ws_${randomBytes3(8).toString("hex")}`;
5217
+ }
5218
+ function mapWorkspaceRow(row) {
5219
+ return {
5220
+ id: row.id,
5221
+ name: row.name,
5222
+ slug: row.slug,
5223
+ description: row.description ?? void 0,
5224
+ isDefault: row.is_default === 1,
5225
+ createdAt: row.created_at,
5226
+ updatedAt: row.updated_at
5227
+ };
5228
+ }
5229
+ function mapApiKeyRow(row) {
5230
+ return {
5231
+ key: row.key,
5232
+ workspaceId: row.workspace_id,
5233
+ label: row.label,
5234
+ createdAt: row.created_at,
5235
+ lastUsedAt: row.last_used_at ?? void 0,
5236
+ expiresAt: row.expires_at ?? void 0,
5237
+ revokedAt: row.revoked_at ?? void 0
5238
+ };
5239
+ }
4530
5240
 
4531
5241
  // src/pm/session-parser.ts
4532
5242
  import { createReadStream } from "fs";
@@ -4733,13 +5443,13 @@ async function parseSessionJsonl(jsonlPath, sessionId, projectId) {
4733
5443
 
4734
5444
  // src/pm/project-discovery.ts
4735
5445
  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";
5446
+ import { join as join5, basename as basename2 } from "path";
5447
+ import { existsSync as existsSync6 } from "fs";
4738
5448
  import { homedir as homedir3 } from "os";
4739
5449
  var LOG_PREFIX = "[RuntimeScope PM]";
4740
5450
  async function detectSdkInstalled(projectPath) {
4741
5451
  try {
4742
- const pkgPath = join4(projectPath, "package.json");
5452
+ const pkgPath = join5(projectPath, "package.json");
4743
5453
  const pkg = JSON.parse(await readFile2(pkgPath, "utf-8"));
4744
5454
  const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
4745
5455
  if ("@runtimescope/sdk" in allDeps || "@runtimescope/server-sdk" in allDeps) {
@@ -4749,13 +5459,13 @@ async function detectSdkInstalled(projectPath) {
4749
5459
  const workspaces = Array.isArray(pkg.workspaces) ? pkg.workspaces : pkg.workspaces.packages ?? [];
4750
5460
  for (const ws of workspaces) {
4751
5461
  const wsBase = ws.replace(/\/?\*$/, "");
4752
- const wsDir = join4(projectPath, wsBase);
5462
+ const wsDir = join5(projectPath, wsBase);
4753
5463
  try {
4754
5464
  const entries = await readdir2(wsDir, { withFileTypes: true });
4755
5465
  for (const entry of entries) {
4756
5466
  if (!entry.isDirectory()) continue;
4757
5467
  try {
4758
- const wsPkg = JSON.parse(await readFile2(join4(wsDir, entry.name, "package.json"), "utf-8"));
5468
+ const wsPkg = JSON.parse(await readFile2(join5(wsDir, entry.name, "package.json"), "utf-8"));
4759
5469
  const wsDeps = { ...wsPkg.dependencies, ...wsPkg.devDependencies };
4760
5470
  if ("@runtimescope/sdk" in wsDeps || "@runtimescope/server-sdk" in wsDeps) {
4761
5471
  return true;
@@ -4770,7 +5480,7 @@ async function detectSdkInstalled(projectPath) {
4770
5480
  } catch {
4771
5481
  }
4772
5482
  try {
4773
- await stat2(join4(projectPath, "node_modules", "@runtimescope"));
5483
+ await stat2(join5(projectPath, "node_modules", "@runtimescope"));
4774
5484
  return true;
4775
5485
  } catch {
4776
5486
  return false;
@@ -4801,7 +5511,7 @@ function slugifyPath(fsPath) {
4801
5511
  }
4802
5512
  function decodeClaudeKey(key) {
4803
5513
  const naive = "/" + key.slice(1).replace(/-/g, "/");
4804
- if (existsSync5(naive)) return naive;
5514
+ if (existsSync6(naive)) return naive;
4805
5515
  const parts = key.slice(1).split("-");
4806
5516
  return resolvePathSegments(parts);
4807
5517
  }
@@ -4809,16 +5519,16 @@ function resolvePathSegments(parts) {
4809
5519
  if (parts.length === 0) return null;
4810
5520
  function tryResolve(prefix, remaining) {
4811
5521
  if (remaining.length === 0) {
4812
- return existsSync5(prefix) ? prefix : null;
5522
+ return existsSync6(prefix) ? prefix : null;
4813
5523
  }
4814
5524
  for (let count = remaining.length; count >= 1; count--) {
4815
5525
  const segment = remaining.slice(0, count).join("-");
4816
- const candidate = join4(prefix, segment);
5526
+ const candidate = join5(prefix, segment);
4817
5527
  if (count === remaining.length) {
4818
- if (existsSync5(candidate)) return candidate;
5528
+ if (existsSync6(candidate)) return candidate;
4819
5529
  } else {
4820
5530
  try {
4821
- if (existsSync5(candidate)) {
5531
+ if (existsSync6(candidate)) {
4822
5532
  const result = tryResolve(candidate, remaining.slice(count));
4823
5533
  if (result) return result;
4824
5534
  }
@@ -4834,13 +5544,14 @@ function toPeriod(timestampMs) {
4834
5544
  const d = new Date(timestampMs);
4835
5545
  const year = d.getFullYear();
4836
5546
  const month = String(d.getMonth() + 1).padStart(2, "0");
4837
- return `${year}-${month}`;
5547
+ const day = String(d.getDate()).padStart(2, "0");
5548
+ return `${year}-${month}-${day}`;
4838
5549
  }
4839
5550
  var ProjectDiscovery = class {
4840
5551
  constructor(pmStore, projectManager, claudeBaseDir) {
4841
5552
  this.pmStore = pmStore;
4842
5553
  this.projectManager = projectManager;
4843
- this.claudeBaseDir = claudeBaseDir ?? join4(homedir3(), ".claude");
5554
+ this.claudeBaseDir = claudeBaseDir ?? join5(homedir3(), ".claude");
4844
5555
  }
4845
5556
  claudeBaseDir;
4846
5557
  /**
@@ -4873,7 +5584,7 @@ var ProjectDiscovery = class {
4873
5584
  sessionsUpdated: 0,
4874
5585
  errors: []
4875
5586
  };
4876
- const projectsDir = join4(this.claudeBaseDir, "projects");
5587
+ const projectsDir = join5(this.claudeBaseDir, "projects");
4877
5588
  try {
4878
5589
  await stat2(projectsDir);
4879
5590
  } catch {
@@ -4940,6 +5651,7 @@ var ProjectDiscovery = class {
4940
5651
  await this.pmStore.upsertProject(updated);
4941
5652
  result.projectsUpdated = (result.projectsUpdated ?? 0) + 1;
4942
5653
  } else {
5654
+ if (this.pmStore.isDeletedPath(sourcePath)) continue;
4943
5655
  const fsPath = projectDir;
4944
5656
  const project = {
4945
5657
  id,
@@ -4985,10 +5697,10 @@ var ProjectDiscovery = class {
4985
5697
  if (!project.claudeProjectKey) {
4986
5698
  return 0;
4987
5699
  }
4988
- const projectDir = join4(this.claudeBaseDir, "projects", project.claudeProjectKey);
5700
+ const projectDir = join5(this.claudeBaseDir, "projects", project.claudeProjectKey);
4989
5701
  let sessionsIndexed = 0;
4990
5702
  try {
4991
- const indexPath = join4(projectDir, "sessions-index.json");
5703
+ const indexPath = join5(projectDir, "sessions-index.json");
4992
5704
  let indexEntries = null;
4993
5705
  try {
4994
5706
  const indexContent = await readFile2(indexPath, "utf-8");
@@ -5001,7 +5713,7 @@ var ProjectDiscovery = class {
5001
5713
  for (const jsonlFile of jsonlFiles) {
5002
5714
  try {
5003
5715
  const sessionId = jsonlFile.replace(".jsonl", "");
5004
- const jsonlPath = join4(projectDir, jsonlFile);
5716
+ const jsonlPath = join5(projectDir, jsonlFile);
5005
5717
  const fileStat = await stat2(jsonlPath);
5006
5718
  const fileSize = fileStat.size;
5007
5719
  const existingSession = await this.pmStore.getSession(sessionId);
@@ -5036,7 +5748,7 @@ var ProjectDiscovery = class {
5036
5748
  * Process a single Claude project directory key.
5037
5749
  */
5038
5750
  async processClaudeProject(key, result) {
5039
- const projectDir = join4(this.claudeBaseDir, "projects", key);
5751
+ const projectDir = join5(this.claudeBaseDir, "projects", key);
5040
5752
  let fsPath = decodeClaudeKey(key);
5041
5753
  if (!fsPath) {
5042
5754
  fsPath = await this.resolvePathFromIndex(projectDir);
@@ -5062,6 +5774,8 @@ var ProjectDiscovery = class {
5062
5774
  await this.pmStore.upsertProject(updated);
5063
5775
  result.projectsUpdated = (result.projectsUpdated ?? 0) + 1;
5064
5776
  } else {
5777
+ if (fsPath && this.pmStore.isDeletedPath(fsPath)) return;
5778
+ if (this.pmStore.isDeletedPath(key)) return;
5065
5779
  const project = {
5066
5780
  id,
5067
5781
  name,
@@ -5088,7 +5802,7 @@ var ProjectDiscovery = class {
5088
5802
  */
5089
5803
  async resolvePathFromIndex(projectDir) {
5090
5804
  try {
5091
- const indexPath = join4(projectDir, "sessions-index.json");
5805
+ const indexPath = join5(projectDir, "sessions-index.json");
5092
5806
  const content = await readFile2(indexPath, "utf-8");
5093
5807
  const index = JSON.parse(content);
5094
5808
  const entry = index.entries?.find((e) => e.projectPath);
@@ -5103,11 +5817,11 @@ var ProjectDiscovery = class {
5103
5817
  */
5104
5818
  async indexSessionsForClaudeProject(projectId, claudeKey) {
5105
5819
  const counts = { discovered: 0, updated: 0 };
5106
- const projectDir = join4(this.claudeBaseDir, "projects", claudeKey);
5820
+ const projectDir = join5(this.claudeBaseDir, "projects", claudeKey);
5107
5821
  try {
5108
5822
  let indexEntries = null;
5109
5823
  try {
5110
- const indexPath = join4(projectDir, "sessions-index.json");
5824
+ const indexPath = join5(projectDir, "sessions-index.json");
5111
5825
  const indexContent = await readFile2(indexPath, "utf-8");
5112
5826
  const index = JSON.parse(indexContent);
5113
5827
  indexEntries = index.entries ?? [];
@@ -5118,7 +5832,7 @@ var ProjectDiscovery = class {
5118
5832
  for (const jsonlFile of jsonlFiles) {
5119
5833
  try {
5120
5834
  const sessionId = jsonlFile.replace(".jsonl", "");
5121
- const jsonlPath = join4(projectDir, jsonlFile);
5835
+ const jsonlPath = join5(projectDir, jsonlFile);
5122
5836
  const fileStat = await stat2(jsonlPath);
5123
5837
  const fileSize = fileStat.size;
5124
5838
  const existingSession = await this.pmStore.getSession(sessionId);
@@ -5308,6 +6022,11 @@ export {
5308
6022
  resolveTlsConfig,
5309
6023
  CollectorServer,
5310
6024
  ProjectManager,
6025
+ readProjectConfig,
6026
+ writeProjectConfig,
6027
+ scaffoldProjectConfig,
6028
+ resolveProjectAppNames,
6029
+ migrateProjectIds,
5311
6030
  AuthManager,
5312
6031
  generateApiKey,
5313
6032
  BUILT_IN_RULES,
@@ -5326,4 +6045,4 @@ export {
5326
6045
  parseSessionJsonl,
5327
6046
  ProjectDiscovery
5328
6047
  };
5329
- //# sourceMappingURL=chunk-GENCCHYK.js.map
6048
+ //# sourceMappingURL=chunk-WWFIEANS.js.map