@threadbase-sh/streamer 1.19.1 → 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 +452 -618
- package/dist/cli.cjs.map +1 -1
- package/dist/index.cjs +164 -318
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +16 -2
- package/dist/index.d.ts +16 -2
- package/dist/index.js +169 -323
- 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) {
|
|
@@ -4319,6 +4128,7 @@ var WSHub = class {
|
|
|
4319
4128
|
|
|
4320
4129
|
// src/server.ts
|
|
4321
4130
|
var BROWSE_SYSTEM_PROMPT = (browseRoot) => `You are working within the project boundary: ${browseRoot}. Do not read, write, or execute commands that access files or directories outside this boundary.`;
|
|
4131
|
+
var DEFAULT_SYSTEM_PROMPT = "When presenting options or choices to the user, limit the options to at most 3.";
|
|
4322
4132
|
var DEFAULT_PTY_GRACE_PERIOD_MS = 27e4;
|
|
4323
4133
|
function parseIncludeAgentsEnv(raw) {
|
|
4324
4134
|
if (raw === void 0) return false;
|
|
@@ -4357,6 +4167,7 @@ var StreamerServer = class {
|
|
|
4357
4167
|
binding = false;
|
|
4358
4168
|
cacheReady = false;
|
|
4359
4169
|
apiKey;
|
|
4170
|
+
apiKeySource;
|
|
4360
4171
|
localNoAuth;
|
|
4361
4172
|
logMenubarRequests;
|
|
4362
4173
|
verbose;
|
|
@@ -4368,7 +4179,10 @@ var StreamerServer = class {
|
|
|
4368
4179
|
publicUrl = null;
|
|
4369
4180
|
pairTokens = new PairTokenStore();
|
|
4370
4181
|
exchangeAttempts = /* @__PURE__ */ new Map();
|
|
4182
|
+
sessionStartAttempts = /* @__PURE__ */ new Map();
|
|
4183
|
+
sessionInputAttempts = /* @__PURE__ */ new Map();
|
|
4371
4184
|
ptyGracePeriodMs;
|
|
4185
|
+
defaultSystemPrompt;
|
|
4372
4186
|
// Map of sessionId → grace timer; fires to kill PTY after WS disconnect
|
|
4373
4187
|
ptyGraceTimers = /* @__PURE__ */ new Map();
|
|
4374
4188
|
// Map of sessionId → set of subscribed WS clients
|
|
@@ -4402,13 +4216,20 @@ var StreamerServer = class {
|
|
|
4402
4216
|
constructor(config) {
|
|
4403
4217
|
this.sessionStatusBus.setMaxListeners(0);
|
|
4404
4218
|
this.apiKey = config.apiKey;
|
|
4219
|
+
this.apiKeySource = config.apiKeySource ?? "config";
|
|
4405
4220
|
this.localNoAuth = config.localNoAuth ?? false;
|
|
4406
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
|
+
}
|
|
4407
4227
|
this.verbose = config.verbose ?? false;
|
|
4408
4228
|
this.disableDb = config.disableDb ?? false;
|
|
4409
4229
|
this.scanProfiles = config.scanProfiles;
|
|
4410
4230
|
this.codexRoots = config.codexRoots ?? [(0, import_path11.join)((0, import_os6.homedir)(), ".codex", "sessions")];
|
|
4411
4231
|
this.ptyGracePeriodMs = config.ptyGracePeriodMs ?? DEFAULT_PTY_GRACE_PERIOD_MS;
|
|
4232
|
+
this.defaultSystemPrompt = config.defaultSystemPrompt ?? DEFAULT_SYSTEM_PROMPT;
|
|
4412
4233
|
this.cacheDir = config.cacheDir ?? loadCacheDir() ?? (0, import_path11.join)((0, import_os6.homedir)(), ".threadbase", "cache");
|
|
4413
4234
|
this.tailSize = config.tailSize ?? loadTailSize() ?? 10;
|
|
4414
4235
|
this.directoryDebounceMs = parseDirScanDebounceEnv(process.env.THREADBASE_DIR_SCAN_DEBOUNCE_MS) ?? config.directoryScanDebounceMs ?? 1e3;
|
|
@@ -4580,10 +4401,15 @@ var StreamerServer = class {
|
|
|
4580
4401
|
});
|
|
4581
4402
|
}
|
|
4582
4403
|
const agentClient = this.agentClient;
|
|
4404
|
+
const self = this;
|
|
4583
4405
|
const apiDeps = {
|
|
4584
|
-
|
|
4406
|
+
// ponytail: getter so rotateApiKey() takes effect without restarting the server
|
|
4407
|
+
get apiKey() {
|
|
4408
|
+
return self.apiKey;
|
|
4409
|
+
},
|
|
4585
4410
|
localNoAuth: this.localNoAuth,
|
|
4586
4411
|
logMenubarRequests: this.logMenubarRequests,
|
|
4412
|
+
rotateApiKey: () => this.rotateApiKey(),
|
|
4587
4413
|
publicUrl: this.publicUrl,
|
|
4588
4414
|
browseRoot: this.browseRoot,
|
|
4589
4415
|
ptyManager: this.ptyManager,
|
|
@@ -4614,8 +4440,8 @@ var StreamerServer = class {
|
|
|
4614
4440
|
handleConversationsCount: (url, res) => this.handleConversationsCount(url, res),
|
|
4615
4441
|
handleGetConversation: (id, url, res, ifNoneMatch) => this.handleGetConversation(id, url, res, ifNoneMatch),
|
|
4616
4442
|
handleSearch: (url, res) => this.handleSearch(url, res),
|
|
4443
|
+
handleListProjects: (url, res) => handleListProjects(url, res),
|
|
4617
4444
|
handleGetPopularProjects: (url, res) => this.handleGetPopularProjects(url, res),
|
|
4618
|
-
handleListProjectChats: (url, res) => this.handleListProjectChats(url, res),
|
|
4619
4445
|
handlePairStart: (res) => this.handlePairStart(res),
|
|
4620
4446
|
handlePairExchange: (req, res) => this.handlePairExchange(req, res),
|
|
4621
4447
|
handleBrowse: (url, res) => this.handleBrowse(url, res),
|
|
@@ -5050,19 +4876,38 @@ var StreamerServer = class {
|
|
|
5050
4876
|
machineName: hostname2()
|
|
5051
4877
|
});
|
|
5052
4878
|
}
|
|
5053
|
-
|
|
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) {
|
|
5054
4887
|
const now = Date.now();
|
|
5055
|
-
const
|
|
5056
|
-
const limit = 5;
|
|
5057
|
-
const arr = (this.exchangeAttempts.get(ip) ?? []).filter((t) => now - t < windowMs);
|
|
4888
|
+
const arr = (map.get(key) ?? []).filter((t) => now - t < windowMs);
|
|
5058
4889
|
if (arr.length >= limit) {
|
|
5059
|
-
|
|
4890
|
+
map.set(key, arr);
|
|
5060
4891
|
return false;
|
|
5061
4892
|
}
|
|
5062
4893
|
arr.push(now);
|
|
5063
|
-
|
|
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);
|
|
5064
4900
|
return true;
|
|
5065
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
|
+
}
|
|
5066
4911
|
async handleListConversations(url, res) {
|
|
5067
4912
|
const limit = intParam(url, "limit", 50);
|
|
5068
4913
|
const offset = intParam(url, "offset", 0);
|
|
@@ -5224,21 +5069,6 @@ var StreamerServer = class {
|
|
|
5224
5069
|
const projects = this.cache.getPopularProjects(limit);
|
|
5225
5070
|
json(res, 200, { projects, total: projects.length });
|
|
5226
5071
|
}
|
|
5227
|
-
async handleListProjectChats(url, res) {
|
|
5228
|
-
if (!this.cache || !this.projectsRepo || !this.conversationsRepo || !this.sessionsRepo || !this.cacheMetadataRepo) {
|
|
5229
|
-
json(res, 503, { error: "Cache not available" });
|
|
5230
|
-
return;
|
|
5231
|
-
}
|
|
5232
|
-
await handleListProjectChats(url, res, {
|
|
5233
|
-
cache: this.cache,
|
|
5234
|
-
projectsRepo: this.projectsRepo,
|
|
5235
|
-
conversationsRepo: this.conversationsRepo,
|
|
5236
|
-
sessionsRepo: this.sessionsRepo,
|
|
5237
|
-
cacheMetadataRepo: this.cacheMetadataRepo,
|
|
5238
|
-
getSessionResponses: () => this.sessionStore.list(this.ptyAttachedIds()),
|
|
5239
|
-
getFreshScanner: () => this.getFreshScanner()
|
|
5240
|
-
});
|
|
5241
|
-
}
|
|
5242
5072
|
buildStatCache(previousScanner) {
|
|
5243
5073
|
if (!this.cache) return void 0;
|
|
5244
5074
|
const dbStats = this.cache.getFileStats();
|
|
@@ -5715,6 +5545,10 @@ var StreamerServer = class {
|
|
|
5715
5545
|
}
|
|
5716
5546
|
}
|
|
5717
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
|
+
}
|
|
5718
5552
|
if (this.agentConfig.enabled) {
|
|
5719
5553
|
const body2 = await readBody(req);
|
|
5720
5554
|
const cache = this.cache;
|
|
@@ -5996,6 +5830,13 @@ var StreamerServer = class {
|
|
|
5996
5830
|
json(res, 201, { sessionId: session.id });
|
|
5997
5831
|
}
|
|
5998
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
|
+
}
|
|
5999
5840
|
if (this.agentConfig.enabled) {
|
|
6000
5841
|
const body2 = await readBody(req);
|
|
6001
5842
|
const result = await handleStartAgentSession(body2, {
|
|
@@ -6019,7 +5860,7 @@ var StreamerServer = class {
|
|
|
6019
5860
|
return;
|
|
6020
5861
|
}
|
|
6021
5862
|
const body = await readBody(req);
|
|
6022
|
-
const { path: relativePath } = body;
|
|
5863
|
+
const { path: relativePath, systemPrompt: clientPrompt } = body;
|
|
6023
5864
|
if (typeof relativePath !== "string") {
|
|
6024
5865
|
json(res, 400, { error: "Missing path field" });
|
|
6025
5866
|
return;
|
|
@@ -6033,11 +5874,16 @@ var StreamerServer = class {
|
|
|
6033
5874
|
return;
|
|
6034
5875
|
}
|
|
6035
5876
|
this.discoveryCache = null;
|
|
5877
|
+
const systemPromptParts = [
|
|
5878
|
+
this.defaultSystemPrompt,
|
|
5879
|
+
BROWSE_SYSTEM_PROMPT(this.browseRoot),
|
|
5880
|
+
typeof clientPrompt === "string" ? clientPrompt : null
|
|
5881
|
+
].filter(Boolean);
|
|
6036
5882
|
try {
|
|
6037
5883
|
const session = await this.ptyManager.startFresh({
|
|
6038
5884
|
projectPath: resolvedPath,
|
|
6039
5885
|
projectName: body.projectName,
|
|
6040
|
-
systemPrompt:
|
|
5886
|
+
systemPrompt: systemPromptParts.join("\n")
|
|
6041
5887
|
});
|
|
6042
5888
|
this.sessionStore.addManaged(session);
|
|
6043
5889
|
json(res, 202, { id: session.id, status: "pending" });
|