@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/cli.cjs +442 -616
- package/dist/cli.cjs.map +1 -1
- package/dist/index.cjs +154 -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 +159 -321
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/dist/index.cjs
CHANGED
|
@@ -406,6 +406,31 @@ function validatePublicUrl(raw) {
|
|
|
406
406
|
function stripTrailingSlash(url) {
|
|
407
407
|
return url.endsWith("/") ? url.slice(0, -1) : url;
|
|
408
408
|
}
|
|
409
|
+
function setApiKey(key) {
|
|
410
|
+
(0, import_fs.mkdirSync)(CONFIG_DIR, { recursive: true });
|
|
411
|
+
let content = "";
|
|
412
|
+
try {
|
|
413
|
+
content = (0, import_fs.readFileSync)(CONFIG_FILE, "utf-8");
|
|
414
|
+
} catch (err) {
|
|
415
|
+
if (err.code !== "ENOENT") throw err;
|
|
416
|
+
}
|
|
417
|
+
const apiKeyLine = `api_key: ${key}`;
|
|
418
|
+
let updated;
|
|
419
|
+
if (/^api_key:\s*.+$/m.test(content)) {
|
|
420
|
+
updated = content.replace(/^api_key:\s*.+$/m, apiKeyLine);
|
|
421
|
+
} else if (content.length === 0 || content.endsWith("\n")) {
|
|
422
|
+
updated = `${content}${apiKeyLine}
|
|
423
|
+
`;
|
|
424
|
+
} else {
|
|
425
|
+
updated = `${content}
|
|
426
|
+
${apiKeyLine}
|
|
427
|
+
`;
|
|
428
|
+
}
|
|
429
|
+
const tmpFile = `${CONFIG_FILE}.tmp`;
|
|
430
|
+
(0, import_fs.writeFileSync)(tmpFile, updated, { encoding: "utf-8", mode: 384 });
|
|
431
|
+
(0, import_fs.chmodSync)(tmpFile, 384);
|
|
432
|
+
(0, import_fs.renameSync)(tmpFile, CONFIG_FILE);
|
|
433
|
+
}
|
|
409
434
|
|
|
410
435
|
// src/db/config.ts
|
|
411
436
|
function isDbEnabled() {
|
|
@@ -1799,13 +1824,23 @@ var authMiddleware = (deps) => async (c, next) => {
|
|
|
1799
1824
|
};
|
|
1800
1825
|
|
|
1801
1826
|
// src/api/middleware/cors.middleware.ts
|
|
1827
|
+
var ALLOWED_ORIGINS = /* @__PURE__ */ new Set([
|
|
1828
|
+
"http://localhost:8081",
|
|
1829
|
+
"http://localhost:19006",
|
|
1830
|
+
"http://localhost:3000"
|
|
1831
|
+
]);
|
|
1802
1832
|
var corsMiddleware = () => async (c, next) => {
|
|
1803
|
-
c.
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1833
|
+
const origin = c.req.header("origin");
|
|
1834
|
+
const allowedOrigin = origin && ALLOWED_ORIGINS.has(origin) ? origin : null;
|
|
1835
|
+
if (allowedOrigin) {
|
|
1836
|
+
c.res.headers.set("Access-Control-Allow-Origin", allowedOrigin);
|
|
1837
|
+
c.res.headers.set("Vary", "Origin");
|
|
1838
|
+
c.res.headers.set("Access-Control-Allow-Methods", "GET, POST, PATCH, OPTIONS");
|
|
1839
|
+
c.res.headers.set("Access-Control-Allow-Headers", "Authorization, Content-Type, If-None-Match");
|
|
1840
|
+
c.res.headers.set("Access-Control-Expose-Headers", "ETag");
|
|
1841
|
+
}
|
|
1807
1842
|
if (c.req.method === "OPTIONS") {
|
|
1808
|
-
return c.newResponse(null, 204);
|
|
1843
|
+
return c.newResponse(null, allowedOrigin ? 204 : 403);
|
|
1809
1844
|
}
|
|
1810
1845
|
await next();
|
|
1811
1846
|
};
|
|
@@ -1992,6 +2027,19 @@ var createMiscRoutes = (deps) => {
|
|
|
1992
2027
|
});
|
|
1993
2028
|
});
|
|
1994
2029
|
app.get("/api/profiles", (c) => c.json([]));
|
|
2030
|
+
app.post("/api/auth/rotate", (c) => {
|
|
2031
|
+
if (deps.localNoAuth) {
|
|
2032
|
+
return c.json({ error: "key rotation is disabled while localNoAuth is active" }, 403);
|
|
2033
|
+
}
|
|
2034
|
+
const { newKey, persisted } = deps.rotateApiKey();
|
|
2035
|
+
return c.json({
|
|
2036
|
+
apiKey: newKey,
|
|
2037
|
+
persisted,
|
|
2038
|
+
...persisted ? {} : {
|
|
2039
|
+
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."
|
|
2040
|
+
}
|
|
2041
|
+
});
|
|
2042
|
+
});
|
|
1995
2043
|
app.post("/api/push/register", (c) => c.json({ ok: true }));
|
|
1996
2044
|
app.post("/api/__update", async (c) => {
|
|
1997
2045
|
const cfg = loadUpdateConfig();
|
|
@@ -2065,6 +2113,11 @@ var ALREADY_HANDLED4 = 597;
|
|
|
2065
2113
|
var alreadyHandled4 = () => new Response(null, { status: ALREADY_HANDLED4 });
|
|
2066
2114
|
var createProjectRoutes = (deps) => {
|
|
2067
2115
|
const app = new import_hono7.Hono();
|
|
2116
|
+
app.get("/", (c) => {
|
|
2117
|
+
const url = new URL(c.req.url);
|
|
2118
|
+
deps.handleListProjects(url, c.env.outgoing);
|
|
2119
|
+
return alreadyHandled4();
|
|
2120
|
+
});
|
|
2068
2121
|
app.get("/popular", (c) => {
|
|
2069
2122
|
const url = new URL(c.req.url);
|
|
2070
2123
|
deps.handleGetPopularProjects(url, c.env.outgoing);
|
|
@@ -2084,11 +2137,6 @@ var createScannerRoutes = (deps) => {
|
|
|
2084
2137
|
await deps.handleSearch(url, c.env.outgoing);
|
|
2085
2138
|
return alreadyHandled5();
|
|
2086
2139
|
});
|
|
2087
|
-
app.get("/project-chats", async (c) => {
|
|
2088
|
-
const url = new URL(c.req.url);
|
|
2089
|
-
await deps.handleListProjectChats(url, c.env.outgoing);
|
|
2090
|
-
return alreadyHandled5();
|
|
2091
|
-
});
|
|
2092
2140
|
return app;
|
|
2093
2141
|
};
|
|
2094
2142
|
|
|
@@ -3261,297 +3309,39 @@ async function recordUpload(pool2, instanceId, row) {
|
|
|
3261
3309
|
);
|
|
3262
3310
|
}
|
|
3263
3311
|
|
|
3264
|
-
// src/
|
|
3265
|
-
var import_zod2 = require("zod");
|
|
3266
|
-
var ListProjectChatsQuerySchema = import_zod2.z.object({
|
|
3267
|
-
refresh: import_zod2.z.enum(["1"]).optional(),
|
|
3268
|
-
refreshConversations: import_zod2.z.enum(["1"]).optional()
|
|
3269
|
-
});
|
|
3270
|
-
|
|
3271
|
-
// src/services/cache/cacheMetadata.ts
|
|
3272
|
-
function getCacheMetadata(repo, key) {
|
|
3273
|
-
return repo.getCacheMetadata(key);
|
|
3274
|
-
}
|
|
3275
|
-
function setCacheMetadata(repo, key, value) {
|
|
3276
|
-
repo.setCacheMetadata(key, value);
|
|
3277
|
-
}
|
|
3278
|
-
|
|
3279
|
-
// src/utils/dates.ts
|
|
3280
|
-
var import_date_fns = require("date-fns");
|
|
3281
|
-
function parseIsoDateOrNull(value) {
|
|
3282
|
-
if (!value) return null;
|
|
3283
|
-
const parsed = (0, import_date_fns.parseISO)(value);
|
|
3284
|
-
return (0, import_date_fns.isValid)(parsed) ? parsed : null;
|
|
3285
|
-
}
|
|
3286
|
-
function compareIsoDesc(a, b) {
|
|
3287
|
-
const dateA = parseIsoDateOrNull(a);
|
|
3288
|
-
const dateB = parseIsoDateOrNull(b);
|
|
3289
|
-
if (!dateA && !dateB) return 0;
|
|
3290
|
-
if (!dateA) return 1;
|
|
3291
|
-
if (!dateB) return -1;
|
|
3292
|
-
return (0, import_date_fns.compareDesc)(dateA, dateB);
|
|
3293
|
-
}
|
|
3294
|
-
|
|
3295
|
-
// src/services/projects/ensureProjectsForConversations.ts
|
|
3296
|
-
function ensureProjectsForConversations(repo, conversations) {
|
|
3297
|
-
const conversationsByPath = /* @__PURE__ */ new Map();
|
|
3298
|
-
for (const conversation of conversations) {
|
|
3299
|
-
if (!conversation.projectPath) continue;
|
|
3300
|
-
const canonical = canonicalizeProjectPath(conversation.projectPath);
|
|
3301
|
-
if (!canonical) continue;
|
|
3302
|
-
const existing = conversationsByPath.get(canonical) ?? [];
|
|
3303
|
-
existing.push(conversation);
|
|
3304
|
-
conversationsByPath.set(canonical, existing);
|
|
3305
|
-
}
|
|
3306
|
-
const pathToProjectId = /* @__PURE__ */ new Map();
|
|
3307
|
-
for (const [path, projectConversations] of conversationsByPath) {
|
|
3308
|
-
const latest = pickLatestConversation(projectConversations);
|
|
3309
|
-
const project = repo.upsertProjectByPath(path, {
|
|
3310
|
-
lastConversationId: latest?.id ?? null,
|
|
3311
|
-
lastConversationCreatedAt: latest?.createdAt ?? null,
|
|
3312
|
-
latestMessageAt: latest?.latestMessageAt ?? null
|
|
3313
|
-
});
|
|
3314
|
-
pathToProjectId.set(path, project.id);
|
|
3315
|
-
}
|
|
3316
|
-
return pathToProjectId;
|
|
3317
|
-
}
|
|
3318
|
-
function pickLatestConversation(conversations) {
|
|
3319
|
-
if (conversations.length === 0) return void 0;
|
|
3320
|
-
return [...conversations].sort((a, b) => {
|
|
3321
|
-
const cmp = compareIsoDesc(a.latestMessageAt ?? null, b.latestMessageAt ?? null);
|
|
3322
|
-
if (cmp !== 0) return cmp;
|
|
3323
|
-
return compareIsoDesc(a.createdAt ?? null, b.createdAt ?? null);
|
|
3324
|
-
})[0];
|
|
3325
|
-
}
|
|
3326
|
-
|
|
3327
|
-
// src/services/conversations/refreshConversationCache.ts
|
|
3328
|
-
function refreshConversationCache(deps) {
|
|
3329
|
-
const { projectsRepo, conversationsRepo, cacheMetadataRepo } = deps;
|
|
3330
|
-
const conversations = conversationsRepo.listConversationsForProjectBackfill();
|
|
3331
|
-
const pathToProjectId = ensureProjectsForConversations(
|
|
3332
|
-
projectsRepo,
|
|
3333
|
-
conversations.map((c) => ({
|
|
3334
|
-
id: c.id,
|
|
3335
|
-
projectPath: c.projectPath,
|
|
3336
|
-
latestMessageAt: c.lastActivity ?? null,
|
|
3337
|
-
createdAt: c.lastActivity ?? null
|
|
3338
|
-
}))
|
|
3339
|
-
);
|
|
3340
|
-
let conversationsBackfilled = 0;
|
|
3341
|
-
for (const conversation of conversations) {
|
|
3342
|
-
if (!conversation.projectPath) continue;
|
|
3343
|
-
if (conversation.projectId) continue;
|
|
3344
|
-
const projectId = pathToProjectId.get(canonicalizeProjectPath(conversation.projectPath));
|
|
3345
|
-
if (!projectId) continue;
|
|
3346
|
-
conversationsRepo.updateConversationProjectId({
|
|
3347
|
-
conversationId: conversation.id,
|
|
3348
|
-
projectId
|
|
3349
|
-
});
|
|
3350
|
-
conversationsBackfilled += 1;
|
|
3351
|
-
}
|
|
3352
|
-
const latest = conversationsRepo.getLatestConversation();
|
|
3353
|
-
if (latest) {
|
|
3354
|
-
setCacheMetadata(cacheMetadataRepo, "last_conversation_id", latest.id);
|
|
3355
|
-
if (latest.lastActivity) {
|
|
3356
|
-
setCacheMetadata(cacheMetadataRepo, "last_conversation_created_at", latest.lastActivity);
|
|
3357
|
-
}
|
|
3358
|
-
}
|
|
3359
|
-
setCacheMetadata(cacheMetadataRepo, "conversations_last_indexed_at", (/* @__PURE__ */ new Date()).toISOString());
|
|
3360
|
-
return {
|
|
3361
|
-
projectsTouched: pathToProjectId.size,
|
|
3362
|
-
conversationsBackfilled,
|
|
3363
|
-
latestConversationId: latest?.id ?? null
|
|
3364
|
-
};
|
|
3365
|
-
}
|
|
3366
|
-
|
|
3367
|
-
// src/services/conversations/shouldRefreshProjectsFromHdd.ts
|
|
3312
|
+
// src/handlers/handleListProjects.ts
|
|
3368
3313
|
var import_fs8 = require("fs");
|
|
3369
3314
|
var import_os5 = require("os");
|
|
3370
3315
|
var import_path9 = require("path");
|
|
3371
|
-
|
|
3372
|
-
|
|
3373
|
-
|
|
3374
|
-
|
|
3375
|
-
|
|
3316
|
+
function decodeProjectPath(dirName) {
|
|
3317
|
+
return dirName.replace(/-/g, "/");
|
|
3318
|
+
}
|
|
3319
|
+
function handleListProjects(url, res) {
|
|
3320
|
+
const limit = Math.max(1, parseInt(url.searchParams.get("limit") ?? "50", 10) || 50);
|
|
3321
|
+
const offset = Math.max(0, parseInt(url.searchParams.get("offset") ?? "0", 10) || 0);
|
|
3322
|
+
const projectsDir = (0, import_path9.join)((0, import_os5.homedir)(), ".claude", "projects");
|
|
3323
|
+
let entries;
|
|
3376
3324
|
try {
|
|
3377
|
-
|
|
3325
|
+
entries = (0, import_fs8.readdirSync)(projectsDir).map((dirName) => {
|
|
3326
|
+
const fullPath = (0, import_path9.join)(projectsDir, dirName);
|
|
3327
|
+
let mtime = 0;
|
|
3328
|
+
try {
|
|
3329
|
+
mtime = (0, import_fs8.statSync)(fullPath).mtimeMs;
|
|
3330
|
+
} catch {
|
|
3331
|
+
}
|
|
3332
|
+
const path = decodeProjectPath(dirName);
|
|
3333
|
+
const name = path.split("/").filter(Boolean).pop() ?? dirName;
|
|
3334
|
+
return { name, path, dirName, mtime };
|
|
3335
|
+
}).sort((a, b) => b.mtime - a.mtime);
|
|
3378
3336
|
} catch {
|
|
3379
|
-
|
|
3380
|
-
|
|
3381
|
-
const lastIndexedIso = getCacheMetadata(cacheMetadataRepo, "conversations_last_indexed_at");
|
|
3382
|
-
if (!lastIndexedIso) return true;
|
|
3383
|
-
const lastIndexedMs = Date.parse(lastIndexedIso);
|
|
3384
|
-
if (Number.isNaN(lastIndexedMs)) return true;
|
|
3385
|
-
return dirMtimeMs > lastIndexedMs;
|
|
3386
|
-
}
|
|
3387
|
-
|
|
3388
|
-
// src/services/sessions/ensureSessionProjectIdsFromExistingProjects.ts
|
|
3389
|
-
function ensureSessionProjectIdsFromExistingProjects(projectsRepo, sessionsRepo) {
|
|
3390
|
-
let linked = 0;
|
|
3391
|
-
let missing = 0;
|
|
3392
|
-
for (const session of sessionsRepo.listManagedSessions()) {
|
|
3393
|
-
if (session.projectId) continue;
|
|
3394
|
-
const canonical = canonicalizeProjectPath(session.projectPath);
|
|
3395
|
-
if (!canonical) continue;
|
|
3396
|
-
const project = projectsRepo.getProjectByPath(canonical);
|
|
3397
|
-
if (!project) {
|
|
3398
|
-
missing += 1;
|
|
3399
|
-
continue;
|
|
3400
|
-
}
|
|
3401
|
-
sessionsRepo.updateSessionProjectId({ sessionId: session.id, projectId: project.id });
|
|
3402
|
-
linked += 1;
|
|
3403
|
-
}
|
|
3404
|
-
return { linked, missing };
|
|
3405
|
-
}
|
|
3406
|
-
|
|
3407
|
-
// src/services/projectChats/sortProjectChats.ts
|
|
3408
|
-
function sortProjectChats(a, b) {
|
|
3409
|
-
const byLatest = compareIsoDesc(a.latestMessageAt, b.latestMessageAt);
|
|
3410
|
-
if (byLatest !== 0) return byLatest;
|
|
3411
|
-
const byUpdated = compareIsoDesc(a.updatedAt ?? null, b.updatedAt ?? null);
|
|
3412
|
-
if (byUpdated !== 0) return byUpdated;
|
|
3413
|
-
const byCreated = compareIsoDesc(a.createdAt ?? null, b.createdAt ?? null);
|
|
3414
|
-
if (byCreated !== 0) return byCreated;
|
|
3415
|
-
return a.title.localeCompare(b.title);
|
|
3416
|
-
}
|
|
3417
|
-
|
|
3418
|
-
// src/services/projectChats/mergeProjectChats.ts
|
|
3419
|
-
function mergeProjectChats(args) {
|
|
3420
|
-
const { sessions, conversations } = args;
|
|
3421
|
-
const resumedConversationIds = new Set(
|
|
3422
|
-
sessions.filter((c) => c.type === "session").map((c) => c.resumedFromConversationId).filter((id) => Boolean(id))
|
|
3423
|
-
);
|
|
3424
|
-
const visibleConversations = conversations.filter((c) => {
|
|
3425
|
-
if (c.type !== "conversation") return true;
|
|
3426
|
-
return !resumedConversationIds.has(c.id);
|
|
3427
|
-
});
|
|
3428
|
-
return [...sessions, ...visibleConversations].sort(sortProjectChats);
|
|
3429
|
-
}
|
|
3430
|
-
|
|
3431
|
-
// src/services/projectChats/deriveProjectChatTitle.ts
|
|
3432
|
-
function deriveProjectChatTitle(input) {
|
|
3433
|
-
const trimmed = input.title?.trim();
|
|
3434
|
-
if (trimmed) return trimmed;
|
|
3435
|
-
const name = input.projectName?.trim();
|
|
3436
|
-
if (name) return name;
|
|
3437
|
-
const pathSuffix = input.projectPath ? input.projectPath.split(/[/\\]/).filter(Boolean).slice(-2).join("/") : "";
|
|
3438
|
-
if (pathSuffix) return pathSuffix;
|
|
3439
|
-
return `Untitled \xB7 ${input.id.slice(0, 8)}`;
|
|
3440
|
-
}
|
|
3441
|
-
|
|
3442
|
-
// src/services/projectChats/normalizeConversationToProjectChat.ts
|
|
3443
|
-
function normalizeConversationToProjectChat(conversation) {
|
|
3444
|
-
if (!conversation.projectId) {
|
|
3445
|
-
throw new Error(
|
|
3446
|
-
`normalizeConversationToProjectChat: conversation ${conversation.id} has no projectId \u2014 resolve it before normalizing`
|
|
3447
|
-
);
|
|
3448
|
-
}
|
|
3449
|
-
return {
|
|
3450
|
-
type: "conversation",
|
|
3451
|
-
id: conversation.id,
|
|
3452
|
-
projectId: conversation.projectId,
|
|
3453
|
-
projectPath: conversation.projectPath ?? null,
|
|
3454
|
-
title: deriveProjectChatTitle({
|
|
3455
|
-
title: conversation.title,
|
|
3456
|
-
projectName: conversation.projectName,
|
|
3457
|
-
projectPath: conversation.projectPath,
|
|
3458
|
-
id: conversation.id
|
|
3459
|
-
}),
|
|
3460
|
-
latestMessageAt: conversation.lastActivity ?? null,
|
|
3461
|
-
updatedAt: conversation.lastActivity ?? null,
|
|
3462
|
-
createdAt: null,
|
|
3463
|
-
status: "resumable",
|
|
3464
|
-
source: "hdd-cache",
|
|
3465
|
-
provider: conversation.provider ?? CLAUDE_CODE_PROVIDER,
|
|
3466
|
-
indexedAt: null,
|
|
3467
|
-
fileMtime: null,
|
|
3468
|
-
filePath: conversation.filePath ?? null,
|
|
3469
|
-
sourceHash: null
|
|
3470
|
-
};
|
|
3471
|
-
}
|
|
3472
|
-
|
|
3473
|
-
// src/services/projectChats/normalizeSessionToProjectChat.ts
|
|
3474
|
-
function normalizeSessionToProjectChat(session) {
|
|
3475
|
-
if (!session.projectId) {
|
|
3476
|
-
throw new Error(
|
|
3477
|
-
`normalizeSessionToProjectChat: session ${session.id} has no projectId \u2014 resolve it before normalizing`
|
|
3478
|
-
);
|
|
3479
|
-
}
|
|
3480
|
-
return {
|
|
3481
|
-
type: "session",
|
|
3482
|
-
id: session.id,
|
|
3483
|
-
projectId: session.projectId,
|
|
3484
|
-
projectPath: session.projectPath ?? null,
|
|
3485
|
-
title: deriveProjectChatTitle({
|
|
3486
|
-
title: session.sessionName,
|
|
3487
|
-
projectName: session.projectName,
|
|
3488
|
-
projectPath: session.projectPath,
|
|
3489
|
-
id: session.id
|
|
3490
|
-
}),
|
|
3491
|
-
latestMessageAt: session.lastMessageAt ?? session.lastActivityAt ?? null,
|
|
3492
|
-
updatedAt: session.lastActivityAt ?? null,
|
|
3493
|
-
createdAt: session.startedAt ?? null,
|
|
3494
|
-
status: "active",
|
|
3495
|
-
source: "session-store",
|
|
3496
|
-
resumedFromConversationId: session.resumedFromConversationId ?? null
|
|
3497
|
-
};
|
|
3498
|
-
}
|
|
3499
|
-
|
|
3500
|
-
// src/services/projectChats/listProjectChats.ts
|
|
3501
|
-
async function listProjectChats(deps, args) {
|
|
3502
|
-
const {
|
|
3503
|
-
cache,
|
|
3504
|
-
projectsRepo,
|
|
3505
|
-
conversationsRepo,
|
|
3506
|
-
sessionsRepo,
|
|
3507
|
-
cacheMetadataRepo,
|
|
3508
|
-
getSessionResponses,
|
|
3509
|
-
getFreshScanner,
|
|
3510
|
-
projectsDir
|
|
3511
|
-
} = deps;
|
|
3512
|
-
const needsRefresh = args.refreshConversations || shouldRefreshProjectsFromHdd(conversationsRepo, cacheMetadataRepo, { projectsDir });
|
|
3513
|
-
if (needsRefresh) {
|
|
3514
|
-
const scanner = await getFreshScanner();
|
|
3515
|
-
const metas = [...scanner.getMetadataCache().values()];
|
|
3516
|
-
if (metas.length > 0) {
|
|
3517
|
-
cache.upsertFromScannerMeta(metas);
|
|
3518
|
-
}
|
|
3519
|
-
refreshConversationCache({ cache, projectsRepo, conversationsRepo, cacheMetadataRepo });
|
|
3520
|
-
}
|
|
3521
|
-
ensureSessionProjectIdsFromExistingProjects(projectsRepo, sessionsRepo);
|
|
3522
|
-
const sessionResponses = getSessionResponses();
|
|
3523
|
-
const sessionChats = [];
|
|
3524
|
-
for (const s of sessionResponses) {
|
|
3525
|
-
if (!s.projectId) continue;
|
|
3526
|
-
sessionChats.push(normalizeSessionToProjectChat(s));
|
|
3527
|
-
}
|
|
3528
|
-
const conversationChats = [];
|
|
3529
|
-
const { conversations } = cache.listConversations({ limit: 1e3, offset: 0 });
|
|
3530
|
-
for (const c of conversations) {
|
|
3531
|
-
if (!c.projectId) continue;
|
|
3532
|
-
conversationChats.push(normalizeConversationToProjectChat(c));
|
|
3533
|
-
}
|
|
3534
|
-
return mergeProjectChats({ sessions: sessionChats, conversations: conversationChats });
|
|
3535
|
-
}
|
|
3536
|
-
|
|
3537
|
-
// src/handlers/handleListProjectChats.ts
|
|
3538
|
-
async function handleListProjectChats(url, res, deps) {
|
|
3539
|
-
const queryObj = Object.fromEntries(url.searchParams.entries());
|
|
3540
|
-
const parsed = ListProjectChatsQuerySchema.safeParse(queryObj);
|
|
3541
|
-
if (!parsed.success) {
|
|
3542
|
-
res.writeHead(400, { "Content-Type": "application/json" });
|
|
3543
|
-
res.end(
|
|
3544
|
-
JSON.stringify({
|
|
3545
|
-
error: "Invalid query parameters",
|
|
3546
|
-
details: parsed.error.flatten()
|
|
3547
|
-
})
|
|
3548
|
-
);
|
|
3337
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
3338
|
+
res.end(JSON.stringify({ projects: [], total: 0 }));
|
|
3549
3339
|
return;
|
|
3550
3340
|
}
|
|
3551
|
-
const
|
|
3552
|
-
const
|
|
3341
|
+
const total = entries.length;
|
|
3342
|
+
const page = entries.slice(offset, offset + limit).map(({ name, path, dirName }) => ({ name, path, dirName }));
|
|
3553
3343
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
3554
|
-
res.end(JSON.stringify({
|
|
3344
|
+
res.end(JSON.stringify({ projects: page, total }));
|
|
3555
3345
|
}
|
|
3556
3346
|
|
|
3557
3347
|
// src/pair-store.ts
|
|
@@ -3780,6 +3570,17 @@ function pruneAgentConversations(cache) {
|
|
|
3780
3570
|
return { scanned: rows.length, pruned, missing };
|
|
3781
3571
|
}
|
|
3782
3572
|
|
|
3573
|
+
// src/services/projectChats/deriveProjectChatTitle.ts
|
|
3574
|
+
function deriveProjectChatTitle(input) {
|
|
3575
|
+
const trimmed = input.title?.trim();
|
|
3576
|
+
if (trimmed) return trimmed;
|
|
3577
|
+
const name = input.projectName?.trim();
|
|
3578
|
+
if (name) return name;
|
|
3579
|
+
const pathSuffix = input.projectPath ? input.projectPath.split(/[/\\]/).filter(Boolean).slice(-2).join("/") : "";
|
|
3580
|
+
if (pathSuffix) return pathSuffix;
|
|
3581
|
+
return `Untitled \xB7 ${input.id.slice(0, 8)}`;
|
|
3582
|
+
}
|
|
3583
|
+
|
|
3783
3584
|
// src/services/questions/detectAskUserQuestion.ts
|
|
3784
3585
|
function normalizeContent2(raw) {
|
|
3785
3586
|
if (Array.isArray(raw)) return raw;
|
|
@@ -4183,6 +3984,14 @@ function debounce(fn, waitMs) {
|
|
|
4183
3984
|
return debounced;
|
|
4184
3985
|
}
|
|
4185
3986
|
|
|
3987
|
+
// src/utils/dates.ts
|
|
3988
|
+
var import_date_fns = require("date-fns");
|
|
3989
|
+
function parseIsoDateOrNull(value) {
|
|
3990
|
+
if (!value) return null;
|
|
3991
|
+
const parsed = (0, import_date_fns.parseISO)(value);
|
|
3992
|
+
return (0, import_date_fns.isValid)(parsed) ? parsed : null;
|
|
3993
|
+
}
|
|
3994
|
+
|
|
4186
3995
|
// src/utils/isScannedSnapshotStale.ts
|
|
4187
3996
|
var STALENESS_TOLERANCE_MS = 1e3;
|
|
4188
3997
|
function isScannedSnapshotStale(snapshotTimestamp, fileMtimeMs) {
|
|
@@ -4358,6 +4167,7 @@ var StreamerServer = class {
|
|
|
4358
4167
|
binding = false;
|
|
4359
4168
|
cacheReady = false;
|
|
4360
4169
|
apiKey;
|
|
4170
|
+
apiKeySource;
|
|
4361
4171
|
localNoAuth;
|
|
4362
4172
|
logMenubarRequests;
|
|
4363
4173
|
verbose;
|
|
@@ -4369,6 +4179,8 @@ var StreamerServer = class {
|
|
|
4369
4179
|
publicUrl = null;
|
|
4370
4180
|
pairTokens = new PairTokenStore();
|
|
4371
4181
|
exchangeAttempts = /* @__PURE__ */ new Map();
|
|
4182
|
+
sessionStartAttempts = /* @__PURE__ */ new Map();
|
|
4183
|
+
sessionInputAttempts = /* @__PURE__ */ new Map();
|
|
4372
4184
|
ptyGracePeriodMs;
|
|
4373
4185
|
defaultSystemPrompt;
|
|
4374
4186
|
// Map of sessionId → grace timer; fires to kill PTY after WS disconnect
|
|
@@ -4404,8 +4216,14 @@ var StreamerServer = class {
|
|
|
4404
4216
|
constructor(config) {
|
|
4405
4217
|
this.sessionStatusBus.setMaxListeners(0);
|
|
4406
4218
|
this.apiKey = config.apiKey;
|
|
4219
|
+
this.apiKeySource = config.apiKeySource ?? "config";
|
|
4407
4220
|
this.localNoAuth = config.localNoAuth ?? false;
|
|
4408
4221
|
this.logMenubarRequests = config.logMenubarRequests ?? false;
|
|
4222
|
+
if (this.localNoAuth) {
|
|
4223
|
+
console.warn(
|
|
4224
|
+
"[WARN] localNoAuth is ENABLED \u2014 all requests from localhost bypass authentication. Do not run with --local-no-auth in shared or production environments."
|
|
4225
|
+
);
|
|
4226
|
+
}
|
|
4409
4227
|
this.verbose = config.verbose ?? false;
|
|
4410
4228
|
this.disableDb = config.disableDb ?? false;
|
|
4411
4229
|
this.scanProfiles = config.scanProfiles;
|
|
@@ -4583,10 +4401,15 @@ var StreamerServer = class {
|
|
|
4583
4401
|
});
|
|
4584
4402
|
}
|
|
4585
4403
|
const agentClient = this.agentClient;
|
|
4404
|
+
const self = this;
|
|
4586
4405
|
const apiDeps = {
|
|
4587
|
-
|
|
4406
|
+
// ponytail: getter so rotateApiKey() takes effect without restarting the server
|
|
4407
|
+
get apiKey() {
|
|
4408
|
+
return self.apiKey;
|
|
4409
|
+
},
|
|
4588
4410
|
localNoAuth: this.localNoAuth,
|
|
4589
4411
|
logMenubarRequests: this.logMenubarRequests,
|
|
4412
|
+
rotateApiKey: () => this.rotateApiKey(),
|
|
4590
4413
|
publicUrl: this.publicUrl,
|
|
4591
4414
|
browseRoot: this.browseRoot,
|
|
4592
4415
|
ptyManager: this.ptyManager,
|
|
@@ -4617,8 +4440,8 @@ var StreamerServer = class {
|
|
|
4617
4440
|
handleConversationsCount: (url, res) => this.handleConversationsCount(url, res),
|
|
4618
4441
|
handleGetConversation: (id, url, res, ifNoneMatch) => this.handleGetConversation(id, url, res, ifNoneMatch),
|
|
4619
4442
|
handleSearch: (url, res) => this.handleSearch(url, res),
|
|
4443
|
+
handleListProjects: (url, res) => handleListProjects(url, res),
|
|
4620
4444
|
handleGetPopularProjects: (url, res) => this.handleGetPopularProjects(url, res),
|
|
4621
|
-
handleListProjectChats: (url, res) => this.handleListProjectChats(url, res),
|
|
4622
4445
|
handlePairStart: (res) => this.handlePairStart(res),
|
|
4623
4446
|
handlePairExchange: (req, res) => this.handlePairExchange(req, res),
|
|
4624
4447
|
handleBrowse: (url, res) => this.handleBrowse(url, res),
|
|
@@ -5053,19 +4876,38 @@ var StreamerServer = class {
|
|
|
5053
4876
|
machineName: hostname2()
|
|
5054
4877
|
});
|
|
5055
4878
|
}
|
|
5056
|
-
|
|
4879
|
+
rotateApiKey() {
|
|
4880
|
+
const newKey = generateApiKey();
|
|
4881
|
+
const persisted = this.apiKeySource === "config";
|
|
4882
|
+
if (persisted) setApiKey(newKey);
|
|
4883
|
+
this.apiKey = newKey;
|
|
4884
|
+
return { newKey, persisted };
|
|
4885
|
+
}
|
|
4886
|
+
checkRateLimit(map, key, limit, windowMs) {
|
|
5057
4887
|
const now = Date.now();
|
|
5058
|
-
const
|
|
5059
|
-
const limit = 5;
|
|
5060
|
-
const arr = (this.exchangeAttempts.get(ip) ?? []).filter((t) => now - t < windowMs);
|
|
4888
|
+
const arr = (map.get(key) ?? []).filter((t) => now - t < windowMs);
|
|
5061
4889
|
if (arr.length >= limit) {
|
|
5062
|
-
|
|
4890
|
+
map.set(key, arr);
|
|
5063
4891
|
return false;
|
|
5064
4892
|
}
|
|
5065
4893
|
arr.push(now);
|
|
5066
|
-
|
|
4894
|
+
map.set(key, arr);
|
|
4895
|
+
setTimeout(() => {
|
|
4896
|
+
const remaining = (map.get(key) ?? []).filter((t) => Date.now() - t < windowMs);
|
|
4897
|
+
if (remaining.length === 0) map.delete(key);
|
|
4898
|
+
else map.set(key, remaining);
|
|
4899
|
+
}, windowMs);
|
|
5067
4900
|
return true;
|
|
5068
4901
|
}
|
|
4902
|
+
checkExchangeRateLimit(ip) {
|
|
4903
|
+
return this.checkRateLimit(this.exchangeAttempts, ip, 5, 6e4);
|
|
4904
|
+
}
|
|
4905
|
+
checkSessionStartRateLimit(ip) {
|
|
4906
|
+
return this.checkRateLimit(this.sessionStartAttempts, ip, 10, 6e4);
|
|
4907
|
+
}
|
|
4908
|
+
checkSessionInputRateLimit(sessionId) {
|
|
4909
|
+
return this.checkRateLimit(this.sessionInputAttempts, sessionId, 500, 6e4);
|
|
4910
|
+
}
|
|
5069
4911
|
async handleListConversations(url, res) {
|
|
5070
4912
|
const limit = intParam(url, "limit", 50);
|
|
5071
4913
|
const offset = intParam(url, "offset", 0);
|
|
@@ -5227,21 +5069,6 @@ var StreamerServer = class {
|
|
|
5227
5069
|
const projects = this.cache.getPopularProjects(limit);
|
|
5228
5070
|
json(res, 200, { projects, total: projects.length });
|
|
5229
5071
|
}
|
|
5230
|
-
async handleListProjectChats(url, res) {
|
|
5231
|
-
if (!this.cache || !this.projectsRepo || !this.conversationsRepo || !this.sessionsRepo || !this.cacheMetadataRepo) {
|
|
5232
|
-
json(res, 503, { error: "Cache not available" });
|
|
5233
|
-
return;
|
|
5234
|
-
}
|
|
5235
|
-
await handleListProjectChats(url, res, {
|
|
5236
|
-
cache: this.cache,
|
|
5237
|
-
projectsRepo: this.projectsRepo,
|
|
5238
|
-
conversationsRepo: this.conversationsRepo,
|
|
5239
|
-
sessionsRepo: this.sessionsRepo,
|
|
5240
|
-
cacheMetadataRepo: this.cacheMetadataRepo,
|
|
5241
|
-
getSessionResponses: () => this.sessionStore.list(this.ptyAttachedIds()),
|
|
5242
|
-
getFreshScanner: () => this.getFreshScanner()
|
|
5243
|
-
});
|
|
5244
|
-
}
|
|
5245
5072
|
buildStatCache(previousScanner) {
|
|
5246
5073
|
if (!this.cache) return void 0;
|
|
5247
5074
|
const dbStats = this.cache.getFileStats();
|
|
@@ -5718,6 +5545,10 @@ var StreamerServer = class {
|
|
|
5718
5545
|
}
|
|
5719
5546
|
}
|
|
5720
5547
|
async handleSendInput(sessionId, req, res) {
|
|
5548
|
+
if (!this.checkSessionInputRateLimit(sessionId)) {
|
|
5549
|
+
json(res, 429, { error: "Too many input requests for this session. Please slow down." });
|
|
5550
|
+
return;
|
|
5551
|
+
}
|
|
5721
5552
|
if (this.agentConfig.enabled) {
|
|
5722
5553
|
const body2 = await readBody(req);
|
|
5723
5554
|
const cache = this.cache;
|
|
@@ -5999,6 +5830,13 @@ var StreamerServer = class {
|
|
|
5999
5830
|
json(res, 201, { sessionId: session.id });
|
|
6000
5831
|
}
|
|
6001
5832
|
async handleStartSession(req, res) {
|
|
5833
|
+
const ip = req.socket?.remoteAddress ?? "unknown";
|
|
5834
|
+
if (!this.checkSessionStartRateLimit(ip)) {
|
|
5835
|
+
json(res, 429, {
|
|
5836
|
+
error: "Too many session start requests. Please wait before trying again."
|
|
5837
|
+
});
|
|
5838
|
+
return;
|
|
5839
|
+
}
|
|
6002
5840
|
if (this.agentConfig.enabled) {
|
|
6003
5841
|
const body2 = await readBody(req);
|
|
6004
5842
|
const result = await handleStartAgentSession(body2, {
|