@threadbase-sh/streamer 1.20.0 → 1.22.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/cli.cjs +678 -827
- package/dist/cli.cjs.map +1 -1
- package/dist/index.cjs +161 -316
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +14 -2
- package/dist/index.d.ts +14 -2
- package/dist/index.js +166 -321
- package/dist/index.js.map +1 -1
- package/package.json +10 -5
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
|
|
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.
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
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/
|
|
3231
|
-
import {
|
|
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
|
-
|
|
3338
|
-
|
|
3339
|
-
|
|
3340
|
-
|
|
3341
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
3518
|
-
const
|
|
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({
|
|
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
|
-
|
|
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,45 @@ var StreamerServer = class {
|
|
|
5019
4842
|
machineName: hostname2()
|
|
5020
4843
|
});
|
|
5021
4844
|
}
|
|
5022
|
-
|
|
4845
|
+
rotateApiKey() {
|
|
4846
|
+
const oldKey = this.apiKey;
|
|
4847
|
+
const newKey = generateApiKey();
|
|
4848
|
+
const persisted = this.apiKeySource === "config";
|
|
4849
|
+
if (persisted) setApiKey(newKey);
|
|
4850
|
+
this.apiKey = newKey;
|
|
4851
|
+
this.log.info("API key rotated", {
|
|
4852
|
+
event: "auth.api_key_rotated",
|
|
4853
|
+
oldKeyMasked: `${oldKey.slice(0, 6)}\u2026`,
|
|
4854
|
+
newKeyMasked: `${newKey.slice(0, 6)}\u2026`,
|
|
4855
|
+
persisted
|
|
4856
|
+
});
|
|
4857
|
+
return { newKey, persisted };
|
|
4858
|
+
}
|
|
4859
|
+
checkRateLimit(map, key, limit, windowMs) {
|
|
5023
4860
|
const now = Date.now();
|
|
5024
|
-
const
|
|
5025
|
-
const limit = 5;
|
|
5026
|
-
const arr = (this.exchangeAttempts.get(ip) ?? []).filter((t) => now - t < windowMs);
|
|
4861
|
+
const arr = (map.get(key) ?? []).filter((t) => now - t < windowMs);
|
|
5027
4862
|
if (arr.length >= limit) {
|
|
5028
|
-
|
|
4863
|
+
map.set(key, arr);
|
|
5029
4864
|
return false;
|
|
5030
4865
|
}
|
|
5031
4866
|
arr.push(now);
|
|
5032
|
-
|
|
4867
|
+
map.set(key, arr);
|
|
4868
|
+
setTimeout(() => {
|
|
4869
|
+
const remaining = (map.get(key) ?? []).filter((t) => Date.now() - t < windowMs);
|
|
4870
|
+
if (remaining.length === 0) map.delete(key);
|
|
4871
|
+
else map.set(key, remaining);
|
|
4872
|
+
}, windowMs);
|
|
5033
4873
|
return true;
|
|
5034
4874
|
}
|
|
4875
|
+
checkExchangeRateLimit(ip) {
|
|
4876
|
+
return this.checkRateLimit(this.exchangeAttempts, ip, 5, 6e4);
|
|
4877
|
+
}
|
|
4878
|
+
checkSessionStartRateLimit(ip) {
|
|
4879
|
+
return this.checkRateLimit(this.sessionStartAttempts, ip, 10, 6e4);
|
|
4880
|
+
}
|
|
4881
|
+
checkSessionInputRateLimit(sessionId) {
|
|
4882
|
+
return this.checkRateLimit(this.sessionInputAttempts, sessionId, 500, 6e4);
|
|
4883
|
+
}
|
|
5035
4884
|
async handleListConversations(url, res) {
|
|
5036
4885
|
const limit = intParam(url, "limit", 50);
|
|
5037
4886
|
const offset = intParam(url, "offset", 0);
|
|
@@ -5193,21 +5042,6 @@ var StreamerServer = class {
|
|
|
5193
5042
|
const projects = this.cache.getPopularProjects(limit);
|
|
5194
5043
|
json(res, 200, { projects, total: projects.length });
|
|
5195
5044
|
}
|
|
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
5045
|
buildStatCache(previousScanner) {
|
|
5212
5046
|
if (!this.cache) return void 0;
|
|
5213
5047
|
const dbStats = this.cache.getFileStats();
|
|
@@ -5276,12 +5110,12 @@ var StreamerServer = class {
|
|
|
5276
5110
|
const projectsDir = join12(homedir5(), ".claude", "projects");
|
|
5277
5111
|
if (!existsSync6(projectsDir)) return null;
|
|
5278
5112
|
const filename = `${uuid}.jsonl`;
|
|
5279
|
-
for (const dir of
|
|
5113
|
+
for (const dir of readdirSync4(projectsDir)) {
|
|
5280
5114
|
const fp = join12(projectsDir, dir, filename);
|
|
5281
5115
|
if (existsSync6(fp)) return fp;
|
|
5282
5116
|
const projectDir = join12(projectsDir, dir);
|
|
5283
5117
|
try {
|
|
5284
|
-
for (const sub of
|
|
5118
|
+
for (const sub of readdirSync4(projectDir)) {
|
|
5285
5119
|
const subagentPath = join12(projectDir, sub, "subagents", filename);
|
|
5286
5120
|
if (existsSync6(subagentPath)) return subagentPath;
|
|
5287
5121
|
}
|
|
@@ -5684,6 +5518,10 @@ var StreamerServer = class {
|
|
|
5684
5518
|
}
|
|
5685
5519
|
}
|
|
5686
5520
|
async handleSendInput(sessionId, req, res) {
|
|
5521
|
+
if (!this.checkSessionInputRateLimit(sessionId)) {
|
|
5522
|
+
json(res, 429, { error: "Too many input requests for this session. Please slow down." });
|
|
5523
|
+
return;
|
|
5524
|
+
}
|
|
5687
5525
|
if (this.agentConfig.enabled) {
|
|
5688
5526
|
const body2 = await readBody(req);
|
|
5689
5527
|
const cache = this.cache;
|
|
@@ -5965,6 +5803,13 @@ var StreamerServer = class {
|
|
|
5965
5803
|
json(res, 201, { sessionId: session.id });
|
|
5966
5804
|
}
|
|
5967
5805
|
async handleStartSession(req, res) {
|
|
5806
|
+
const ip = req.socket?.remoteAddress ?? "unknown";
|
|
5807
|
+
if (!this.checkSessionStartRateLimit(ip)) {
|
|
5808
|
+
json(res, 429, {
|
|
5809
|
+
error: "Too many session start requests. Please wait before trying again."
|
|
5810
|
+
});
|
|
5811
|
+
return;
|
|
5812
|
+
}
|
|
5968
5813
|
if (this.agentConfig.enabled) {
|
|
5969
5814
|
const body2 = await readBody(req);
|
|
5970
5815
|
const result = await handleStartAgentSession(body2, {
|
|
@@ -6100,7 +5945,7 @@ var StreamerServer = class {
|
|
|
6100
5945
|
if (!resolvedFilePath && existsSync6(projectsDir)) {
|
|
6101
5946
|
try {
|
|
6102
5947
|
const now = Date.now();
|
|
6103
|
-
const recent =
|
|
5948
|
+
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
5949
|
if (recent) resolvedFilePath = join12(projectsDir, recent.f);
|
|
6105
5950
|
} catch {
|
|
6106
5951
|
}
|