@threadbase-sh/streamer 1.36.0 → 1.36.2
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 +621 -372
- package/dist/cli.cjs.map +1 -1
- package/dist/index.cjs +328 -87
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +20 -1
- package/dist/index.d.ts +20 -1
- package/dist/index.js +313 -72
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1397,9 +1397,13 @@ import { existsSync as existsSync3 } from "fs";
|
|
|
1397
1397
|
import { basename as basename2 } from "path";
|
|
1398
1398
|
|
|
1399
1399
|
// src/services/questions/detectPermissionGate.ts
|
|
1400
|
-
var
|
|
1400
|
+
var OSC_777_PERMISSION_RE = /\x1b\]777;notify;Claude Code;[^\x07\x1b]*needs your permission/;
|
|
1401
|
+
var OSC_777_WAITING_RE = /\x1b\]777;notify;Claude Code;[^\x07\x1b]*waiting for your input/;
|
|
1401
1402
|
function hasPermissionOsc(rawData) {
|
|
1402
|
-
return
|
|
1403
|
+
return OSC_777_PERMISSION_RE.test(rawData);
|
|
1404
|
+
}
|
|
1405
|
+
function hasWaitingForInputOsc(rawData) {
|
|
1406
|
+
return OSC_777_WAITING_RE.test(rawData);
|
|
1403
1407
|
}
|
|
1404
1408
|
var OPTION_RE = /^\s*(❯)?\s*(\d+)\.\s+(.+?)\s*$/;
|
|
1405
1409
|
var FOOTER_RE = /Enter to select|Esc to cancel|↑|↓|to navigate|to cancel/i;
|
|
@@ -1550,22 +1554,29 @@ function detectShellPrompt(lines) {
|
|
|
1550
1554
|
};
|
|
1551
1555
|
}
|
|
1552
1556
|
const lastNumberedIdx = (() => {
|
|
1553
|
-
|
|
1554
|
-
|
|
1557
|
+
let i = last.idx;
|
|
1558
|
+
if (!NUMBERED_RE.test(lines[i])) {
|
|
1559
|
+
if (i > 0 && (PRESS_ENTER_RE.test(lines[i].trim()) || CONTINUE_RE.test(lines[i].trim()))) {
|
|
1560
|
+
i--;
|
|
1561
|
+
} else {
|
|
1562
|
+
return -1;
|
|
1563
|
+
}
|
|
1555
1564
|
}
|
|
1556
|
-
|
|
1565
|
+
while (i >= 0 && !NUMBERED_RE.test(lines[i]) && lines[i].trim().length === 0) i--;
|
|
1566
|
+
return i >= 0 && NUMBERED_RE.test(lines[i]) ? i : -1;
|
|
1557
1567
|
})();
|
|
1558
1568
|
if (lastNumberedIdx >= 0) {
|
|
1559
1569
|
const options = [];
|
|
1560
|
-
|
|
1570
|
+
let firstRow = lastNumberedIdx;
|
|
1571
|
+
for (let i = lastNumberedIdx; i >= 0; i--) {
|
|
1561
1572
|
const m = NUMBERED_RE.exec(lines[i]);
|
|
1562
|
-
if (!m)
|
|
1573
|
+
if (!m) break;
|
|
1563
1574
|
const num = Number.parseInt(m[1], 10);
|
|
1564
|
-
if (!Number.isFinite(num))
|
|
1565
|
-
options.
|
|
1575
|
+
if (!Number.isFinite(num)) break;
|
|
1576
|
+
options.unshift({ index: num, label: m[2].trim(), answerKeys: `${num}${ENTER}` });
|
|
1577
|
+
firstRow = i;
|
|
1566
1578
|
}
|
|
1567
1579
|
if (options.length >= 2) {
|
|
1568
|
-
const firstRow = lines.findIndex((l) => NUMBERED_RE.test(l));
|
|
1569
1580
|
let prompt = "";
|
|
1570
1581
|
for (let i = firstRow - 1; i >= 0; i--) {
|
|
1571
1582
|
const t = lines[i].trim();
|
|
@@ -2134,12 +2145,13 @@ var PTYManager = class {
|
|
|
2134
2145
|
const session = this.sessions.get(sessionId);
|
|
2135
2146
|
if (!session) return;
|
|
2136
2147
|
const oscPermission = hasPermissionOsc(rawData);
|
|
2148
|
+
const oscWaitingForInput = hasWaitingForInputOsc(rawData);
|
|
2137
2149
|
const hasAskFooter = /Enter to select/i.test(stripped);
|
|
2138
2150
|
const hasPromptMarker = CLAUDE_PROMPT_MARKERS.some((m) => stripped.includes(m));
|
|
2139
2151
|
const hasShellPromptHint = /[[(]\s*y\s*\/\s*n\s*[\])]|press\s+(enter|return|any key)|\bcontinue\b\s*\?|^\s*(?:❯|>)?\s*\d+[.)]\s+\S/im.test(
|
|
2140
2152
|
stripped
|
|
2141
2153
|
);
|
|
2142
|
-
if (!oscPermission && !hasAskFooter && !hasShellPromptHint && !this.permissionOpen.has(sessionId) && !this.shellPromptOpen.has(sessionId) && !this.lastScreenQuestionKey.has(sessionId)) {
|
|
2154
|
+
if (!oscPermission && !oscWaitingForInput && !hasAskFooter && !hasShellPromptHint && !this.permissionOpen.has(sessionId) && !this.shellPromptOpen.has(sessionId) && !this.lastScreenQuestionKey.has(sessionId)) {
|
|
2143
2155
|
return;
|
|
2144
2156
|
}
|
|
2145
2157
|
const lines = await this.getOutputLines(sessionId, 60);
|
|
@@ -2162,11 +2174,11 @@ var PTYManager = class {
|
|
|
2162
2174
|
this.onPermissionChange?.(sessionId, gate ?? { options: [] });
|
|
2163
2175
|
} else if (this.permissionOpen.has(sessionId) && !askFooterOnScreen) {
|
|
2164
2176
|
const gate = scrapePermissionGate(lines);
|
|
2165
|
-
if (gate) {
|
|
2166
|
-
this.onPermissionChange?.(sessionId, gate);
|
|
2167
|
-
} else if (hasPromptMarker) {
|
|
2177
|
+
if (oscWaitingForInput || !gate && hasPromptMarker) {
|
|
2168
2178
|
this.permissionOpen.delete(sessionId);
|
|
2169
2179
|
this.onPermissionChange?.(sessionId, null);
|
|
2180
|
+
} else if (gate) {
|
|
2181
|
+
this.onPermissionChange?.(sessionId, gate);
|
|
2170
2182
|
}
|
|
2171
2183
|
}
|
|
2172
2184
|
if (askFooterOnScreen) {
|
|
@@ -2672,14 +2684,14 @@ import {
|
|
|
2672
2684
|
createReadStream,
|
|
2673
2685
|
existsSync as existsSync10,
|
|
2674
2686
|
watch as fsWatch,
|
|
2675
|
-
readdirSync as
|
|
2687
|
+
readdirSync as readdirSync6,
|
|
2676
2688
|
readFileSync as readFileSync8,
|
|
2677
|
-
statSync as
|
|
2689
|
+
statSync as statSync9
|
|
2678
2690
|
} from "fs";
|
|
2679
2691
|
import { realpath as realpath2 } from "fs/promises";
|
|
2680
2692
|
import { createServer } from "http";
|
|
2681
|
-
import { homedir as
|
|
2682
|
-
import { basename as basename5, dirname as dirname9, join as
|
|
2693
|
+
import { homedir as homedir9 } from "os";
|
|
2694
|
+
import { basename as basename5, dirname as dirname9, join as join18 } from "path";
|
|
2683
2695
|
import { createInterface } from "readline";
|
|
2684
2696
|
|
|
2685
2697
|
// node_modules/nanoid/index.js
|
|
@@ -3592,8 +3604,8 @@ var createSessionRoutes = (deps) => {
|
|
|
3592
3604
|
await deps.handleStopSession(c.req.param("id"), c.env.outgoing);
|
|
3593
3605
|
return alreadyHandled6();
|
|
3594
3606
|
});
|
|
3595
|
-
app.get("/:id", (c) => {
|
|
3596
|
-
deps.handleGetSession(c.req.param("id"), c.env.outgoing);
|
|
3607
|
+
app.get("/:id", async (c) => {
|
|
3608
|
+
await deps.handleGetSession(c.req.param("id"), c.env.outgoing);
|
|
3597
3609
|
return alreadyHandled6();
|
|
3598
3610
|
});
|
|
3599
3611
|
return app;
|
|
@@ -5408,6 +5420,14 @@ function seal(plaintext, recipientPublicKeyBase64) {
|
|
|
5408
5420
|
};
|
|
5409
5421
|
}
|
|
5410
5422
|
|
|
5423
|
+
// src/services/cache/cacheMetadata.ts
|
|
5424
|
+
function getCacheMetadata(repo, key) {
|
|
5425
|
+
return repo.getCacheMetadata(key);
|
|
5426
|
+
}
|
|
5427
|
+
function setCacheMetadata(repo, key, value) {
|
|
5428
|
+
repo.setCacheMetadata(key, value);
|
|
5429
|
+
}
|
|
5430
|
+
|
|
5411
5431
|
// src/services/cache-integrity/cacheIntegrityMonitor.ts
|
|
5412
5432
|
import { createHash as createHash2 } from "crypto";
|
|
5413
5433
|
import { existsSync as existsSync8 } from "fs";
|
|
@@ -5948,6 +5968,140 @@ function pruneAgentConversations(cache) {
|
|
|
5948
5968
|
return { scanned: rows.length, pruned, missing };
|
|
5949
5969
|
}
|
|
5950
5970
|
|
|
5971
|
+
// src/utils/dates.ts
|
|
5972
|
+
import { compareDesc, isValid, parseISO } from "date-fns";
|
|
5973
|
+
function parseIsoDateOrNull(value) {
|
|
5974
|
+
if (!value) return null;
|
|
5975
|
+
const parsed = parseISO(value);
|
|
5976
|
+
return isValid(parsed) ? parsed : null;
|
|
5977
|
+
}
|
|
5978
|
+
function compareIsoDesc(a, b) {
|
|
5979
|
+
const dateA = parseIsoDateOrNull(a);
|
|
5980
|
+
const dateB = parseIsoDateOrNull(b);
|
|
5981
|
+
if (!dateA && !dateB) return 0;
|
|
5982
|
+
if (!dateA) return 1;
|
|
5983
|
+
if (!dateB) return -1;
|
|
5984
|
+
return compareDesc(dateA, dateB);
|
|
5985
|
+
}
|
|
5986
|
+
|
|
5987
|
+
// src/services/projects/ensureProjectsForConversations.ts
|
|
5988
|
+
function ensureProjectsForConversations(repo, conversations) {
|
|
5989
|
+
const conversationsByPath = /* @__PURE__ */ new Map();
|
|
5990
|
+
for (const conversation of conversations) {
|
|
5991
|
+
if (!conversation.projectPath) continue;
|
|
5992
|
+
const canonical = canonicalizeProjectPath(conversation.projectPath);
|
|
5993
|
+
if (!canonical) continue;
|
|
5994
|
+
const existing = conversationsByPath.get(canonical) ?? [];
|
|
5995
|
+
existing.push(conversation);
|
|
5996
|
+
conversationsByPath.set(canonical, existing);
|
|
5997
|
+
}
|
|
5998
|
+
const pathToProjectId = /* @__PURE__ */ new Map();
|
|
5999
|
+
for (const [path, projectConversations] of conversationsByPath) {
|
|
6000
|
+
const latest = pickLatestConversation(projectConversations);
|
|
6001
|
+
const project = repo.upsertProjectByPath(path, {
|
|
6002
|
+
lastConversationId: latest?.id ?? null,
|
|
6003
|
+
lastConversationCreatedAt: latest?.createdAt ?? null,
|
|
6004
|
+
latestMessageAt: latest?.latestMessageAt ?? null
|
|
6005
|
+
});
|
|
6006
|
+
pathToProjectId.set(path, project.id);
|
|
6007
|
+
}
|
|
6008
|
+
return pathToProjectId;
|
|
6009
|
+
}
|
|
6010
|
+
function pickLatestConversation(conversations) {
|
|
6011
|
+
if (conversations.length === 0) return void 0;
|
|
6012
|
+
return [...conversations].sort((a, b) => {
|
|
6013
|
+
const cmp = compareIsoDesc(a.latestMessageAt ?? null, b.latestMessageAt ?? null);
|
|
6014
|
+
if (cmp !== 0) return cmp;
|
|
6015
|
+
return compareIsoDesc(a.createdAt ?? null, b.createdAt ?? null);
|
|
6016
|
+
})[0];
|
|
6017
|
+
}
|
|
6018
|
+
|
|
6019
|
+
// src/services/conversations/refreshConversationCache.ts
|
|
6020
|
+
function refreshConversationCache(deps) {
|
|
6021
|
+
const { projectsRepo, conversationsRepo, cacheMetadataRepo } = deps;
|
|
6022
|
+
const conversations = conversationsRepo.listConversationsForProjectBackfill();
|
|
6023
|
+
const pathToProjectId = ensureProjectsForConversations(
|
|
6024
|
+
projectsRepo,
|
|
6025
|
+
conversations.map((c) => ({
|
|
6026
|
+
id: c.id,
|
|
6027
|
+
projectPath: c.projectPath,
|
|
6028
|
+
latestMessageAt: c.lastActivity ?? null,
|
|
6029
|
+
createdAt: c.lastActivity ?? null
|
|
6030
|
+
}))
|
|
6031
|
+
);
|
|
6032
|
+
let conversationsBackfilled = 0;
|
|
6033
|
+
for (const conversation of conversations) {
|
|
6034
|
+
if (!conversation.projectPath) continue;
|
|
6035
|
+
if (conversation.projectId) continue;
|
|
6036
|
+
const projectId = pathToProjectId.get(canonicalizeProjectPath(conversation.projectPath));
|
|
6037
|
+
if (!projectId) continue;
|
|
6038
|
+
conversationsRepo.updateConversationProjectId({
|
|
6039
|
+
conversationId: conversation.id,
|
|
6040
|
+
projectId
|
|
6041
|
+
});
|
|
6042
|
+
conversationsBackfilled += 1;
|
|
6043
|
+
}
|
|
6044
|
+
const latest = conversationsRepo.getLatestConversation();
|
|
6045
|
+
if (latest) {
|
|
6046
|
+
setCacheMetadata(cacheMetadataRepo, "last_conversation_id", latest.id);
|
|
6047
|
+
if (latest.lastActivity) {
|
|
6048
|
+
setCacheMetadata(cacheMetadataRepo, "last_conversation_created_at", latest.lastActivity);
|
|
6049
|
+
}
|
|
6050
|
+
}
|
|
6051
|
+
setCacheMetadata(cacheMetadataRepo, "conversations_last_indexed_at", (/* @__PURE__ */ new Date()).toISOString());
|
|
6052
|
+
return {
|
|
6053
|
+
projectsTouched: pathToProjectId.size,
|
|
6054
|
+
conversationsBackfilled,
|
|
6055
|
+
latestConversationId: latest?.id ?? null
|
|
6056
|
+
};
|
|
6057
|
+
}
|
|
6058
|
+
|
|
6059
|
+
// src/services/conversations/shouldRefreshProjectsFromHdd.ts
|
|
6060
|
+
import { readdirSync as readdirSync5, statSync as statSync7 } from "fs";
|
|
6061
|
+
import { homedir as homedir8 } from "os";
|
|
6062
|
+
import { join as join16 } from "path";
|
|
6063
|
+
var DEFAULT_PROJECTS_DIR = join16(homedir8(), ".claude", "projects");
|
|
6064
|
+
function maxProjectsTreeMtimeMs(projectsDir) {
|
|
6065
|
+
let maxMs;
|
|
6066
|
+
try {
|
|
6067
|
+
maxMs = statSync7(projectsDir).mtimeMs;
|
|
6068
|
+
} catch {
|
|
6069
|
+
return null;
|
|
6070
|
+
}
|
|
6071
|
+
try {
|
|
6072
|
+
for (const ent of readdirSync5(projectsDir, { withFileTypes: true })) {
|
|
6073
|
+
if (!ent.isDirectory()) continue;
|
|
6074
|
+
try {
|
|
6075
|
+
const childMs = statSync7(join16(projectsDir, ent.name)).mtimeMs;
|
|
6076
|
+
if (childMs > maxMs) maxMs = childMs;
|
|
6077
|
+
} catch {
|
|
6078
|
+
}
|
|
6079
|
+
}
|
|
6080
|
+
} catch {
|
|
6081
|
+
}
|
|
6082
|
+
return maxMs;
|
|
6083
|
+
}
|
|
6084
|
+
function shouldRefreshProjectsFromHdd(conversationsRepo, cacheMetadataRepo, opts = {}) {
|
|
6085
|
+
if (conversationsRepo.hasOrphanRows()) return true;
|
|
6086
|
+
const dirs = /* @__PURE__ */ new Set();
|
|
6087
|
+
if (opts.projectsDirs) {
|
|
6088
|
+
for (const d of opts.projectsDirs) dirs.add(d);
|
|
6089
|
+
}
|
|
6090
|
+
dirs.add(opts.projectsDir ?? DEFAULT_PROJECTS_DIR);
|
|
6091
|
+
let newestMs = null;
|
|
6092
|
+
for (const dir of dirs) {
|
|
6093
|
+
const ms = maxProjectsTreeMtimeMs(dir);
|
|
6094
|
+
if (ms === null) continue;
|
|
6095
|
+
if (newestMs === null || ms > newestMs) newestMs = ms;
|
|
6096
|
+
}
|
|
6097
|
+
if (newestMs === null) return false;
|
|
6098
|
+
const lastIndexedIso = getCacheMetadata(cacheMetadataRepo, "conversations_last_indexed_at");
|
|
6099
|
+
if (!lastIndexedIso) return true;
|
|
6100
|
+
const lastIndexedMs = Date.parse(lastIndexedIso);
|
|
6101
|
+
if (Number.isNaN(lastIndexedMs)) return true;
|
|
6102
|
+
return newestMs > lastIndexedMs;
|
|
6103
|
+
}
|
|
6104
|
+
|
|
5951
6105
|
// src/services/projectChats/deriveProjectChatTitle.ts
|
|
5952
6106
|
function deriveProjectChatTitle(input) {
|
|
5953
6107
|
const trimmed = input.title?.trim();
|
|
@@ -5959,6 +6113,32 @@ function deriveProjectChatTitle(input) {
|
|
|
5959
6113
|
return `Untitled \xB7 ${input.id.slice(0, 8)}`;
|
|
5960
6114
|
}
|
|
5961
6115
|
|
|
6116
|
+
// src/services/questions/parseStatusLine.ts
|
|
6117
|
+
var MODEL_RE = /(Opus|Sonnet|Haiku|Fable)\s+[\d.]+(?:\s*\([^)]*\))?/;
|
|
6118
|
+
var EFFORT_RE = /●\s*([A-Za-z]+)\s*·\s*\/effort/;
|
|
6119
|
+
var PERMISSION_MODE_RE = /⏵⏵\s*([^(·\n]+?)\s*(?:\(|·|$)/;
|
|
6120
|
+
function parseStatusLine(lines) {
|
|
6121
|
+
const info = {};
|
|
6122
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
6123
|
+
const line = lines[i];
|
|
6124
|
+
if (!line) continue;
|
|
6125
|
+
if (info.effort === void 0) {
|
|
6126
|
+
const m = EFFORT_RE.exec(line);
|
|
6127
|
+
if (m) info.effort = m[1];
|
|
6128
|
+
}
|
|
6129
|
+
if (info.permissionMode === void 0) {
|
|
6130
|
+
const m = PERMISSION_MODE_RE.exec(line);
|
|
6131
|
+
if (m) info.permissionMode = m[1].trim();
|
|
6132
|
+
}
|
|
6133
|
+
if (info.model === void 0) {
|
|
6134
|
+
const m = MODEL_RE.exec(line);
|
|
6135
|
+
if (m) info.model = m[0].replace(/\s+/g, " ").trim();
|
|
6136
|
+
}
|
|
6137
|
+
if (info.model && info.effort && info.permissionMode) break;
|
|
6138
|
+
}
|
|
6139
|
+
return info;
|
|
6140
|
+
}
|
|
6141
|
+
|
|
5962
6142
|
// src/services/questions/detectAskUserQuestion.ts
|
|
5963
6143
|
function normalizeContent2(raw) {
|
|
5964
6144
|
if (Array.isArray(raw)) return raw;
|
|
@@ -6083,7 +6263,7 @@ function resolveAnswer(pending, body) {
|
|
|
6083
6263
|
}
|
|
6084
6264
|
|
|
6085
6265
|
// src/services/sessions/conversationBusy.ts
|
|
6086
|
-
import { statSync as
|
|
6266
|
+
import { statSync as statSync8 } from "fs";
|
|
6087
6267
|
var RESUME_BUSY_WINDOW_MS = 12e4;
|
|
6088
6268
|
function resolveResumeBusyWindowMs(env = process.env) {
|
|
6089
6269
|
const raw = env.THREADBASE_RESUME_BUSY_WINDOW_MS;
|
|
@@ -6100,7 +6280,7 @@ function conversationBusy(input) {
|
|
|
6100
6280
|
let lastActivityMs = null;
|
|
6101
6281
|
if (input.jsonlPath) {
|
|
6102
6282
|
try {
|
|
6103
|
-
const mtimeMs =
|
|
6283
|
+
const mtimeMs = statSync8(input.jsonlPath).mtimeMs;
|
|
6104
6284
|
const age = now - mtimeMs;
|
|
6105
6285
|
lastActivityMs = Math.max(0, age);
|
|
6106
6286
|
const isSelfEcho = input.selfPtyEndedAt != null && mtimeMs <= input.selfPtyEndedAt + SELF_ACTIVITY_SKEW_MS;
|
|
@@ -6328,7 +6508,7 @@ function discoveredToResponse(d, conversationId) {
|
|
|
6328
6508
|
import { randomBytes as randomBytes3 } from "crypto";
|
|
6329
6509
|
import { mkdir as mkdir3, writeFile } from "fs/promises";
|
|
6330
6510
|
import heicConvert from "heic-convert";
|
|
6331
|
-
import { join as
|
|
6511
|
+
import { join as join17 } from "path";
|
|
6332
6512
|
var UPLOAD_DIR_NAME = ".threadbase-uploads";
|
|
6333
6513
|
var MAX_BYTES = 25 * 1024 * 1024;
|
|
6334
6514
|
var HEIC_MIMES = /* @__PURE__ */ new Set(["image/heic", "image/heif"]);
|
|
@@ -6361,9 +6541,9 @@ async function saveUploadFile(input) {
|
|
|
6361
6541
|
}
|
|
6362
6542
|
const id = `up_${randomBytes3(8).toString("hex")}`;
|
|
6363
6543
|
const safeName = sanitizeFilename(originalName) || `file${MIME_TO_EXT[mimeType] ?? ""}`;
|
|
6364
|
-
const dir =
|
|
6544
|
+
const dir = join17(input.projectPath, UPLOAD_DIR_NAME, input.sessionId);
|
|
6365
6545
|
await mkdir3(dir, { recursive: true });
|
|
6366
|
-
const filePath =
|
|
6546
|
+
const filePath = join17(dir, `${Date.now()}-${id}-${safeName}`);
|
|
6367
6547
|
await writeFile(filePath, buffer);
|
|
6368
6548
|
return {
|
|
6369
6549
|
id,
|
|
@@ -6473,14 +6653,6 @@ function computeConversationEtag({
|
|
|
6473
6653
|
return `"${digest}"`;
|
|
6474
6654
|
}
|
|
6475
6655
|
|
|
6476
|
-
// src/utils/dates.ts
|
|
6477
|
-
import { compareDesc, isValid, parseISO } from "date-fns";
|
|
6478
|
-
function parseIsoDateOrNull(value) {
|
|
6479
|
-
if (!value) return null;
|
|
6480
|
-
const parsed = parseISO(value);
|
|
6481
|
-
return isValid(parsed) ? parsed : null;
|
|
6482
|
-
}
|
|
6483
|
-
|
|
6484
6656
|
// src/utils/isScannedSnapshotStale.ts
|
|
6485
6657
|
var STALENESS_TOLERANCE_MS = 1e3;
|
|
6486
6658
|
function isScannedSnapshotStale(snapshotTimestamp, fileMtimeMs) {
|
|
@@ -6781,13 +6953,13 @@ var StreamerServer = class {
|
|
|
6781
6953
|
this.disableDb = config.disableDb ?? false;
|
|
6782
6954
|
this.scannerPersistenceDisabled = config.scannerPersistent === false;
|
|
6783
6955
|
this.scanProfiles = config.scanProfiles;
|
|
6784
|
-
this.codexRoots = config.codexRoots ?? [
|
|
6956
|
+
this.codexRoots = config.codexRoots ?? [join18(homedir9(), ".codex", "sessions")];
|
|
6785
6957
|
this.ptyGracePeriodMs = config.ptyGracePeriodMs ?? DEFAULT_PTY_GRACE_PERIOD_MS;
|
|
6786
6958
|
this.defaultSystemPrompt = config.defaultSystemPrompt ?? DEFAULT_SYSTEM_PROMPT;
|
|
6787
6959
|
this.defaultPermissionMode = config.defaultPermissionMode ?? loadDefaultPermissionMode() ?? "acceptEdits";
|
|
6788
6960
|
this.defaultModel = config.defaultModel ?? "sonnet";
|
|
6789
6961
|
this.defaultEffort = config.defaultEffort ?? "low";
|
|
6790
|
-
this.cacheDir = config.cacheDir ?? loadCacheDir() ??
|
|
6962
|
+
this.cacheDir = config.cacheDir ?? loadCacheDir() ?? join18(homedir9(), ".threadbase", "cache");
|
|
6791
6963
|
this.tailSize = config.tailSize ?? loadTailSize() ?? 10;
|
|
6792
6964
|
this.directoryDebounceMs = parseDirScanDebounceEnv(process.env.THREADBASE_DIR_SCAN_DEBOUNCE_MS) ?? config.directoryScanDebounceMs ?? 1e3;
|
|
6793
6965
|
this.markScannerStaleDebounced = debounce(() => {
|
|
@@ -6828,7 +7000,7 @@ var StreamerServer = class {
|
|
|
6828
7000
|
const seqs = cache.extendMessageIndex(
|
|
6829
7001
|
filePath,
|
|
6830
7002
|
spans,
|
|
6831
|
-
|
|
7003
|
+
statSync9(filePath),
|
|
6832
7004
|
readFrom,
|
|
6833
7005
|
endOffset
|
|
6834
7006
|
);
|
|
@@ -6994,7 +7166,7 @@ var StreamerServer = class {
|
|
|
6994
7166
|
temporalClient,
|
|
6995
7167
|
taskQueue: agentConfig.temporal.taskQueue
|
|
6996
7168
|
});
|
|
6997
|
-
const conversationsBaseDir = agentConfig.conversationsDir ||
|
|
7169
|
+
const conversationsBaseDir = agentConfig.conversationsDir || join18(dirname9(this.cacheDir), "conversations");
|
|
6998
7170
|
conversationWriter = createConversationWriter({
|
|
6999
7171
|
baseDir: conversationsBaseDir
|
|
7000
7172
|
});
|
|
@@ -7115,7 +7287,7 @@ var StreamerServer = class {
|
|
|
7115
7287
|
}
|
|
7116
7288
|
}
|
|
7117
7289
|
if (msg.type === "hold_session" && typeof msg.sessionId === "string") {
|
|
7118
|
-
this.startGraceTimer(msg.sessionId,
|
|
7290
|
+
this.startGraceTimer(msg.sessionId, this.ptyGracePeriodMs);
|
|
7119
7291
|
}
|
|
7120
7292
|
} catch {
|
|
7121
7293
|
}
|
|
@@ -7317,7 +7489,7 @@ var StreamerServer = class {
|
|
|
7317
7489
|
});
|
|
7318
7490
|
try {
|
|
7319
7491
|
this.cache = ConversationCache.open(
|
|
7320
|
-
|
|
7492
|
+
join18(this.cacheDir, "cache.db"),
|
|
7321
7493
|
this.tailSize,
|
|
7322
7494
|
void 0,
|
|
7323
7495
|
{
|
|
@@ -7443,6 +7615,20 @@ var StreamerServer = class {
|
|
|
7443
7615
|
count: pruned.length,
|
|
7444
7616
|
event: "cache.prune_ghosts"
|
|
7445
7617
|
});
|
|
7618
|
+
if (this.projectsRepo && this.conversationsRepo && this.cacheMetadataRepo) {
|
|
7619
|
+
refreshConversationCache({
|
|
7620
|
+
cache: this.cache,
|
|
7621
|
+
projectsRepo: this.projectsRepo,
|
|
7622
|
+
conversationsRepo: this.conversationsRepo,
|
|
7623
|
+
cacheMetadataRepo: this.cacheMetadataRepo
|
|
7624
|
+
});
|
|
7625
|
+
} else if (this.cacheMetadataRepo) {
|
|
7626
|
+
setCacheMetadata(
|
|
7627
|
+
this.cacheMetadataRepo,
|
|
7628
|
+
"conversations_last_indexed_at",
|
|
7629
|
+
(/* @__PURE__ */ new Date()).toISOString()
|
|
7630
|
+
);
|
|
7631
|
+
}
|
|
7446
7632
|
}
|
|
7447
7633
|
}).catch((err) => {
|
|
7448
7634
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -7682,6 +7868,56 @@ var StreamerServer = class {
|
|
|
7682
7868
|
checkSessionInputRateLimit(sessionId) {
|
|
7683
7869
|
return this.checkRateLimit(this.sessionInputAttempts, sessionId, 500, 6e4);
|
|
7684
7870
|
}
|
|
7871
|
+
/** Project roots watched for conversation JSONLs (profiles or ~/.claude/projects). */
|
|
7872
|
+
projectsDirsForFreshnessCheck() {
|
|
7873
|
+
if (this.scanProfiles && this.scanProfiles.length > 0) {
|
|
7874
|
+
return this.scanProfiles.filter((p) => p.enabled).map((p) => join18(p.configDir, "projects"));
|
|
7875
|
+
}
|
|
7876
|
+
return [join18(homedir9(), ".claude", "projects")];
|
|
7877
|
+
}
|
|
7878
|
+
/**
|
|
7879
|
+
* Full-glob scan + cache upsert/delete reconcile. Used by ?refresh=1 and by
|
|
7880
|
+
* the automatic freshness path when the directory watcher marked the scanner
|
|
7881
|
+
* stale or shouldRefreshProjectsFromHdd detected disk drift.
|
|
7882
|
+
*/
|
|
7883
|
+
async reconcileConversationsCacheFromDisk() {
|
|
7884
|
+
if (!this.cache) return;
|
|
7885
|
+
const scanner = await this.rescanForRefresh();
|
|
7886
|
+
const metas = [...scanner.getMetadataCache().values()];
|
|
7887
|
+
try {
|
|
7888
|
+
this.cache.upsertFromScannerMeta(metas);
|
|
7889
|
+
if (!this.cacheMonitor?.pending) {
|
|
7890
|
+
this.cache.reconcileDeletions(canonicalLivePathSet(metas));
|
|
7891
|
+
}
|
|
7892
|
+
if (this.projectsRepo && this.conversationsRepo && this.cacheMetadataRepo) {
|
|
7893
|
+
refreshConversationCache({
|
|
7894
|
+
cache: this.cache,
|
|
7895
|
+
projectsRepo: this.projectsRepo,
|
|
7896
|
+
conversationsRepo: this.conversationsRepo,
|
|
7897
|
+
cacheMetadataRepo: this.cacheMetadataRepo
|
|
7898
|
+
});
|
|
7899
|
+
} else if (this.cacheMetadataRepo) {
|
|
7900
|
+
setCacheMetadata(
|
|
7901
|
+
this.cacheMetadataRepo,
|
|
7902
|
+
"conversations_last_indexed_at",
|
|
7903
|
+
(/* @__PURE__ */ new Date()).toISOString()
|
|
7904
|
+
);
|
|
7905
|
+
}
|
|
7906
|
+
} catch (err) {
|
|
7907
|
+
this.log.warn(
|
|
7908
|
+
`refresh reconcile failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
7909
|
+
{ event: "conversations.reconcile_failed" }
|
|
7910
|
+
);
|
|
7911
|
+
}
|
|
7912
|
+
}
|
|
7913
|
+
shouldAutoReconcileConversationList() {
|
|
7914
|
+
if (!this.cache) return false;
|
|
7915
|
+
if (this.scannerStale) return true;
|
|
7916
|
+
if (!this.conversationsRepo || !this.cacheMetadataRepo) return false;
|
|
7917
|
+
return shouldRefreshProjectsFromHdd(this.conversationsRepo, this.cacheMetadataRepo, {
|
|
7918
|
+
projectsDirs: this.projectsDirsForFreshnessCheck()
|
|
7919
|
+
});
|
|
7920
|
+
}
|
|
7685
7921
|
async handleListConversations(url, res) {
|
|
7686
7922
|
if (this.rejectIfWarmingUp(res)) return;
|
|
7687
7923
|
const limit = intParam(url, "limit", 50);
|
|
@@ -7690,19 +7926,14 @@ var StreamerServer = class {
|
|
|
7690
7926
|
const project = url.searchParams.get("project") ?? void 0;
|
|
7691
7927
|
const providerFilter = url.searchParams.get("provider") ?? void 0;
|
|
7692
7928
|
const bustCache = url.searchParams.get("refresh") === "1";
|
|
7693
|
-
if (
|
|
7694
|
-
|
|
7695
|
-
|
|
7696
|
-
|
|
7697
|
-
|
|
7698
|
-
if (!this.cacheMonitor?.pending) {
|
|
7699
|
-
this.cache.reconcileDeletions(canonicalLivePathSet(metas2));
|
|
7700
|
-
}
|
|
7701
|
-
} catch (err) {
|
|
7702
|
-
this.log.warn(
|
|
7703
|
-
`refresh reconcile failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
7704
|
-
{ event: "conversations.reconcile_failed" }
|
|
7929
|
+
if (this.cache && (bustCache || this.shouldAutoReconcileConversationList())) {
|
|
7930
|
+
if (bustCache) {
|
|
7931
|
+
await this.withWarmup(
|
|
7932
|
+
"conversation_refresh",
|
|
7933
|
+
() => this.reconcileConversationsCacheFromDisk()
|
|
7705
7934
|
);
|
|
7935
|
+
} else {
|
|
7936
|
+
await this.reconcileConversationsCacheFromDisk();
|
|
7706
7937
|
}
|
|
7707
7938
|
}
|
|
7708
7939
|
if (this.cache) {
|
|
@@ -7964,21 +8195,21 @@ var StreamerServer = class {
|
|
|
7964
8195
|
*/
|
|
7965
8196
|
projectsDirs() {
|
|
7966
8197
|
if (this.scanProfiles && this.scanProfiles.length > 0) {
|
|
7967
|
-
return this.scanProfiles.filter((p) => p.enabled).map((p) =>
|
|
8198
|
+
return this.scanProfiles.filter((p) => p.enabled).map((p) => join18(p.configDir, "projects"));
|
|
7968
8199
|
}
|
|
7969
|
-
return [
|
|
8200
|
+
return [join18(homedir9(), ".claude", "projects")];
|
|
7970
8201
|
}
|
|
7971
8202
|
findJsonlPath(uuid) {
|
|
7972
8203
|
const filename = `${uuid}.jsonl`;
|
|
7973
8204
|
for (const projectsDir of this.projectsDirs()) {
|
|
7974
8205
|
if (!existsSync10(projectsDir)) continue;
|
|
7975
|
-
for (const dir of
|
|
7976
|
-
const fp =
|
|
8206
|
+
for (const dir of readdirSync6(projectsDir)) {
|
|
8207
|
+
const fp = join18(projectsDir, dir, filename);
|
|
7977
8208
|
if (existsSync10(fp)) return fp;
|
|
7978
|
-
const projectDir =
|
|
8209
|
+
const projectDir = join18(projectsDir, dir);
|
|
7979
8210
|
try {
|
|
7980
|
-
for (const sub of
|
|
7981
|
-
const subagentPath =
|
|
8211
|
+
for (const sub of readdirSync6(projectDir)) {
|
|
8212
|
+
const subagentPath = join18(projectDir, sub, "subagents", filename);
|
|
7982
8213
|
if (existsSync10(subagentPath)) return subagentPath;
|
|
7983
8214
|
}
|
|
7984
8215
|
} catch {
|
|
@@ -8079,7 +8310,7 @@ var StreamerServer = class {
|
|
|
8079
8310
|
if (this.isManagedTailPath(key)) return;
|
|
8080
8311
|
let mtimeMs;
|
|
8081
8312
|
try {
|
|
8082
|
-
mtimeMs =
|
|
8313
|
+
mtimeMs = statSync9(filePath).mtimeMs;
|
|
8083
8314
|
} catch {
|
|
8084
8315
|
return;
|
|
8085
8316
|
}
|
|
@@ -8261,7 +8492,7 @@ var StreamerServer = class {
|
|
|
8261
8492
|
if (!conv.filePath) return false;
|
|
8262
8493
|
let mtimeMs = null;
|
|
8263
8494
|
try {
|
|
8264
|
-
mtimeMs =
|
|
8495
|
+
mtimeMs = statSync9(conv.filePath).mtimeMs;
|
|
8265
8496
|
} catch {
|
|
8266
8497
|
return false;
|
|
8267
8498
|
}
|
|
@@ -8630,13 +8861,23 @@ var StreamerServer = class {
|
|
|
8630
8861
|
throw err;
|
|
8631
8862
|
}
|
|
8632
8863
|
}
|
|
8633
|
-
handleGetSession(sessionId, res) {
|
|
8864
|
+
async handleGetSession(sessionId, res) {
|
|
8634
8865
|
if (this.rejectIfWarmingUp(res)) return;
|
|
8635
8866
|
const session = this.sessionStore.get(sessionId, this.ptyAttachedIds());
|
|
8636
8867
|
if (session) {
|
|
8637
8868
|
if (!existsSync10(session.projectPath)) {
|
|
8638
8869
|
session.failureReason = `Project directory not found: ${session.projectPath}`;
|
|
8639
8870
|
}
|
|
8871
|
+
if (this.ptyManager.hasSession(sessionId)) {
|
|
8872
|
+
try {
|
|
8873
|
+
const lines = await this.ptyManager.getOutputLines(sessionId, 10);
|
|
8874
|
+
const status = parseStatusLine(lines);
|
|
8875
|
+
if (session.model == null && status.model) session.model = status.model;
|
|
8876
|
+
if (status.effort) session.effort = status.effort;
|
|
8877
|
+
if (status.permissionMode) session.permissionMode = status.permissionMode;
|
|
8878
|
+
} catch {
|
|
8879
|
+
}
|
|
8880
|
+
}
|
|
8640
8881
|
json(res, 200, session);
|
|
8641
8882
|
return;
|
|
8642
8883
|
}
|
|
@@ -9171,7 +9412,7 @@ var StreamerServer = class {
|
|
|
9171
9412
|
sessionStore: this.sessionStore,
|
|
9172
9413
|
// biome-ignore lint/style/noNonNullAssertion: agentClient is set when agentConfig.enabled is true
|
|
9173
9414
|
agentClient: this.agentClient,
|
|
9174
|
-
conversationsDir: this.cacheDir ?
|
|
9415
|
+
conversationsDir: this.cacheDir ? join18(dirname9(this.cacheDir), "conversations") : "",
|
|
9175
9416
|
agentConfig: this.agentConfig
|
|
9176
9417
|
});
|
|
9177
9418
|
json(res, result.status, result.body);
|
|
@@ -9330,9 +9571,9 @@ var StreamerServer = class {
|
|
|
9330
9571
|
// was passed to Claude via --session-id so the filename matches from the start.
|
|
9331
9572
|
watchForJsonl(sessionId, projectPath) {
|
|
9332
9573
|
const encoded = projectPath.replace(/[/\\:.]/g, "-");
|
|
9333
|
-
const projectsDir =
|
|
9574
|
+
const projectsDir = join18(homedir9(), ".claude", "projects", encoded);
|
|
9334
9575
|
const expectedFile = `${sessionId}.jsonl`;
|
|
9335
|
-
const filePath =
|
|
9576
|
+
const filePath = join18(projectsDir, expectedFile);
|
|
9336
9577
|
const deadline = Date.now() + 12e4;
|
|
9337
9578
|
let watcher = null;
|
|
9338
9579
|
const cleanup = () => {
|
|
@@ -9354,10 +9595,10 @@ var StreamerServer = class {
|
|
|
9354
9595
|
if (!resolvedFilePath && existsSync10(projectsDir)) {
|
|
9355
9596
|
try {
|
|
9356
9597
|
const now = Date.now();
|
|
9357
|
-
const match =
|
|
9358
|
-
({ f }) => basename5(f, ".jsonl") === sessionId || this.readFirstLineSessionId(
|
|
9598
|
+
const match = readdirSync6(projectsDir).filter((f) => f.endsWith(".jsonl")).map((f) => ({ f, mtime: statSync9(join18(projectsDir, f)).mtimeMs })).filter(({ mtime }) => now - mtime < 5e3).filter(
|
|
9599
|
+
({ f }) => basename5(f, ".jsonl") === sessionId || this.readFirstLineSessionId(join18(projectsDir, f)) === sessionId
|
|
9359
9600
|
).sort((a, b) => b.mtime - a.mtime)[0];
|
|
9360
|
-
if (match) resolvedFilePath =
|
|
9601
|
+
if (match) resolvedFilePath = join18(projectsDir, match.f);
|
|
9361
9602
|
} catch {
|
|
9362
9603
|
}
|
|
9363
9604
|
}
|
|
@@ -9405,7 +9646,7 @@ var StreamerServer = class {
|
|
|
9405
9646
|
watchForCodexRollout(sessionId, projectPath) {
|
|
9406
9647
|
const deadline = Date.now() + 12e4;
|
|
9407
9648
|
const now = /* @__PURE__ */ new Date();
|
|
9408
|
-
const dateDir =
|
|
9649
|
+
const dateDir = join18(
|
|
9409
9650
|
String(now.getFullYear()),
|
|
9410
9651
|
String(now.getMonth() + 1).padStart(2, "0"),
|
|
9411
9652
|
String(now.getDate()).padStart(2, "0")
|
|
@@ -9446,18 +9687,18 @@ var StreamerServer = class {
|
|
|
9446
9687
|
this.sessionStore.listManaged().filter((s) => s.id !== sessionId && s.boundConversationId != null).map((s) => s.boundConversationId)
|
|
9447
9688
|
);
|
|
9448
9689
|
for (const root of this.codexRoots) {
|
|
9449
|
-
const sessionsDir =
|
|
9690
|
+
const sessionsDir = join18(root, dateDir);
|
|
9450
9691
|
if (!existsSync10(sessionsDir)) continue;
|
|
9451
9692
|
let candidateFiles;
|
|
9452
9693
|
try {
|
|
9453
|
-
candidateFiles =
|
|
9694
|
+
candidateFiles = readdirSync6(sessionsDir).filter((f) => f.endsWith(".jsonl"));
|
|
9454
9695
|
} catch {
|
|
9455
9696
|
continue;
|
|
9456
9697
|
}
|
|
9457
9698
|
const nowMs = Date.now();
|
|
9458
|
-
const recentCandidates = candidateFiles.map((f) => ({ f, mtime:
|
|
9699
|
+
const recentCandidates = candidateFiles.map((f) => ({ f, mtime: statSync9(join18(sessionsDir, f)).mtimeMs })).filter(({ mtime }) => nowMs - mtime < 1e4).sort((a, b) => b.mtime - a.mtime);
|
|
9459
9700
|
for (const { f } of recentCandidates) {
|
|
9460
|
-
const candidatePath =
|
|
9701
|
+
const candidatePath = join18(sessionsDir, f);
|
|
9461
9702
|
const match = matchesProjectPath(candidatePath);
|
|
9462
9703
|
if (!match) continue;
|
|
9463
9704
|
if (boundElsewhere.has(match.id)) continue;
|