@threadbase-sh/streamer 1.20.0 → 1.21.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.
package/dist/index.js CHANGED
@@ -360,6 +360,31 @@ function validatePublicUrl(raw) {
360
360
  function stripTrailingSlash(url) {
361
361
  return url.endsWith("/") ? url.slice(0, -1) : url;
362
362
  }
363
+ function setApiKey(key) {
364
+ mkdirSync(CONFIG_DIR, { recursive: true });
365
+ let content = "";
366
+ try {
367
+ content = readFileSync(CONFIG_FILE, "utf-8");
368
+ } catch (err) {
369
+ if (err.code !== "ENOENT") throw err;
370
+ }
371
+ const apiKeyLine = `api_key: ${key}`;
372
+ let updated;
373
+ if (/^api_key:\s*.+$/m.test(content)) {
374
+ updated = content.replace(/^api_key:\s*.+$/m, apiKeyLine);
375
+ } else if (content.length === 0 || content.endsWith("\n")) {
376
+ updated = `${content}${apiKeyLine}
377
+ `;
378
+ } else {
379
+ updated = `${content}
380
+ ${apiKeyLine}
381
+ `;
382
+ }
383
+ const tmpFile = `${CONFIG_FILE}.tmp`;
384
+ writeFileSync(tmpFile, updated, { encoding: "utf-8", mode: 384 });
385
+ chmodSync(tmpFile, 384);
386
+ renameSync(tmpFile, CONFIG_FILE);
387
+ }
363
388
 
364
389
  // src/db/config.ts
365
390
  function isDbEnabled() {
@@ -1458,7 +1483,7 @@ import {
1458
1483
  createReadStream,
1459
1484
  existsSync as existsSync6,
1460
1485
  watch as fsWatch,
1461
- readdirSync as readdirSync3,
1486
+ readdirSync as readdirSync4,
1462
1487
  readFileSync as readFileSync6,
1463
1488
  statSync as statSync5
1464
1489
  } from "fs";
@@ -1766,13 +1791,23 @@ var authMiddleware = (deps) => async (c, next) => {
1766
1791
  };
1767
1792
 
1768
1793
  // src/api/middleware/cors.middleware.ts
1794
+ var ALLOWED_ORIGINS = /* @__PURE__ */ new Set([
1795
+ "http://localhost:8081",
1796
+ "http://localhost:19006",
1797
+ "http://localhost:3000"
1798
+ ]);
1769
1799
  var corsMiddleware = () => async (c, next) => {
1770
- c.res.headers.set("Access-Control-Allow-Origin", "*");
1771
- c.res.headers.set("Access-Control-Allow-Methods", "GET, POST, PATCH, OPTIONS");
1772
- c.res.headers.set("Access-Control-Allow-Headers", "Authorization, Content-Type, If-None-Match");
1773
- c.res.headers.set("Access-Control-Expose-Headers", "ETag");
1800
+ const origin = c.req.header("origin");
1801
+ const allowedOrigin = origin && ALLOWED_ORIGINS.has(origin) ? origin : null;
1802
+ if (allowedOrigin) {
1803
+ c.res.headers.set("Access-Control-Allow-Origin", allowedOrigin);
1804
+ c.res.headers.set("Vary", "Origin");
1805
+ c.res.headers.set("Access-Control-Allow-Methods", "GET, POST, PATCH, OPTIONS");
1806
+ c.res.headers.set("Access-Control-Allow-Headers", "Authorization, Content-Type, If-None-Match");
1807
+ c.res.headers.set("Access-Control-Expose-Headers", "ETag");
1808
+ }
1774
1809
  if (c.req.method === "OPTIONS") {
1775
- return c.newResponse(null, 204);
1810
+ return c.newResponse(null, allowedOrigin ? 204 : 403);
1776
1811
  }
1777
1812
  await next();
1778
1813
  };
@@ -1959,6 +1994,19 @@ var createMiscRoutes = (deps) => {
1959
1994
  });
1960
1995
  });
1961
1996
  app.get("/api/profiles", (c) => c.json([]));
