@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.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) {
|
|
@@ -4285,6 +4094,7 @@ var WSHub = class {
|
|
|
4285
4094
|
|
|
4286
4095
|
// src/server.ts
|
|
4287
4096
|
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.`;
|
|
4097
|
+
var DEFAULT_SYSTEM_PROMPT = "When presenting options or choices to the user, limit the options to at most 3.";
|
|
4288
4098
|
var DEFAULT_PTY_GRACE_PERIOD_MS = 27e4;
|
|
4289
4099
|
function parseIncludeAgentsEnv(raw) {
|
|
4290
4100
|
if (raw === void 0) return false;
|
|
@@ -4323,6 +4133,7 @@ var StreamerServer = class {
|
|
|
4323
4133
|
binding = false;
|
|
4324
4134
|
cacheReady = false;
|
|
4325
4135
|
apiKey;
|
|
4136
|
+
apiKeySource;
|
|
4326
4137
|
localNoAuth;
|
|
4327
4138
|
logMenubarRequests;
|
|
4328
4139
|
verbose;
|
|
@@ -4334,7 +4145,10 @@ var StreamerServer = class {
|
|
|
4334
4145
|
publicUrl = null;
|
|
4335
4146
|
pairTokens = new PairTokenStore();
|
|
4336
4147
|
exchangeAttempts = /* @__PURE__ */ new Map();
|
|
4148
|
+
sessionStartAttempts = /* @__PURE__ */ new Map();
|
|
4149
|
+
sessionInputAttempts = /* @__PURE__ */ new Map();
|
|
4337
4150
|
ptyGracePeriodMs;
|
|
4151
|
+
defaultSystemPrompt;
|
|
4338
4152
|
// Map of sessionId → grace timer; fires to kill PTY after WS disconnect
|
|
4339
4153
|
ptyGraceTimers = /* @__PURE__ */ new Map();
|
|
4340
4154
|
// Map of sessionId → set of subscribed WS clients
|
|
@@ -4368,13 +4182,20 @@ var StreamerServer = class {
|
|
|
4368
4182
|
constructor(config) {
|
|
4369
4183
|
this.sessionStatusBus.setMaxListeners(0);
|
|
4370
4184
|
this.apiKey = config.apiKey;
|
|
4185
|
+
this.apiKeySource = config.apiKeySource ?? "config";
|
|
4371
4186
|
this.localNoAuth = config.localNoAuth ?? false;
|
|
4372
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
|
+
}
|
|
4373
4193
|
this.verbose = config.verbose ?? false;
|
|
4374
4194
|
this.disableDb = config.disableDb ?? false;
|
|
4375
4195
|
this.scanProfiles = config.scanProfiles;
|
|
4376
4196
|
this.codexRoots = config.codexRoots ?? [join12(homedir5(), ".codex", "sessions")];
|
|
4377
4197
|
this.ptyGracePeriodMs = config.ptyGracePeriodMs ?? DEFAULT_PTY_GRACE_PERIOD_MS;
|
|
4198
|
+
this.defaultSystemPrompt = config.defaultSystemPrompt ?? DEFAULT_SYSTEM_PROMPT;
|
|
4378
4199
|
this.cacheDir = config.cacheDir ?? loadCacheDir() ?? join12(homedir5(), ".threadbase", "cache");
|
|
4379
4200
|
this.tailSize = config.tailSize ?? loadTailSize() ?? 10;
|
|
4380
4201
|
this.directoryDebounceMs = parseDirScanDebounceEnv(process.env.THREADBASE_DIR_SCAN_DEBOUNCE_MS) ?? config.directoryScanDebounceMs ?? 1e3;
|
|
@@ -4546,10 +4367,15 @@ var StreamerServer = class {
|
|
|
4546
4367
|
});
|
|
4547
4368
|
}
|
|
4548
4369
|
const agentClient = this.agentClient;
|
|
4370
|
+
const self = this;
|
|
4549
4371
|
const apiDeps = {
|
|
4550
|
-
|
|
4372
|
+
// ponytail: getter so rotateApiKey() takes effect without restarting the server
|
|
4373
|
+
get apiKey() {
|
|
4374
|
+
return self.apiKey;
|
|
4375
|
+
},
|
|
4551
4376
|
localNoAuth: this.localNoAuth,
|
|
4552
4377
|
logMenubarRequests: this.logMenubarRequests,
|
|
4378
|
+
rotateApiKey: () => this.rotateApiKey(),
|
|
4553
4379
|
publicUrl: this.publicUrl,
|
|
4554
4380
|
browseRoot: this.browseRoot,
|
|
4555
4381
|
ptyManager: this.ptyManager,
|
|
@@ -4580,8 +4406,8 @@ var StreamerServer = class {
|
|
|
4580
4406
|
handleConversationsCount: (url, res) => this.handleConversationsCount(url, res),
|
|
4581
4407
|
handleGetConversation: (id, url, res, ifNoneMatch) => this.handleGetConversation(id, url, res, ifNoneMatch),
|
|
4582
4408
|
handleSearch: (url, res) => this.handleSearch(url, res),
|
|
4409
|
+
handleListProjects: (url, res) => handleListProjects(url, res),
|
|
4583
4410
|
handleGetPopularProjects: (url, res) => this.handleGetPopularProjects(url, res),
|
|
4584
|
-
handleListProjectChats: (url, res) => this.handleListProjectChats(url, res),
|
|
4585
4411
|
handlePairStart: (res) => this.handlePairStart(res),
|
|
4586
4412
|
handlePairExchange: (req, res) => this.handlePairExchange(req, res),
|
|
4587
4413
|
handleBrowse: (url, res) => this.handleBrowse(url, res),
|
|
@@ -5016,19 +4842,38 @@ var StreamerServer = class {
|
|
|
5016
4842
|
machineName: hostname2()
|
|
5017
4843
|
});
|
|
5018
4844
|
}
|
|
5019
|
-
|
|
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) {
|
|
5020
4853
|
const now = Date.now();
|
|
5021
|
-
const
|
|
5022
|
-
const limit = 5;
|
|
5023
|
-
const arr = (this.exchangeAttempts.get(ip) ?? []).filter((t) => now - t < windowMs);
|
|
4854
|
+
const arr = (map.get(key) ?? []).filter((t) => now - t < windowMs);
|
|
5024
4855
|
if (arr.length >= limit) {
|
|
5025
|
-
|
|
4856
|
+
map.set(key, arr);
|
|
5026
4857
|
return false;
|
|
5027
4858
|
}
|
|
5028
4859
|
arr.push(now);
|
|
5029
|
-
|
|
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);
|
|
5030
4866
|
return true;
|
|
5031
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
|
+
}
|
|
5032
4877
|
async handleListConversations(url, res) {
|
|
5033
4878
|
const limit = intParam(url, "limit", 50);
|
|
5034
4879
|
const offset = intParam(url, "offset", 0);
|
|
@@ -5190,21 +5035,6 @@ var StreamerServer = class {
|
|
|
5190
5035
|
const projects = this.cache.getPopularProjects(limit);
|
|
5191
5036
|
json(res, 200, { projects, total: projects.length });
|
|
5192
5037
|
}
|
|
5193
|
-
async handleListProjectChats(url, res) {
|
|
5194
|
-
if (!this.cache || !this.projectsRepo || !this.conversationsRepo || !this.sessionsRepo || !this.cacheMetadataRepo) {
|
|
5195
|
-
json(res, 503, { error: "Cache not available" });
|
|
5196
|
-
return;
|
|
5197
|
-
}
|
|
5198
|
-
await handleListProjectChats(url, res, {
|
|
5199
|
-
cache: this.cache,
|
|
5200
|
-
projectsRepo: this.projectsRepo,
|
|
5201
|
-
conversationsRepo: this.conversationsRepo,
|
|
5202
|
-
sessionsRepo: this.sessionsRepo,
|
|
5203
|
-
cacheMetadataRepo: this.cacheMetadataRepo,
|
|
5204
|
-
getSessionResponses: () => this.sessionStore.list(this.ptyAttachedIds()),
|
|
5205
|
-
getFreshScanner: () => this.getFreshScanner()
|
|
5206
|
-
});
|
|
5207
|
-
}
|
|
5208
5038
|
buildStatCache(previousScanner) {
|
|
5209
5039
|
if (!this.cache) return void 0;
|
|
5210
5040
|
const dbStats = this.cache.getFileStats();
|
|
@@ -5273,12 +5103,12 @@ var StreamerServer = class {
|
|
|
5273
5103
|
const projectsDir = join12(homedir5(), ".claude", "projects");
|
|
5274
5104
|
if (!existsSync6(projectsDir)) return null;
|
|
5275
5105
|
const filename = `${uuid}.jsonl`;
|
|
5276
|
-
for (const dir of
|
|
5106
|
+
for (const dir of readdirSync4(projectsDir)) {
|
|
5277
5107
|
const fp = join12(projectsDir, dir, filename);
|
|
5278
5108
|
if (existsSync6(fp)) return fp;
|
|
5279
5109
|
const projectDir = join12(projectsDir, dir);
|
|
5280
5110
|
try {
|
|
5281
|
-
for (const sub of
|
|
5111
|
+
for (const sub of readdirSync4(projectDir)) {
|
|
5282
5112
|
const subagentPath = join12(projectDir, sub, "subagents", filename);
|
|
5283
5113
|
if (existsSync6(subagentPath)) return subagentPath;
|
|
5284
5114
|
}
|
|
@@ -5681,6 +5511,10 @@ var StreamerServer = class {
|
|
|
5681
5511
|
}
|
|
5682
5512
|
}
|
|
5683
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
|
+
}
|
|
5684
5518
|
if (this.agentConfig.enabled) {
|
|
5685
5519
|
const body2 = await readBody(req);
|
|
5686
5520
|
const cache = this.cache;
|
|
@@ -5962,6 +5796,13 @@ var StreamerServer = class {
|
|
|
5962
5796
|
json(res, 201, { sessionId: session.id });
|
|
5963
5797
|
}
|
|
5964
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
|
+
}
|
|
5965
5806
|
if (this.agentConfig.enabled) {
|
|
5966
5807
|
const body2 = await readBody(req);
|
|
5967
5808
|
const result = await handleStartAgentSession(body2, {
|
|
@@ -5985,7 +5826,7 @@ var StreamerServer = class {
|
|
|
5985
5826
|
return;
|
|
5986
5827
|
}
|
|
5987
5828
|
const body = await readBody(req);
|
|
5988
|
-
const { path: relativePath } = body;
|
|
5829
|
+
const { path: relativePath, systemPrompt: clientPrompt } = body;
|
|
5989
5830
|
if (typeof relativePath !== "string") {
|
|
5990
5831
|
json(res, 400, { error: "Missing path field" });
|
|
5991
5832
|
return;
|
|
@@ -5999,11 +5840,16 @@ var StreamerServer = class {
|
|
|
5999
5840
|
return;
|
|
6000
5841
|
}
|
|
6001
5842
|
this.discoveryCache = null;
|
|
5843
|
+
const systemPromptParts = [
|
|
5844
|
+
this.defaultSystemPrompt,
|
|
5845
|
+
BROWSE_SYSTEM_PROMPT(this.browseRoot),
|
|
5846
|
+
typeof clientPrompt === "string" ? clientPrompt : null
|
|
5847
|
+
].filter(Boolean);
|
|
6002
5848
|
try {
|
|
6003
5849
|
const session = await this.ptyManager.startFresh({
|
|
6004
5850
|
projectPath: resolvedPath,
|
|
6005
5851
|
projectName: body.projectName,
|
|
6006
|
-
systemPrompt:
|
|
5852
|
+
systemPrompt: systemPromptParts.join("\n")
|
|
6007
5853
|
});
|
|
6008
5854
|
this.sessionStore.addManaged(session);
|
|
6009
5855
|
json(res, 202, { id: session.id, status: "pending" });
|
|
@@ -6092,7 +5938,7 @@ var StreamerServer = class {
|
|
|
6092
5938
|
if (!resolvedFilePath && existsSync6(projectsDir)) {
|
|
6093
5939
|
try {
|
|
6094
5940
|
const now = Date.now();
|
|
6095
|
-
const recent =
|
|
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];
|
|
6096
5942
|
if (recent) resolvedFilePath = join12(projectsDir, recent.f);
|
|
6097
5943
|
} catch {
|
|
6098
5944
|
}
|