1997
+ app.post("/api/auth/rotate", (c) => {
1998
+ if (deps.localNoAuth) {
1999
+ return c.json({ error: "key rotation is disabled while localNoAuth is active" }, 403);
2000
+ }
2001
+ const { newKey, persisted } = deps.rotateApiKey();
2002
+ return c.json({
2003
+ apiKey: newKey,
2004
+ persisted,
2005
+ ...persisted ? {} : {
2006
+ warning: "Key rotated in memory only. The server was started with --api-key, so the old key will be restored on restart. Remove --api-key and let the server manage the key via ~/.threadbase/server.yaml for rotation to survive restarts."
2007
+ }
2008
+ });
2009
+ });
1962
2010
  app.post("/api/push/register", (c) => c.json({ ok: true }));
1963
2011
  app.post("/api/__update", async (c) => {
1964
2012
  const cfg = loadUpdateConfig();
@@ -2032,6 +2080,11 @@ var ALREADY_HANDLED4 = 597;
2032
2080
  var alreadyHandled4 = () => new Response(null, { status: ALREADY_HANDLED4 });
2033
2081
  var createProjectRoutes = (deps) => {
2034
2082
  const app = new Hono7();
2083
+ app.get("/", (c) => {
2084
+ const url = new URL(c.req.url);
2085
+ deps.handleListProjects(url, c.env.outgoing);
2086
+ return alreadyHandled4();
2087
+ });
2035
2088
  app.get("/popular", (c) => {
2036
2089
  const url = new URL(c.req.url);
2037
2090
  deps.handleGetPopularProjects(url, c.env.outgoing);
@@ -2051,11 +2104,6 @@ var createScannerRoutes = (deps) => {
2051
2104
  await deps.handleSearch(url, c.env.outgoing);
2052
2105
  return alreadyHandled5();
2053
2106
  });
2054
- app.get("/project-chats", async (c) => {
2055
- const url = new URL(c.req.url);
2056
- await deps.handleListProjectChats(url, c.env.outgoing);
2057
- return alreadyHandled5();
2058
- });
2059
2107
  return app;
2060
2108
  };
2061
2109
 
@@ -3227,297 +3275,39 @@ async function recordUpload(pool2, instanceId, row) {
3227
3275
  );
3228
3276
  }
3229
3277
 
3230
- // src/schemas/queryParams.schema.ts
3231
- import { z as z2 } from "zod";
3232
- var ListProjectChatsQuerySchema = z2.object({
3233
- refresh: z2.enum(["1"]).optional(),
3234
- refreshConversations: z2.enum(["1"]).optional()
3235
- });
3236
-
3237
- // src/services/cache/cacheMetadata.ts
3238
- function getCacheMetadata(repo, key) {
3239
- return repo.getCacheMetadata(key);
3240
- }
3241
- function setCacheMetadata(repo, key, value) {
3242
- repo.setCacheMetadata(key, value);
3243
- }
3244
-
3245
- // src/utils/dates.ts
3246
- import { compareDesc, isValid, parseISO } from "date-fns";
3247
- function parseIsoDateOrNull(value) {
3248
- if (!value) return null;
3249
- const parsed = parseISO(value);
3250
- return isValid(parsed) ? parsed : null;
3251
- }
3252
- function compareIsoDesc(a, b) {
3253
- const dateA = parseIsoDateOrNull(a);
3254
- const dateB = parseIsoDateOrNull(b);
3255
- if (!dateA && !dateB) return 0;
3256
- if (!dateA) return 1;
3257
- if (!dateB) return -1;
3258
- return compareDesc(dateA, dateB);
3259
- }
3260
-
3261
- // src/services/projects/ensureProjectsForConversations.ts
3262
- function ensureProjectsForConversations(repo, conversations) {
3263
- const conversationsByPath = /* @__PURE__ */ new Map();
3264
- for (const conversation of conversations) {
3265
- if (!conversation.projectPath) continue;
3266
- const canonical = canonicalizeProjectPath(conversation.projectPath);
3267
- if (!canonical) continue;
3268
- const existing = conversationsByPath.get(canonical) ?? [];
3269
- existing.push(conversation);
3270
- conversationsByPath.set(canonical, existing);
3271
- }
3272
- const pathToProjectId = /* @__PURE__ */ new Map();
3273
- for (const [path, projectConversations] of conversationsByPath) {
3274
- const latest = pickLatestConversation(projectConversations);
3275
- const project = repo.upsertProjectByPath(path, {
3276
- lastConversationId: latest?.id ?? null,
3277
- lastConversationCreatedAt: latest?.createdAt ?? null,
3278
- latestMessageAt: latest?.latestMessageAt ?? null
3279
- });
3280
- pathToProjectId.set(path, project.id);
3281
- }
3282
- return pathToProjectId;
3283
- }
3284
- function pickLatestConversation(conversations) {
3285
- if (conversations.length === 0) return void 0;
3286
- return [...conversations].sort((a, b) => {
3287
- const cmp = compareIsoDesc(a.latestMessageAt ?? null, b.latestMessageAt ?? null);
3288
- if (cmp !== 0) return cmp;
3289
- return compareIsoDesc(a.createdAt ?? null, b.createdAt ?? null);
3290
- })[0];
3291
- }
3292
-
3293
- // src/services/conversations/refreshConversationCache.ts
3294
- function refreshConversationCache(deps) {
3295
- const { projectsRepo, conversationsRepo, cacheMetadataRepo } = deps;
3296
- const conversations = conversationsRepo.listConversationsForProjectBackfill();
3297
- const pathToProjectId = ensureProjectsForConversations(
3298
- projectsRepo,
3299
- conversations.map((c) => ({
3300
- id: c.id,
3301
- projectPath: c.projectPath,
3302
- latestMessageAt: c.lastActivity ?? null,
3303
- createdAt: c.lastActivity ?? null
3304
- }))
3305
- );
3306
- let conversationsBackfilled = 0;
3307
- for (const conversation of conversations) {
3308
- if (!conversation.projectPath) continue;
3309
- if (conversation.projectId) continue;
3310
- const projectId = pathToProjectId.get(canonicalizeProjectPath(conversation.projectPath));
3311
- if (!projectId) continue;
3312
- conversationsRepo.updateConversationProjectId({
3313
- conversationId: conversation.id,
3314
- projectId
3315
- });
3316
- conversationsBackfilled += 1;
3317
- }
3318
- const latest = conversationsRepo.getLatestConversation();
3319
- if (latest) {
3320
- setCacheMetadata(cacheMetadataRepo, "last_conversation_id", latest.id);
3321
- if (latest.lastActivity) {
3322
- setCacheMetadata(cacheMetadataRepo, "last_conversation_created_at", latest.lastActivity);
3323
- }
3324
- }
3325
- setCacheMetadata(cacheMetadataRepo, "conversations_last_indexed_at", (/* @__PURE__ */ new Date()).toISOString());
3326
- return {
3327
- projectsTouched: pathToProjectId.size,
3328
- conversationsBackfilled,
3329
- latestConversationId: latest?.id ?? null
3330
- };
3331
- }
3332
-
3333
- // src/services/conversations/shouldRefreshProjectsFromHdd.ts
3334
- import { statSync as statSync3 } from "fs";
3278
+ // src/handlers/handleListProjects.ts
3279
+ import { readdirSync as readdirSync3, statSync as statSync3 } from "fs";
3335
3280
  import { homedir as homedir4 } from "os";
3336
3281
  import { join as join10 } from "path";
3337
- var DEFAULT_PROJECTS_DIR = join10(homedir4(), ".claude", "projects");
3338
- function shouldRefreshProjectsFromHdd(conversationsRepo, cacheMetadataRepo, opts = {}) {
3339
- if (conversationsRepo.hasOrphanRows()) return true;
3340
- const projectsDir = opts.projectsDir ?? DEFAULT_PROJECTS_DIR;
3341
- let dirMtimeMs;
3282
+ function decodeProjectPath(dirName) {
3283
+ return dirName.replace(/-/g, "/");
3284
+ }
3285
+ function handleListProjects(url, res) {
3286
+ const limit = Math.max(1, parseInt(url.searchParams.get("limit") ?? "50", 10) || 50);
3287
+ const offset = Math.max(0, parseInt(url.searchParams.get("offset") ?? "0", 10) || 0);
3288
+ const projectsDir = join10(homedir4(), ".claude", "projects");
3289
+ let entries;
3342
3290
  try {
3343
- dirMtimeMs = statSync3(projectsDir).mtimeMs;
3291
+ entries = readdirSync3(projectsDir).map((dirName) => {
3292
+ const fullPath = join10(projectsDir, dirName);
3293
+ let mtime = 0;
3294
+ try {
3295
+ mtime = statSync3(fullPath).mtimeMs;
3296
+ } catch {
3297
+ }
3298
+ const path = decodeProjectPath(dirName);
3299
+ const name = path.split("/").filter(Boolean).pop() ?? dirName;
3300
+ return { name, path, dirName, mtime };
3301
+ }).sort((a, b) => b.mtime - a.mtime);
3344
3302
  } catch {
3345
- return false;
3346
- }
3347
- const lastIndexedIso = getCacheMetadata(cacheMetadataRepo, "conversations_last_indexed_at");
3348
- if (!lastIndexedIso) return true;
3349
- const lastIndexedMs = Date.parse(lastIndexedIso);
3350
- if (Number.isNaN(lastIndexedMs)) return true;
3351
- return dirMtimeMs > lastIndexedMs;
3352
- }
3353
-
3354
- // src/services/sessions/ensureSessionProjectIdsFromExistingProjects.ts
3355
- function ensureSessionProjectIdsFromExistingProjects(projectsRepo, sessionsRepo) {
3356
- let linked = 0;
3357
- let missing = 0;
3358
- for (const session of sessionsRepo.listManagedSessions()) {
3359
- if (session.projectId) continue;
3360
- const canonical = canonicalizeProjectPath(session.projectPath);
3361
- if (!canonical) continue;
3362
- const project = projectsRepo.getProjectByPath(canonical);
3363
- if (!project) {
3364
- missing += 1;
3365
- continue;
3366
- }
3367
- sessionsRepo.updateSessionProjectId({ sessionId: session.id, projectId: project.id });
3368
- linked += 1;
3369
- }
3370
- return { linked, missing };
3371
- }
3372
-
3373
- // src/services/projectChats/sortProjectChats.ts
3374
- function sortProjectChats(a, b) {
3375
- const byLatest = compareIsoDesc(a.latestMessageAt, b.latestMessageAt);
3376
- if (byLatest !== 0) return byLatest;
3377
- const byUpdated = compareIsoDesc(a.updatedAt ?? null, b.updatedAt ?? null);
3378
- if (byUpdated !== 0) return byUpdated;
3379
- const byCreated = compareIsoDesc(a.createdAt ?? null, b.createdAt ?? null);
3380
- if (byCreated !== 0) return byCreated;
3381
- return a.title.localeCompare(b.title);
3382
- }
3383
-
3384
- // src/services/projectChats/mergeProjectChats.ts
3385
- function mergeProjectChats(args) {
3386
- const { sessions, conversations } = args;
3387
- const resumedConversationIds = new Set(
3388
- sessions.filter((c) => c.type === "session").map((c) => c.resumedFromConversationId).filter((id) => Boolean(id))
3389
- );
3390
- const visibleConversations = conversations.filter((c) => {
3391
- if (c.type !== "conversation") return true;
3392
- return !resumedConversationIds.has(c.id);
3393
- });
3394
- return [...sessions, ...visibleConversations].sort(sortProjectChats);
3395
- }
3396
-
3397
- // src/services/projectChats/deriveProjectChatTitle.ts
3398
- function deriveProjectChatTitle(input) {
3399
- const trimmed = input.title?.trim();
3400
- if (trimmed) return trimmed;
3401
- const name = input.projectName?.trim();
3402
- if (name) return name;
3403
- const pathSuffix = input.projectPath ? input.projectPath.split(/[/\\]/).filter(Boolean).slice(-2).join("/") : "";
3404
- if (pathSuffix) return pathSuffix;
3405
- return `Untitled \xB7 ${input.id.slice(0, 8)}`;
3406
- }
3407
-
3408
- // src/services/projectChats/normalizeConversationToProjectChat.ts
3409
- function normalizeConversationToProjectChat(conversation) {
3410
- if (!conversation.projectId) {
3411
- throw new Error(
3412
- `normalizeConversationToProjectChat: conversation ${conversation.id} has no projectId \u2014 resolve it before normalizing`
3413
- );
3414
- }
3415
- return {
3416
- type: "conversation",
3417
- id: conversation.id,
3418
- projectId: conversation.projectId,
3419
- projectPath: conversation.projectPath ?? null,
3420
- title: deriveProjectChatTitle({
3421
- title: conversation.title,
3422
- projectName: conversation.projectName,
3423
- projectPath: conversation.projectPath,
3424
- id: conversation.id
3425
- }),
3426
- latestMessageAt: conversation.lastActivity ?? null,
3427
- updatedAt: conversation.lastActivity ?? null,
3428
- createdAt: null,
3429
- status: "resumable",
3430
- source: "hdd-cache",
3431
- provider: conversation.provider ?? CLAUDE_CODE_PROVIDER,
3432
- indexedAt: null,
3433
- fileMtime: null,
3434
- filePath: conversation.filePath ?? null,
3435
- sourceHash: null
3436
- };
3437
- }
3438
-
3439
- // src/services/projectChats/normalizeSessionToProjectChat.ts
3440
- function normalizeSessionToProjectChat(session) {
3441
- if (!session.projectId) {
3442
- throw new Error(
3443
- `normalizeSessionToProjectChat: session ${session.id} has no projectId \u2014 resolve it before normalizing`
3444
- );
3445
- }
3446
- return {
3447
- type: "session",
3448
- id: session.id,
3449
- projectId: session.projectId,
3450
- projectPath: session.projectPath ?? null,
3451
- title: deriveProjectChatTitle({
3452
- title: session.sessionName,
3453
- projectName: session.projectName,
3454
- projectPath: session.projectPath,
3455
- id: session.id
3456
- }),
3457
- latestMessageAt: session.lastMessageAt ?? session.lastActivityAt ?? null,
3458
- updatedAt: session.lastActivityAt ?? null,
3459
- createdAt: session.startedAt ?? null,
3460
- status: "active",
3461
- source: "session-store",
3462
- resumedFromConversationId: session.resumedFromConversationId ?? null
3463
- };
3464
- }
3465
-
3466
- // src/services/projectChats/listProjectChats.ts
3467
- async function listProjectChats(deps, args) {
3468
- const {
3469
- cache,
3470
- projectsRepo,
3471
- conversationsRepo,
3472
- sessionsRepo,
3473
- cacheMetadataRepo,
3474
- getSessionResponses,
3475
- getFreshScanner,
3476
- projectsDir
3477
- } = deps;
3478
- const needsRefresh = args.refreshConversations || shouldRefreshProjectsFromHdd(conversationsRepo, cacheMetadataRepo, { projectsDir });
3479
- if (needsRefresh) {
3480
- const scanner = await getFreshScanner();
3481
- const metas = [...scanner.getMetadataCache().values()];
3482
- if (metas.length > 0) {
3483
- cache.upsertFromScannerMeta(metas);
3484
- }
3485
- refreshConversationCache({ cache, projectsRepo, conversationsRepo, cacheMetadataRepo });
3486
- }
3487
- ensureSessionProjectIdsFromExistingProjects(projectsRepo, sessionsRepo);
3488
- const sessionResponses = getSessionResponses();
3489
- const sessionChats = [];
3490
- for (const s of sessionResponses) {
3491
- if (!s.projectId) continue;
3492
- sessionChats.push(normalizeSessionToProjectChat(s));
3493
- }
3494
- const conversationChats = [];
3495
- const { conversations } = cache.listConversations({ limit: 1e3, offset: 0 });
3496
- for (const c of conversations) {
3497
- if (!c.projectId) continue;
3498
- conversationChats.push(normalizeConversationToProjectChat(c));
3499
- }
3500
- return mergeProjectChats({ sessions: sessionChats, conversations: conversationChats });
3501
- }
3502
-
3503
- // src/handlers/handleListProjectChats.ts
3504
- async function handleListProjectChats(url, res, deps) {
3505
- const queryObj = Object.fromEntries(url.searchParams.entries());
3506
- const parsed = ListProjectChatsQuerySchema.safeParse(queryObj);
3507
- if (!parsed.success) {
3508
- res.writeHead(400, { "Content-Type": "application/json" });
3509
- res.end(
3510
- JSON.stringify({
3511
- error: "Invalid query parameters",
3512
- details: parsed.error.flatten()
3513
- })
3514
- );
3303
+ res.writeHead(200, { "Content-Type": "application/json" });
3304
+ res.end(JSON.stringify({ projects: [], total: 0 }));
3515
3305
  return;
3516
3306
  }
3517
- const refreshConversations = parsed.data.refreshConversations === "1" || parsed.data.refresh === "1";
3518
- const chats = await listProjectChats(deps, { refreshConversations });
3307
+ const total = entries.length;
3308
+ const page = entries.slice(offset, offset + limit).map(({ name, path, dirName }) => ({ name, path, dirName }));
3519
3309
  res.writeHead(200, { "Content-Type": "application/json" });
3520
- res.end(JSON.stringify({ projectChats: chats }));
3310
+ res.end(JSON.stringify({ projects: page, total }));
3521
3311
  }
3522
3312
 
3523
3313
  // src/pair-store.ts
@@ -3746,6 +3536,17 @@ function pruneAgentConversations(cache) {
3746
3536
  return { scanned: rows.length, pruned, missing };
3747
3537
  }
3748
3538
 
3539
+ // src/services/projectChats/deriveProjectChatTitle.ts
3540
+ function deriveProjectChatTitle(input) {
3541
+ const trimmed = input.title?.trim();
3542
+ if (trimmed) return trimmed;
3543
+ const name = input.projectName?.trim();
3544
+ if (name) return name;
3545
+ const pathSuffix = input.projectPath ? input.projectPath.split(/[/\\]/).filter(Boolean).slice(-2).join("/") : "";
3546
+ if (pathSuffix) return pathSuffix;
3547
+ return `Untitled \xB7 ${input.id.slice(0, 8)}`;
3548
+ }
3549
+
3749
3550
  // src/services/questions/detectAskUserQuestion.ts
3750
3551
  function normalizeContent2(raw) {
3751
3552
  if (Array.isArray(raw)) return raw;
@@ -4149,6 +3950,14 @@ function debounce(fn, waitMs) {
4149
3950
  return debounced;
4150
3951
  }
4151
3952
 
3953
+ // src/utils/dates.ts
3954
+ import { compareDesc, isValid, parseISO } from "date-fns";
3955
+ function parseIsoDateOrNull(value) {
3956
+ if (!value) return null;
3957
+ const parsed = parseISO(value);
3958
+ return isValid(parsed) ? parsed : null;
3959
+ }
3960
+
4152
3961
  // src/utils/isScannedSnapshotStale.ts
4153
3962
  var STALENESS_TOLERANCE_MS = 1e3;
4154
3963
  function isScannedSnapshotStale(snapshotTimestamp, fileMtimeMs) {
@@ -4324,6 +4133,7 @@ var StreamerServer = class {
4324
4133
  binding = false;
4325
4134
  cacheReady = false;
4326
4135
  apiKey;
4136
+ apiKeySource;
4327
4137
  localNoAuth;
4328
4138
  logMenubarRequests;
4329
4139
  verbose;
@@ -4335,6 +4145,8 @@ var StreamerServer = class {
4335
4145
  publicUrl = null;
4336
4146
  pairTokens = new PairTokenStore();
4337
4147
  exchangeAttempts = /* @__PURE__ */ new Map();
4148
+ sessionStartAttempts = /* @__PURE__ */ new Map();
4149
+ sessionInputAttempts = /* @__PURE__ */ new Map();
4338
4150
  ptyGracePeriodMs;
4339
4151
  defaultSystemPrompt;
4340
4152
  // Map of sessionId → grace timer; fires to kill PTY after WS disconnect
@@ -4370,8 +4182,14 @@ var StreamerServer = class {
4370
4182
  constructor(config) {
4371
4183
  this.sessionStatusBus.setMaxListeners(0);
4372
4184
  this.apiKey = config.apiKey;
4185
+ this.apiKeySource = config.apiKeySource ?? "config";
4373
4186
  this.localNoAuth = config.localNoAuth ?? false;
4374
4187
  this.logMenubarRequests = config.logMenubarRequests ?? false;
4188
+ if (this.localNoAuth) {
4189
+ console.warn(
4190
+ "[WARN] localNoAuth is ENABLED \u2014 all requests from localhost bypass authentication. Do not run with --local-no-auth in shared or production environments."
4191
+ );
4192
+ }
4375
4193
  this.verbose = config.verbose ?? false;
4376
4194
  this.disableDb = config.disableDb ?? false;
4377
4195
  this.scanProfiles = config.scanProfiles;
@@ -4549,10 +4367,15 @@ var StreamerServer = class {
4549
4367
  });
4550
4368
  }
4551
4369
  const agentClient = this.agentClient;
4370
+ const self = this;
4552
4371
  const apiDeps = {
4553
- apiKey: this.apiKey,
4372
+ // ponytail: getter so rotateApiKey() takes effect without restarting the server
4373
+ get apiKey() {
4374
+ return self.apiKey;
4375
+ },
4554
4376
  localNoAuth: this.localNoAuth,
4555
4377
  logMenubarRequests: this.logMenubarRequests,
4378
+ rotateApiKey: () => this.rotateApiKey(),
4556
4379
  publicUrl: this.publicUrl,
4557
4380
  browseRoot: this.browseRoot,
4558
4381
  ptyManager: this.ptyManager,
@@ -4583,8 +4406,8 @@ var StreamerServer = class {
4583
4406
  handleConversationsCount: (url, res) => this.handleConversationsCount(url, res),
4584
4407
  handleGetConversation: (id, url, res, ifNoneMatch) => this.handleGetConversation(id, url, res, ifNoneMatch),
4585
4408
  handleSearch: (url, res) => this.handleSearch(url, res),
4409
+ handleListProjects: (url, res) => handleListProjects(url, res),
4586
4410
  handleGetPopularProjects: (url, res) => this.handleGetPopularProjects(url, res),
4587
- handleListProjectChats: (url, res) => this.handleListProjectChats(url, res),
4588
4411
  handlePairStart: (res) => this.handlePairStart(res),
4589
4412
  handlePairExchange: (req, res) => this.handlePairExchange(req, res),
4590
4413
  handleBrowse: (url, res) => this.handleBrowse(url, res),
@@ -5019,19 +4842,38 @@ var StreamerServer = class {
5019
4842
  machineName: hostname2()
5020
4843
  });
5021
4844
  }
5022
- checkExchangeRateLimit(ip) {
4845
+ rotateApiKey() {
4846
+ const newKey = generateApiKey();
4847
+ const persisted = this.apiKeySource === "config";
4848
+ if (persisted) setApiKey(newKey);
4849
+ this.apiKey = newKey;
4850
+ return { newKey, persisted };
4851
+ }
4852
+ checkRateLimit(map, key, limit, windowMs) {
5023
4853
  const now = Date.now();
5024
- const windowMs = 6e4;
5025
- const limit = 5;
5026
- const arr = (this.exchangeAttempts.get(ip) ?? []).filter((t) => now - t < windowMs);
4854
+ const arr = (map.get(key) ?? []).filter((t) => now - t < windowMs);
5027
4855
  if (arr.length >= limit) {
5028
- this.exchangeAttempts.set(ip, arr);
4856
+ map.set(key, arr);
5029
4857
  return false;
5030
4858
  }
5031
4859
  arr.push(now);
5032
- this.exchangeAttempts.set(ip, arr);
4860
+ map.set(key, arr);
4861
+ setTimeout(() => {
4862
+ const remaining = (map.get(key) ?? []).filter((t) => Date.now() - t < windowMs);
4863
+ if (remaining.length === 0) map.delete(key);
4864
+ else map.set(key, remaining);
4865
+ }, windowMs);
5033
4866
  return true;
5034
4867
  }
4868
+ checkExchangeRateLimit(ip) {
4869
+ return this.checkRateLimit(this.exchangeAttempts, ip, 5, 6e4);
4870
+ }
4871
+ checkSessionStartRateLimit(ip) {
4872
+ return this.checkRateLimit(this.sessionStartAttempts, ip, 10, 6e4);
4873
+ }
4874
+ checkSessionInputRateLimit(sessionId) {
4875
+ return this.checkRateLimit(this.sessionInputAttempts, sessionId, 500, 6e4);
4876
+ }
5035
4877
  async handleListConversations(url, res) {
5036
4878
  const limit = intParam(url, "limit", 50);
5037
4879
  const offset = intParam(url, "offset", 0);
@@ -5193,21 +5035,6 @@ var StreamerServer = class {
5193
5035
  const projects = this.cache.getPopularProjects(limit);
5194
5036
  json(res, 200, { projects, total: projects.length });
5195
5037
  }
5196
- async handleListProjectChats(url, res) {
5197
- if (!this.cache || !this.projectsRepo || !this.conversationsRepo || !this.sessionsRepo || !this.cacheMetadataRepo) {
5198
- json(res, 503, { error: "Cache not available" });
5199
- return;
5200
- }
5201
- await handleListProjectChats(url, res, {
5202
- cache: this.cache,
5203
- projectsRepo: this.projectsRepo,
5204
- conversationsRepo: this.conversationsRepo,
5205
- sessionsRepo: this.sessionsRepo,
5206
- cacheMetadataRepo: this.cacheMetadataRepo,
5207
- getSessionResponses: () => this.sessionStore.list(this.ptyAttachedIds()),
5208
- getFreshScanner: () => this.getFreshScanner()
5209
- });
5210
- }
5211
5038
  buildStatCache(previousScanner) {
5212
5039
  if (!this.cache) return void 0;
5213
5040
  const dbStats = this.cache.getFileStats();
@@ -5276,12 +5103,12 @@ var StreamerServer = class {
5276
5103
  const projectsDir = join12(homedir5(), ".claude", "projects");
5277
5104
  if (!existsSync6(projectsDir)) return null;
5278
5105
  const filename = `${uuid}.jsonl`;
5279
- for (const dir of readdirSync3(projectsDir)) {
5106
+ for (const dir of readdirSync4(projectsDir)) {
5280
5107
  const fp = join12(projectsDir, dir, filename);
5281
5108
  if (existsSync6(fp)) return fp;
5282
5109
  const projectDir = join12(projectsDir, dir);
5283
5110
  try {
5284
- for (const sub of readdirSync3(projectDir)) {
5111
+ for (const sub of readdirSync4(projectDir)) {
5285
5112
  const subagentPath = join12(projectDir, sub, "subagents", filename);
5286
5113
  if (existsSync6(subagentPath)) return subagentPath;
5287
5114
  }
@@ -5684,6 +5511,10 @@ var StreamerServer = class {
5684
5511
  }
5685
5512
  }
5686
5513
  async handleSendInput(sessionId, req, res) {
5514
+ if (!this.checkSessionInputRateLimit(sessionId)) {
5515
+ json(res, 429, { error: "Too many input requests for this session. Please slow down." });
5516
+ return;
5517
+ }
5687
5518
  if (this.agentConfig.enabled) {
5688
5519
  const body2 = await readBody(req);
5689
5520
  const cache = this.cache;
@@ -5965,6 +5796,13 @@ var StreamerServer = class {
5965
5796
  json(res, 201, { sessionId: session.id });
5966
5797
  }
5967
5798
  async handleStartSession(req, res) {
5799
+ const ip = req.socket?.remoteAddress ?? "unknown";
5800
+ if (!this.checkSessionStartRateLimit(ip)) {
5801
+ json(res, 429, {
5802
+ error: "Too many session start requests. Please wait before trying again."
5803
+ });
5804
+ return;
5805
+ }
5968
5806
  if (this.agentConfig.enabled) {
5969
5807
  const body2 = await readBody(req);
5970
5808
  const result = await handleStartAgentSession(body2, {
@@ -6100,7 +5938,7 @@ var StreamerServer = class {
6100
5938
  if (!resolvedFilePath && existsSync6(projectsDir)) {
6101
5939
  try {
6102
5940
  const now = Date.now();
6103
- const recent = readdirSync3(projectsDir).filter((f) => f.endsWith(".jsonl")).map((f) => ({ f, mtime: statSync5(join12(projectsDir, f)).mtimeMs })).filter(({ mtime }) => now - mtime < 5e3).sort((a, b) => b.mtime - a.mtime)[0];
5941
+ const recent = readdirSync4(projectsDir).filter((f) => f.endsWith(".jsonl")).map((f) => ({ f, mtime: statSync5(join12(projectsDir, f)).mtimeMs })).filter(({ mtime }) => now - mtime < 5e3).sort((a, b) => b.mtime - a.mtime)[0];
6104
5942
  if (recent) resolvedFilePath = join12(projectsDir, recent.f);
6105
5943
  } catch {
6106
5944
  }