@threadbase-sh/scanner 0.7.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/LICENSE +21 -0
- package/README.md +304 -0
- package/dist/cli.js +1447 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.cjs +1340 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +269 -0
- package/dist/index.d.ts +269 -0
- package/dist/index.js +1279 -0
- package/dist/index.js.map +1 -0
- package/package.json +84 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,1340 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var index_exports = {};
|
|
32
|
+
__export(index_exports, {
|
|
33
|
+
ConversationScanner: () => ConversationScanner,
|
|
34
|
+
DEFAULT_TIERS: () => DEFAULT_TIERS,
|
|
35
|
+
SearchIndexer: () => SearchIndexer,
|
|
36
|
+
VALID_SORT_ORDERS: () => VALID_SORT_ORDERS,
|
|
37
|
+
applyAccountFilter: () => applyAccountFilter,
|
|
38
|
+
applyIncludeFilter: () => applyIncludeFilter,
|
|
39
|
+
applyPagination: () => applyPagination,
|
|
40
|
+
applyProjectFilter: () => applyProjectFilter,
|
|
41
|
+
applySinceFilter: () => applySinceFilter,
|
|
42
|
+
applySort: () => applySort,
|
|
43
|
+
cleanSystemTags: () => cleanSystemTags,
|
|
44
|
+
createLogger: () => createLogger,
|
|
45
|
+
detectDefaultProfile: () => detectDefaultProfile,
|
|
46
|
+
getConversation: () => getConversation,
|
|
47
|
+
getLogger: () => getLogger,
|
|
48
|
+
getProjectsDir: () => getProjectsDir,
|
|
49
|
+
loadProfiles: () => loadProfiles,
|
|
50
|
+
readGitBranch: () => readGitBranch,
|
|
51
|
+
resetDefaultScanner: () => resetDefaultScanner,
|
|
52
|
+
resolveConfigDir: () => resolveConfigDir,
|
|
53
|
+
resolveTier: () => resolveTier,
|
|
54
|
+
saveProfiles: () => saveProfiles,
|
|
55
|
+
scan: () => scan,
|
|
56
|
+
search: () => search,
|
|
57
|
+
setLogger: () => setLogger
|
|
58
|
+
});
|
|
59
|
+
module.exports = __toCommonJS(index_exports);
|
|
60
|
+
|
|
61
|
+
// src/filters.ts
|
|
62
|
+
function applySort(metas, order) {
|
|
63
|
+
const out = [...metas];
|
|
64
|
+
switch (order) {
|
|
65
|
+
case "recent":
|
|
66
|
+
out.sort((a, b) => b.timestamp.localeCompare(a.timestamp));
|
|
67
|
+
break;
|
|
68
|
+
case "oldest":
|
|
69
|
+
out.sort((a, b) => a.timestamp.localeCompare(b.timestamp));
|
|
70
|
+
break;
|
|
71
|
+
case "messages-desc":
|
|
72
|
+
out.sort((a, b) => b.messageCount - a.messageCount);
|
|
73
|
+
break;
|
|
74
|
+
case "messages-asc":
|
|
75
|
+
out.sort((a, b) => a.messageCount - b.messageCount);
|
|
76
|
+
break;
|
|
77
|
+
case "alpha":
|
|
78
|
+
out.sort((a, b) => {
|
|
79
|
+
const cmp = a.projectName.localeCompare(b.projectName);
|
|
80
|
+
return cmp !== 0 ? cmp : a.preview.localeCompare(b.preview);
|
|
81
|
+
});
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
return out;
|
|
85
|
+
}
|
|
86
|
+
function applySinceFilter(metas, since) {
|
|
87
|
+
const cutoff = parseSinceCutoff(since);
|
|
88
|
+
return metas.filter((m) => new Date(m.timestamp).getTime() >= cutoff.getTime());
|
|
89
|
+
}
|
|
90
|
+
function applyIncludeFilter(metas, include) {
|
|
91
|
+
switch (include) {
|
|
92
|
+
case "all":
|
|
93
|
+
return metas;
|
|
94
|
+
case "conversations":
|
|
95
|
+
return metas.filter((m) => !m.isSubagent && !m.isTeammate);
|
|
96
|
+
case "subagents":
|
|
97
|
+
return metas.filter((m) => m.isSubagent);
|
|
98
|
+
case "teammates":
|
|
99
|
+
return metas.filter((m) => m.isTeammate);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
function applyProjectFilter(metas, project) {
|
|
103
|
+
const lower = project.toLowerCase();
|
|
104
|
+
return metas.filter(
|
|
105
|
+
(m) => m.projectPath.toLowerCase().includes(lower) || m.projectName.toLowerCase().includes(lower)
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
function applyAccountFilter(metas, account) {
|
|
109
|
+
return metas.filter((m) => m.account === account);
|
|
110
|
+
}
|
|
111
|
+
function applyPagination(items, limit, offset) {
|
|
112
|
+
return {
|
|
113
|
+
items: items.slice(offset, offset + limit),
|
|
114
|
+
total: items.length
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
function parseSinceCutoff(value) {
|
|
118
|
+
const s = value.trim();
|
|
119
|
+
if (!s) throw new Error("Empty --since value");
|
|
120
|
+
const isoMatch = s.match(/^\d{4}-\d{2}-\d{2}$/);
|
|
121
|
+
if (isoMatch) {
|
|
122
|
+
const d = /* @__PURE__ */ new Date(`${s}T00:00:00Z`);
|
|
123
|
+
if (!Number.isNaN(d.getTime())) return d;
|
|
124
|
+
}
|
|
125
|
+
const durationMatch = s.match(/^(\d+)([hdw])$/);
|
|
126
|
+
if (!durationMatch) {
|
|
127
|
+
throw new Error(
|
|
128
|
+
`Invalid --since value "${s}": expected duration like "7d", "24h", "2w" or ISO date "2006-01-02"`
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
const n = parseInt(durationMatch[1], 10);
|
|
132
|
+
const unit = durationMatch[2];
|
|
133
|
+
let ms;
|
|
134
|
+
switch (unit) {
|
|
135
|
+
case "h":
|
|
136
|
+
ms = n * 60 * 60 * 1e3;
|
|
137
|
+
break;
|
|
138
|
+
case "d":
|
|
139
|
+
ms = n * 24 * 60 * 60 * 1e3;
|
|
140
|
+
break;
|
|
141
|
+
case "w":
|
|
142
|
+
ms = n * 7 * 24 * 60 * 60 * 1e3;
|
|
143
|
+
break;
|
|
144
|
+
default:
|
|
145
|
+
throw new Error(`Invalid unit "${unit}"`);
|
|
146
|
+
}
|
|
147
|
+
return new Date(Date.now() - ms);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// src/git.ts
|
|
151
|
+
var import_fs = require("fs");
|
|
152
|
+
var import_path = require("path");
|
|
153
|
+
|
|
154
|
+
// src/logger.ts
|
|
155
|
+
var import_pino = __toESM(require("pino"), 1);
|
|
156
|
+
var currentLogger = (0, import_pino.default)({ level: "silent" });
|
|
157
|
+
function createLogger(options) {
|
|
158
|
+
if (options && typeof options.child === "function") {
|
|
159
|
+
return options;
|
|
160
|
+
}
|
|
161
|
+
return (0, import_pino.default)(options ?? { level: "silent" });
|
|
162
|
+
}
|
|
163
|
+
function setLogger(logger) {
|
|
164
|
+
currentLogger = logger;
|
|
165
|
+
}
|
|
166
|
+
function getLogger() {
|
|
167
|
+
return currentLogger;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// src/git.ts
|
|
171
|
+
var REF_PREFIX = "ref: refs/heads/";
|
|
172
|
+
var MAX_DEPTH = 6;
|
|
173
|
+
function readGitBranch(projectPath) {
|
|
174
|
+
if (!projectPath) return null;
|
|
175
|
+
const log = getLogger();
|
|
176
|
+
let dir = projectPath;
|
|
177
|
+
let depth = 0;
|
|
178
|
+
while (depth < MAX_DEPTH) {
|
|
179
|
+
const headPath = (0, import_path.join)(dir, ".git", "HEAD");
|
|
180
|
+
try {
|
|
181
|
+
const content = (0, import_fs.readFileSync)(headPath, "utf-8").trim();
|
|
182
|
+
if (content.startsWith(REF_PREFIX)) {
|
|
183
|
+
const branch = content.slice(REF_PREFIX.length);
|
|
184
|
+
log.trace({ projectPath, dir, branch }, "git: branch resolved");
|
|
185
|
+
return branch;
|
|
186
|
+
}
|
|
187
|
+
if (content.length >= 7) {
|
|
188
|
+
log.trace({ projectPath, dir }, "git: detached HEAD");
|
|
189
|
+
return "(detached)";
|
|
190
|
+
}
|
|
191
|
+
return null;
|
|
192
|
+
} catch {
|
|
193
|
+
}
|
|
194
|
+
const parent = (0, import_path.dirname)(dir);
|
|
195
|
+
if (parent === dir) return null;
|
|
196
|
+
dir = parent;
|
|
197
|
+
depth++;
|
|
198
|
+
}
|
|
199
|
+
log.trace({ projectPath }, "git: no .git found within depth");
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// src/indexer.ts
|
|
204
|
+
var import_flexsearch = __toESM(require("flexsearch"), 1);
|
|
205
|
+
var FlexSearch = import_flexsearch.default.default ?? import_flexsearch.default;
|
|
206
|
+
var SearchIndexer = class {
|
|
207
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
208
|
+
index;
|
|
209
|
+
documents = /* @__PURE__ */ new Map();
|
|
210
|
+
constructor() {
|
|
211
|
+
this.index = this.createIndex();
|
|
212
|
+
}
|
|
213
|
+
createIndex() {
|
|
214
|
+
return new FlexSearch.Document({
|
|
215
|
+
document: {
|
|
216
|
+
id: "id",
|
|
217
|
+
index: [
|
|
218
|
+
"content",
|
|
219
|
+
"projectName",
|
|
220
|
+
"projectPath",
|
|
221
|
+
"sessionId",
|
|
222
|
+
"sessionName",
|
|
223
|
+
"account",
|
|
224
|
+
"model",
|
|
225
|
+
"gitBranch",
|
|
226
|
+
"toolNames"
|
|
227
|
+
],
|
|
228
|
+
store: ["id"]
|
|
229
|
+
},
|
|
230
|
+
tokenize: "forward",
|
|
231
|
+
resolution: 9,
|
|
232
|
+
cache: 100
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
addDocument(meta) {
|
|
236
|
+
this.documents.set(meta.id, meta);
|
|
237
|
+
this.index.add({
|
|
238
|
+
id: meta.id,
|
|
239
|
+
content: meta.contentSnippet,
|
|
240
|
+
projectName: meta.projectName,
|
|
241
|
+
projectPath: meta.projectPath,
|
|
242
|
+
sessionId: meta.sessionId,
|
|
243
|
+
sessionName: meta.sessionName,
|
|
244
|
+
account: meta.account,
|
|
245
|
+
model: meta.model || "",
|
|
246
|
+
gitBranch: meta.gitBranch || "",
|
|
247
|
+
toolNames: meta.toolNames.join(" ")
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
buildIndex(metas) {
|
|
251
|
+
this.clear();
|
|
252
|
+
for (const meta of metas) {
|
|
253
|
+
this.addDocument(meta);
|
|
254
|
+
}
|
|
255
|
+
getLogger().debug({ docCount: metas.length }, "indexer: built");
|
|
256
|
+
}
|
|
257
|
+
search(query, options) {
|
|
258
|
+
const limit = options?.limit ?? 50;
|
|
259
|
+
if (!query.trim()) {
|
|
260
|
+
return this.getRecent(limit);
|
|
261
|
+
}
|
|
262
|
+
const results = this.index.search(query, { limit: limit * 2, enrich: true });
|
|
263
|
+
const seen = /* @__PURE__ */ new Set();
|
|
264
|
+
const searchResults = [];
|
|
265
|
+
for (const fieldResult of results) {
|
|
266
|
+
if (!fieldResult.result) continue;
|
|
267
|
+
for (const item of fieldResult.result) {
|
|
268
|
+
const id = typeof item === "object" ? item.id : String(item);
|
|
269
|
+
if (seen.has(id)) continue;
|
|
270
|
+
seen.add(id);
|
|
271
|
+
const meta = this.documents.get(id);
|
|
272
|
+
if (!meta) continue;
|
|
273
|
+
const matches = this.generateMatches(meta, query);
|
|
274
|
+
searchResults.push({ meta, score: 1, matches });
|
|
275
|
+
if (searchResults.length >= limit) break;
|
|
276
|
+
}
|
|
277
|
+
if (searchResults.length >= limit) break;
|
|
278
|
+
}
|
|
279
|
+
return searchResults;
|
|
280
|
+
}
|
|
281
|
+
getRecent(limit) {
|
|
282
|
+
return Array.from(this.documents.values()).sort((a, b) => b.timestamp.localeCompare(a.timestamp)).slice(0, limit).map((meta) => ({
|
|
283
|
+
meta,
|
|
284
|
+
score: 1,
|
|
285
|
+
matches: [{ field: "timestamp", snippet: meta.preview }]
|
|
286
|
+
}));
|
|
287
|
+
}
|
|
288
|
+
generateMatches(meta, query) {
|
|
289
|
+
const matches = [];
|
|
290
|
+
const lowerQuery = query.toLowerCase();
|
|
291
|
+
const fields = [
|
|
292
|
+
["contentSnippet", meta.contentSnippet],
|
|
293
|
+
["projectName", meta.projectName],
|
|
294
|
+
["sessionId", meta.sessionId],
|
|
295
|
+
["sessionName", meta.sessionName],
|
|
296
|
+
["account", meta.account],
|
|
297
|
+
["model", meta.model || ""],
|
|
298
|
+
["gitBranch", meta.gitBranch || ""],
|
|
299
|
+
["toolNames", meta.toolNames.join(" ")]
|
|
300
|
+
];
|
|
301
|
+
for (const [field, value] of fields) {
|
|
302
|
+
const idx = value.toLowerCase().indexOf(lowerQuery);
|
|
303
|
+
if (idx !== -1) {
|
|
304
|
+
const start = Math.max(0, idx - 80);
|
|
305
|
+
const end = Math.min(value.length, idx + query.length + 120);
|
|
306
|
+
let snippet = value.slice(start, end);
|
|
307
|
+
if (start > 0) snippet = `...${snippet}`;
|
|
308
|
+
if (end < value.length) snippet = `${snippet}...`;
|
|
309
|
+
matches.push({ field, snippet });
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
return matches.length > 0 ? matches : [{ field: "preview", snippet: meta.preview }];
|
|
313
|
+
}
|
|
314
|
+
getDocumentCount() {
|
|
315
|
+
return this.documents.size;
|
|
316
|
+
}
|
|
317
|
+
// Replace an already-indexed document in place. FlexSearch's `add` does not
|
|
318
|
+
// overwrite an existing id, so a single-file refresh must go through
|
|
319
|
+
// `update` to avoid stale matches lingering in the index.
|
|
320
|
+
updateDocument(meta) {
|
|
321
|
+
this.documents.set(meta.id, meta);
|
|
322
|
+
this.index.update({
|
|
323
|
+
id: meta.id,
|
|
324
|
+
content: meta.contentSnippet,
|
|
325
|
+
projectName: meta.projectName,
|
|
326
|
+
projectPath: meta.projectPath,
|
|
327
|
+
sessionId: meta.sessionId,
|
|
328
|
+
sessionName: meta.sessionName,
|
|
329
|
+
account: meta.account,
|
|
330
|
+
model: meta.model || "",
|
|
331
|
+
gitBranch: meta.gitBranch || "",
|
|
332
|
+
toolNames: meta.toolNames.join(" ")
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
removeDocument(id) {
|
|
336
|
+
this.documents.delete(id);
|
|
337
|
+
this.index.remove(id);
|
|
338
|
+
}
|
|
339
|
+
clear() {
|
|
340
|
+
this.documents.clear();
|
|
341
|
+
this.index = this.createIndex();
|
|
342
|
+
getLogger().trace("indexer: cleared");
|
|
343
|
+
}
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
// src/profiles.ts
|
|
347
|
+
var import_promises = require("fs/promises");
|
|
348
|
+
var import_os = require("os");
|
|
349
|
+
var import_path2 = require("path");
|
|
350
|
+
var PROFILES_FILE = "profiles.json";
|
|
351
|
+
function resolveConfigDir(configDir) {
|
|
352
|
+
return configDir.replace(/^~/, (0, import_os.homedir)());
|
|
353
|
+
}
|
|
354
|
+
function getProjectsDir(profile) {
|
|
355
|
+
return (0, import_path2.join)(resolveConfigDir(profile.configDir), "projects");
|
|
356
|
+
}
|
|
357
|
+
async function detectDefaultProfile() {
|
|
358
|
+
return {
|
|
359
|
+
id: "default",
|
|
360
|
+
label: "Default",
|
|
361
|
+
configDir: (0, import_path2.join)((0, import_os.homedir)(), ".claude"),
|
|
362
|
+
enabled: true,
|
|
363
|
+
emoji: "\u{1F916}"
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
async function loadProfiles(configPath) {
|
|
367
|
+
const log = getLogger();
|
|
368
|
+
try {
|
|
369
|
+
const resolved = resolveConfigDir(configPath);
|
|
370
|
+
const data = await (0, import_promises.readFile)((0, import_path2.join)(resolved, PROFILES_FILE), "utf-8");
|
|
371
|
+
const profiles = JSON.parse(data);
|
|
372
|
+
log.debug({ configPath, count: profiles.length }, "profiles: loaded");
|
|
373
|
+
return profiles;
|
|
374
|
+
} catch (err) {
|
|
375
|
+
log.debug({ configPath, err }, "profiles: load failed, using default");
|
|
376
|
+
const defaultProfile = await detectDefaultProfile();
|
|
377
|
+
return [defaultProfile];
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
async function saveProfiles(profiles, configPath) {
|
|
381
|
+
const resolved = resolveConfigDir(configPath);
|
|
382
|
+
await (0, import_promises.mkdir)(resolved, { recursive: true });
|
|
383
|
+
await (0, import_promises.writeFile)((0, import_path2.join)(resolved, PROFILES_FILE), JSON.stringify(profiles, null, 2));
|
|
384
|
+
getLogger().debug({ configPath, count: profiles.length }, "profiles: saved");
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// src/scanner.ts
|
|
388
|
+
var import_fs3 = require("fs");
|
|
389
|
+
|
|
390
|
+
// src/cache.ts
|
|
391
|
+
var LRUCache = class {
|
|
392
|
+
map = /* @__PURE__ */ new Map();
|
|
393
|
+
capacity;
|
|
394
|
+
constructor(capacity) {
|
|
395
|
+
this.capacity = capacity;
|
|
396
|
+
}
|
|
397
|
+
get(key) {
|
|
398
|
+
const value = this.map.get(key);
|
|
399
|
+
if (value === void 0) return void 0;
|
|
400
|
+
this.map.delete(key);
|
|
401
|
+
this.map.set(key, value);
|
|
402
|
+
return value;
|
|
403
|
+
}
|
|
404
|
+
set(key, value) {
|
|
405
|
+
this.map.delete(key);
|
|
406
|
+
this.map.set(key, value);
|
|
407
|
+
if (this.map.size > this.capacity) {
|
|
408
|
+
const oldest = this.map.keys().next();
|
|
409
|
+
if (!oldest.done) this.map.delete(oldest.value);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
has(key) {
|
|
413
|
+
return this.map.has(key);
|
|
414
|
+
}
|
|
415
|
+
delete(key) {
|
|
416
|
+
return this.map.delete(key);
|
|
417
|
+
}
|
|
418
|
+
clear() {
|
|
419
|
+
this.map.clear();
|
|
420
|
+
}
|
|
421
|
+
get size() {
|
|
422
|
+
return this.map.size;
|
|
423
|
+
}
|
|
424
|
+
};
|
|
425
|
+
|
|
426
|
+
// src/discovery.ts
|
|
427
|
+
var import_fast_glob = __toESM(require("fast-glob"), 1);
|
|
428
|
+
var import_promises2 = require("fs/promises");
|
|
429
|
+
var EXCLUDED_SEGMENTS = ["/memory/", "/tool-results/"];
|
|
430
|
+
var STAT_CONCURRENCY = 32;
|
|
431
|
+
async function discoverJsonlFiles(dirs, onProgress) {
|
|
432
|
+
const log = getLogger();
|
|
433
|
+
const results = [];
|
|
434
|
+
for (const { projectsDir, account } of dirs) {
|
|
435
|
+
let filePaths;
|
|
436
|
+
try {
|
|
437
|
+
filePaths = await (0, import_fast_glob.default)("**/*.jsonl", {
|
|
438
|
+
cwd: projectsDir,
|
|
439
|
+
absolute: true,
|
|
440
|
+
dot: false
|
|
441
|
+
});
|
|
442
|
+
} catch (err) {
|
|
443
|
+
log.warn({ projectsDir, account, err }, "discovery: glob failed");
|
|
444
|
+
continue;
|
|
445
|
+
}
|
|
446
|
+
const filtered = filePaths.filter((fp) => !EXCLUDED_SEGMENTS.some((seg) => fp.includes(seg)));
|
|
447
|
+
let kept = 0;
|
|
448
|
+
let skippedEmpty = 0;
|
|
449
|
+
let skippedInaccessible = 0;
|
|
450
|
+
for (let i = 0; i < filtered.length; i += STAT_CONCURRENCY) {
|
|
451
|
+
const chunk = filtered.slice(i, i + STAT_CONCURRENCY);
|
|
452
|
+
const statted = await Promise.all(
|
|
453
|
+
chunk.map(async (filePath) => {
|
|
454
|
+
try {
|
|
455
|
+
const s = await (0, import_promises2.stat)(filePath);
|
|
456
|
+
return { filePath, size: s.size };
|
|
457
|
+
} catch (err) {
|
|
458
|
+
log.warn({ filePath, err }, "discovery: stat failed");
|
|
459
|
+
return { filePath, size: -1 };
|
|
460
|
+
}
|
|
461
|
+
})
|
|
462
|
+
);
|
|
463
|
+
for (const { filePath, size } of statted) {
|
|
464
|
+
if (size < 0) {
|
|
465
|
+
skippedInaccessible++;
|
|
466
|
+
} else if (size > 0) {
|
|
467
|
+
results.push({ filePath, account });
|
|
468
|
+
kept++;
|
|
469
|
+
} else {
|
|
470
|
+
skippedEmpty++;
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
log.debug(
|
|
475
|
+
{
|
|
476
|
+
projectsDir,
|
|
477
|
+
account,
|
|
478
|
+
globMatches: filePaths.length,
|
|
479
|
+
afterExclusions: filtered.length,
|
|
480
|
+
kept,
|
|
481
|
+
skippedEmpty,
|
|
482
|
+
skippedInaccessible
|
|
483
|
+
},
|
|
484
|
+
"discovery: directory scanned"
|
|
485
|
+
);
|
|
486
|
+
onProgress?.(results.length);
|
|
487
|
+
}
|
|
488
|
+
log.debug({ totalFiles: results.length, dirs: dirs.length }, "discovery: complete");
|
|
489
|
+
return results;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// src/parser.ts
|
|
493
|
+
var import_fs2 = require("fs");
|
|
494
|
+
var import_path3 = require("path");
|
|
495
|
+
var import_readline = require("readline");
|
|
496
|
+
|
|
497
|
+
// src/tags.ts
|
|
498
|
+
var SYSTEM_TAGS = [
|
|
499
|
+
"system-reminder",
|
|
500
|
+
"command-name",
|
|
501
|
+
"command-message",
|
|
502
|
+
"command-args",
|
|
503
|
+
"ide_selection",
|
|
504
|
+
"ide_opened_file",
|
|
505
|
+
"local-command-stdout",
|
|
506
|
+
"local-command-caveat",
|
|
507
|
+
"retrieval_status",
|
|
508
|
+
"task_id",
|
|
509
|
+
"task_type",
|
|
510
|
+
"task-id",
|
|
511
|
+
"task-notification",
|
|
512
|
+
"fast_mode_info",
|
|
513
|
+
"persisted-output",
|
|
514
|
+
"tool_use_error",
|
|
515
|
+
"user-prompt-submit-hook",
|
|
516
|
+
"thinking",
|
|
517
|
+
"ask_user",
|
|
518
|
+
"teammate-message"
|
|
519
|
+
];
|
|
520
|
+
var SYSTEM_TAG_RE = new RegExp(`<(${SYSTEM_TAGS.join("|")})[^>]*>[\\s\\S]*?<\\/\\1>`, "g");
|
|
521
|
+
function cleanSystemTags(text) {
|
|
522
|
+
return text.replace(SYSTEM_TAG_RE, "").replace(/[^\S\n]+/g, " ").replace(/\n{3,}/g, "\n\n").trim();
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// src/parser.ts
|
|
526
|
+
async function parseMeta(filePath, account, tier) {
|
|
527
|
+
const log = getLogger();
|
|
528
|
+
log.trace({ filePath, account, tier: tier.name }, "parseMeta: start");
|
|
529
|
+
let sessionId = "";
|
|
530
|
+
let badJsonLines = 0;
|
|
531
|
+
let sessionName = "";
|
|
532
|
+
let latestTimestamp = "";
|
|
533
|
+
let cwd = "";
|
|
534
|
+
let teamName = "";
|
|
535
|
+
let model = null;
|
|
536
|
+
let messageCount = 0;
|
|
537
|
+
let lastMessageSender = "user";
|
|
538
|
+
let isTeammate = false;
|
|
539
|
+
let firstUserSeen = false;
|
|
540
|
+
let firstMessage = null;
|
|
541
|
+
let lastMessage = null;
|
|
542
|
+
let lastPrompt = "";
|
|
543
|
+
const toolNameSet = /* @__PURE__ */ new Set();
|
|
544
|
+
const previewParts = [];
|
|
545
|
+
const snippetParts = [];
|
|
546
|
+
let snippetLength = 0;
|
|
547
|
+
let previewLength = 0;
|
|
548
|
+
const fileStream = (0, import_fs2.createReadStream)(filePath);
|
|
549
|
+
const rl = (0, import_readline.createInterface)({ input: fileStream, crlfDelay: Infinity });
|
|
550
|
+
try {
|
|
551
|
+
for await (const line of rl) {
|
|
552
|
+
if (!line.trim()) continue;
|
|
553
|
+
let entry;
|
|
554
|
+
try {
|
|
555
|
+
entry = JSON.parse(line);
|
|
556
|
+
} catch {
|
|
557
|
+
badJsonLines++;
|
|
558
|
+
continue;
|
|
559
|
+
}
|
|
560
|
+
if (entry.cwd && !cwd) cwd = entry.cwd;
|
|
561
|
+
if (entry.sessionId && !sessionId) sessionId = entry.sessionId;
|
|
562
|
+
if (entry.slug && !sessionName) sessionName = entry.slug;
|
|
563
|
+
if (entry.teamName && !teamName) teamName = entry.teamName;
|
|
564
|
+
if (entry.timestamp) {
|
|
565
|
+
const ts = entry.timestamp;
|
|
566
|
+
if (!latestTimestamp || ts > latestTimestamp) latestTimestamp = ts;
|
|
567
|
+
}
|
|
568
|
+
const type = entry.type;
|
|
569
|
+
if (type === "last-prompt") {
|
|
570
|
+
if (entry.lastPrompt && !lastPrompt) lastPrompt = entry.lastPrompt;
|
|
571
|
+
continue;
|
|
572
|
+
}
|
|
573
|
+
if (type !== "user" && type !== "assistant") continue;
|
|
574
|
+
if (entry.isMeta) continue;
|
|
575
|
+
if (model === null) {
|
|
576
|
+
const msg2 = entry.message;
|
|
577
|
+
if (msg2?.model) model = msg2.model;
|
|
578
|
+
}
|
|
579
|
+
if (type === "user" && !firstUserSeen) {
|
|
580
|
+
firstUserSeen = true;
|
|
581
|
+
if (isTeammateContent(entry.message?.content)) {
|
|
582
|
+
isTeammate = true;
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
const msg = entry.message;
|
|
586
|
+
const content = extractTextContent(msg?.content);
|
|
587
|
+
const hasToolUseResult = type === "user" && entry.toolUseResult != null;
|
|
588
|
+
const isOnlyToolResult = hasToolUseResult && isOnlyToolResultContent(msg?.content);
|
|
589
|
+
collectToolNames(msg?.content, toolNameSet);
|
|
590
|
+
if (content || isOnlyToolResult) {
|
|
591
|
+
messageCount++;
|
|
592
|
+
lastMessageSender = type;
|
|
593
|
+
if (content) {
|
|
594
|
+
const ts = entry.timestamp || "";
|
|
595
|
+
if (!firstMessage) {
|
|
596
|
+
firstMessage = { text: content.slice(0, 200), timestamp: ts };
|
|
597
|
+
}
|
|
598
|
+
lastMessage = { text: content.slice(0, 200), timestamp: ts };
|
|
599
|
+
if (previewLength < tier.previewMax) {
|
|
600
|
+
previewParts.push(content);
|
|
601
|
+
previewLength += content.length;
|
|
602
|
+
}
|
|
603
|
+
if (snippetLength < tier.snippetMax) {
|
|
604
|
+
const remaining = tier.snippetMax - snippetLength;
|
|
605
|
+
const chunk = content.length > remaining ? content.slice(0, remaining) : content;
|
|
606
|
+
snippetParts.push(chunk);
|
|
607
|
+
snippetLength += chunk.length;
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
} catch (err) {
|
|
613
|
+
log.warn({ filePath, err }, "parseMeta: read failed");
|
|
614
|
+
return null;
|
|
615
|
+
}
|
|
616
|
+
if (badJsonLines > 0) {
|
|
617
|
+
log.warn({ filePath, badJsonLines }, "parseMeta: skipped malformed JSON lines");
|
|
618
|
+
}
|
|
619
|
+
if (messageCount === 0) {
|
|
620
|
+
log.trace({ filePath }, "parseMeta: no messages");
|
|
621
|
+
return null;
|
|
622
|
+
}
|
|
623
|
+
const isSubagent = filePath.includes("/subagents/");
|
|
624
|
+
let parentSessionId = null;
|
|
625
|
+
if (isSubagent) {
|
|
626
|
+
const uuidDir = (0, import_path3.dirname)((0, import_path3.dirname)(filePath));
|
|
627
|
+
parentSessionId = (0, import_path3.join)((0, import_path3.dirname)(uuidDir), `${(0, import_path3.basename)(uuidDir)}.jsonl`);
|
|
628
|
+
}
|
|
629
|
+
const projectPath = cwd;
|
|
630
|
+
const preview = previewParts.join(" ").slice(0, tier.previewMax);
|
|
631
|
+
return {
|
|
632
|
+
id: filePath,
|
|
633
|
+
filePath,
|
|
634
|
+
sessionId: sessionId || (0, import_path3.basename)(filePath, ".jsonl"),
|
|
635
|
+
sessionName,
|
|
636
|
+
projectPath,
|
|
637
|
+
projectName: getShortProjectName(projectPath),
|
|
638
|
+
account,
|
|
639
|
+
timestamp: latestTimestamp || (/* @__PURE__ */ new Date()).toISOString(),
|
|
640
|
+
messageCount,
|
|
641
|
+
lastMessageSender,
|
|
642
|
+
preview,
|
|
643
|
+
contentSnippet: snippetParts.join(" "),
|
|
644
|
+
gitBranch: null,
|
|
645
|
+
model,
|
|
646
|
+
isSubagent,
|
|
647
|
+
parentSessionId,
|
|
648
|
+
isTeammate,
|
|
649
|
+
teamName: teamName || null,
|
|
650
|
+
toolNames: Array.from(toolNameSet),
|
|
651
|
+
firstMessage,
|
|
652
|
+
lastMessage,
|
|
653
|
+
lastPrompt: lastPrompt || void 0
|
|
654
|
+
};
|
|
655
|
+
}
|
|
656
|
+
async function parseConversation(filePath, account) {
|
|
657
|
+
const log = getLogger();
|
|
658
|
+
log.trace({ filePath, account }, "parseConversation: start");
|
|
659
|
+
const messages = [];
|
|
660
|
+
let badJsonLines = 0;
|
|
661
|
+
let sessionId = "";
|
|
662
|
+
let sessionName = "";
|
|
663
|
+
let latestTimestamp = "";
|
|
664
|
+
let cwd = "";
|
|
665
|
+
const textParts = [];
|
|
666
|
+
const pendingToolUses = /* @__PURE__ */ new Map();
|
|
667
|
+
const teamInfoMap = /* @__PURE__ */ new Map();
|
|
668
|
+
const turnDurations = [];
|
|
669
|
+
let lastPrompt = "";
|
|
670
|
+
const fileStream = (0, import_fs2.createReadStream)(filePath);
|
|
671
|
+
const rl = (0, import_readline.createInterface)({ input: fileStream, crlfDelay: Infinity });
|
|
672
|
+
try {
|
|
673
|
+
for await (const line of rl) {
|
|
674
|
+
if (!line.trim()) continue;
|
|
675
|
+
let entry;
|
|
676
|
+
try {
|
|
677
|
+
entry = JSON.parse(line);
|
|
678
|
+
} catch {
|
|
679
|
+
badJsonLines++;
|
|
680
|
+
continue;
|
|
681
|
+
}
|
|
682
|
+
if (entry.cwd && !cwd) cwd = entry.cwd;
|
|
683
|
+
if (entry.sessionId && !sessionId) sessionId = entry.sessionId;
|
|
684
|
+
if (entry.slug && !sessionName) sessionName = entry.slug;
|
|
685
|
+
if (entry.timestamp) {
|
|
686
|
+
const ts = entry.timestamp;
|
|
687
|
+
if (!latestTimestamp || ts > latestTimestamp) latestTimestamp = ts;
|
|
688
|
+
}
|
|
689
|
+
const type = entry.type;
|
|
690
|
+
if (type === "last-prompt") {
|
|
691
|
+
if (entry.lastPrompt && !lastPrompt) lastPrompt = entry.lastPrompt;
|
|
692
|
+
continue;
|
|
693
|
+
}
|
|
694
|
+
if (type === "system") {
|
|
695
|
+
if (entry.subtype === "turn_duration" && typeof entry.durationMs === "number") {
|
|
696
|
+
turnDurations.push({
|
|
697
|
+
durationMs: entry.durationMs,
|
|
698
|
+
messageCount: entry.messageCount || 0,
|
|
699
|
+
uuid: entry.uuid
|
|
700
|
+
});
|
|
701
|
+
}
|
|
702
|
+
continue;
|
|
703
|
+
}
|
|
704
|
+
if (type !== "user" && type !== "assistant") continue;
|
|
705
|
+
if (entry.isMeta) continue;
|
|
706
|
+
const msg = entry.message;
|
|
707
|
+
const toolUseBlocks = extractToolUseBlocks(msg?.content);
|
|
708
|
+
for (const block of toolUseBlocks) {
|
|
709
|
+
pendingToolUses.set(block.id, block);
|
|
710
|
+
}
|
|
711
|
+
const hasToolUseResult = type === "user" && entry.toolUseResult != null;
|
|
712
|
+
const isToolResultOnly = hasToolUseResult && isOnlyToolResultContent(msg?.content);
|
|
713
|
+
const content = extractTextContent(msg?.content);
|
|
714
|
+
const thinking = type === "assistant" ? extractThinking(msg?.content) : null;
|
|
715
|
+
const hasThinking = !!(thinking?.content || thinking?.signature);
|
|
716
|
+
if (content || isToolResultOnly || toolUseBlocks.length > 0 || hasThinking) {
|
|
717
|
+
const metadata = {};
|
|
718
|
+
if (msg?.model) metadata.model = msg.model;
|
|
719
|
+
if (msg?.stop_reason !== void 0) metadata.stopReason = msg.stop_reason;
|
|
720
|
+
if (entry.gitBranch) metadata.gitBranch = entry.gitBranch;
|
|
721
|
+
if (entry.version) metadata.version = entry.version;
|
|
722
|
+
const usage = msg?.usage;
|
|
723
|
+
if (usage) {
|
|
724
|
+
if (usage.input_tokens) metadata.inputTokens = usage.input_tokens;
|
|
725
|
+
if (usage.output_tokens) metadata.outputTokens = usage.output_tokens;
|
|
726
|
+
if (usage.cache_read_input_tokens)
|
|
727
|
+
metadata.cacheReadTokens = usage.cache_read_input_tokens;
|
|
728
|
+
if (usage.cache_creation_input_tokens)
|
|
729
|
+
metadata.cacheCreationTokens = usage.cache_creation_input_tokens;
|
|
730
|
+
}
|
|
731
|
+
const toolUseNames = extractToolUseNames(msg?.content);
|
|
732
|
+
if (toolUseNames.length > 0) metadata.toolUses = toolUseNames;
|
|
733
|
+
if (toolUseBlocks.length > 0) metadata.toolUseBlocks = toolUseBlocks;
|
|
734
|
+
if (isToolResultOnly) {
|
|
735
|
+
const toolResultBlocks = extractToolResultBlocks(msg?.content, pendingToolUses);
|
|
736
|
+
if (toolResultBlocks.length > 0) metadata.toolResults = toolResultBlocks;
|
|
737
|
+
}
|
|
738
|
+
if (entry.teamName) {
|
|
739
|
+
metadata.teamName = entry.teamName;
|
|
740
|
+
if (!teamInfoMap.has(metadata.teamName) && content) {
|
|
741
|
+
const info = parseTeammateMessageTag(content);
|
|
742
|
+
if (info) teamInfoMap.set(metadata.teamName, info);
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
const thinkingContent = thinking?.content || void 0;
|
|
746
|
+
const thinkingSignature = thinking?.signature || void 0;
|
|
747
|
+
const hasMetadata = Object.keys(metadata).length > 0;
|
|
748
|
+
messages.push({
|
|
749
|
+
role: type,
|
|
750
|
+
text: content || "",
|
|
751
|
+
timestamp: entry.timestamp || "",
|
|
752
|
+
uuid: entry.uuid || void 0,
|
|
753
|
+
metadata: hasMetadata ? metadata : void 0,
|
|
754
|
+
isToolResult: isToolResultOnly || void 0,
|
|
755
|
+
isThinking: thinkingContent || thinkingSignature ? true : void 0,
|
|
756
|
+
thinkingContent,
|
|
757
|
+
thinkingSignature,
|
|
758
|
+
parentUuid: entry.parentUuid !== void 0 ? entry.parentUuid : void 0,
|
|
759
|
+
requestId: type === "assistant" ? entry.requestId : void 0,
|
|
760
|
+
promptId: type === "user" ? entry.promptId : void 0,
|
|
761
|
+
isSidechain: typeof entry.isSidechain === "boolean" ? entry.isSidechain : void 0,
|
|
762
|
+
permissionMode: type === "user" ? entry.permissionMode : void 0,
|
|
763
|
+
hasImages: hasImageBlocks(msg?.content) || void 0,
|
|
764
|
+
attachment: entry.attachment !== void 0 ? entry.attachment : void 0
|
|
765
|
+
});
|
|
766
|
+
if (content) textParts.push(content);
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
} catch (err) {
|
|
770
|
+
log.warn({ filePath, err }, "parseConversation: read failed");
|
|
771
|
+
return null;
|
|
772
|
+
}
|
|
773
|
+
if (badJsonLines > 0) {
|
|
774
|
+
log.warn({ filePath, badJsonLines }, "parseConversation: skipped malformed JSON lines");
|
|
775
|
+
}
|
|
776
|
+
if (messages.length === 0) {
|
|
777
|
+
log.trace({ filePath }, "parseConversation: no messages");
|
|
778
|
+
return null;
|
|
779
|
+
}
|
|
780
|
+
log.debug({ filePath, messageCount: messages.length }, "parseConversation: complete");
|
|
781
|
+
if (teamInfoMap.size > 0) {
|
|
782
|
+
for (const msg of messages) {
|
|
783
|
+
if (msg.metadata?.teamName) {
|
|
784
|
+
const info = teamInfoMap.get(msg.metadata.teamName);
|
|
785
|
+
if (info) msg.metadata.teamInfo = info;
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
return {
|
|
790
|
+
id: filePath,
|
|
791
|
+
filePath,
|
|
792
|
+
projectPath: cwd,
|
|
793
|
+
projectName: getShortProjectName(cwd),
|
|
794
|
+
sessionId: sessionId || (0, import_path3.basename)(filePath, ".jsonl"),
|
|
795
|
+
sessionName,
|
|
796
|
+
messages,
|
|
797
|
+
fullText: textParts.join(" "),
|
|
798
|
+
timestamp: latestTimestamp || (/* @__PURE__ */ new Date()).toISOString(),
|
|
799
|
+
messageCount: messages.length,
|
|
800
|
+
account,
|
|
801
|
+
turnDurations: turnDurations.length > 0 ? turnDurations : void 0,
|
|
802
|
+
lastPrompt: lastPrompt || void 0
|
|
803
|
+
};
|
|
804
|
+
}
|
|
805
|
+
function extractTextContent(content) {
|
|
806
|
+
if (!content) return "";
|
|
807
|
+
if (typeof content === "string") return cleanSystemTags(content);
|
|
808
|
+
if (Array.isArray(content)) {
|
|
809
|
+
return content.map((item) => {
|
|
810
|
+
if (typeof item === "string") return item;
|
|
811
|
+
if (item?.type === "text" && item?.text) return item.text;
|
|
812
|
+
if (item?.type === "tool_result" && typeof item?.content === "string") return item.content;
|
|
813
|
+
return "";
|
|
814
|
+
}).filter(Boolean).map(cleanSystemTags).join(" ");
|
|
815
|
+
}
|
|
816
|
+
return "";
|
|
817
|
+
}
|
|
818
|
+
function extractToolUseNames(content) {
|
|
819
|
+
if (!Array.isArray(content)) return [];
|
|
820
|
+
return content.filter((item) => item?.type === "tool_use" && item?.name).map((item) => item.name);
|
|
821
|
+
}
|
|
822
|
+
function extractToolUseBlocks(content) {
|
|
823
|
+
if (!Array.isArray(content)) return [];
|
|
824
|
+
return content.filter((item) => item?.type === "tool_use" && item?.name && item?.id).map((item) => ({
|
|
825
|
+
id: item.id,
|
|
826
|
+
name: item.name,
|
|
827
|
+
input: item.input || {}
|
|
828
|
+
}));
|
|
829
|
+
}
|
|
830
|
+
var TOOL_NAME_TO_TYPE = {
|
|
831
|
+
Edit: "edit",
|
|
832
|
+
Write: "write",
|
|
833
|
+
Read: "read",
|
|
834
|
+
Bash: "bash",
|
|
835
|
+
Grep: "grep",
|
|
836
|
+
Glob: "glob",
|
|
837
|
+
Agent: "taskAgent",
|
|
838
|
+
TaskCreate: "taskCreate",
|
|
839
|
+
TaskUpdate: "taskUpdate"
|
|
840
|
+
};
|
|
841
|
+
function extractToolResultBlocks(content, pendingToolUses) {
|
|
842
|
+
if (!Array.isArray(content)) return [];
|
|
843
|
+
return content.filter((item) => item?.type === "tool_result" && item?.tool_use_id).map((item) => {
|
|
844
|
+
const toolName = pendingToolUses.get(item.tool_use_id)?.name ?? "";
|
|
845
|
+
return {
|
|
846
|
+
toolUseId: item.tool_use_id,
|
|
847
|
+
type: TOOL_NAME_TO_TYPE[toolName] ?? "generic",
|
|
848
|
+
content: typeof item.content === "string" ? { text: item.content } : item.content ?? {},
|
|
849
|
+
isError: typeof item.is_error === "boolean" ? item.is_error : void 0
|
|
850
|
+
};
|
|
851
|
+
});
|
|
852
|
+
}
|
|
853
|
+
function collectToolNames(content, toolSet) {
|
|
854
|
+
if (!Array.isArray(content)) return;
|
|
855
|
+
for (const item of content) {
|
|
856
|
+
if (item?.type === "tool_use" && item?.name) {
|
|
857
|
+
toolSet.add(item.name);
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
function isOnlyToolResultContent(content) {
|
|
862
|
+
if (!Array.isArray(content)) return false;
|
|
863
|
+
return content.length > 0 && content.every((item) => item?.type === "tool_result");
|
|
864
|
+
}
|
|
865
|
+
function isTeammateContent(content) {
|
|
866
|
+
const raw = typeof content === "string" ? content : Array.isArray(content) ? content.map(
|
|
867
|
+
(item) => typeof item === "string" ? item : item?.type === "text" ? item.text ?? "" : ""
|
|
868
|
+
).join("") : "";
|
|
869
|
+
return raw.includes("<teammate-message");
|
|
870
|
+
}
|
|
871
|
+
function extractThinking(content) {
|
|
872
|
+
if (!Array.isArray(content)) return { content: "", signature: "" };
|
|
873
|
+
const blocks = content.filter((item) => item?.type === "thinking");
|
|
874
|
+
return {
|
|
875
|
+
content: blocks.map((b) => b.thinking).filter(Boolean).join("\n\n"),
|
|
876
|
+
signature: blocks.map((b) => b.signature).filter(Boolean).join("")
|
|
877
|
+
};
|
|
878
|
+
}
|
|
879
|
+
function hasImageBlocks(content) {
|
|
880
|
+
if (!Array.isArray(content)) return false;
|
|
881
|
+
return content.some(
|
|
882
|
+
(item) => item?.type === "image" && (item?.source?.type === "base64" || item?.file?.base64 !== void 0)
|
|
883
|
+
);
|
|
884
|
+
}
|
|
885
|
+
function parseTeammateMessageTag(content) {
|
|
886
|
+
const match = content.match(/<teammate-message\s+([^>]*)>/);
|
|
887
|
+
if (!match) return null;
|
|
888
|
+
const attrs = match[1];
|
|
889
|
+
const id = attrs.match(/teammate_id="([^"]*)"/)?.[1];
|
|
890
|
+
if (!id) return null;
|
|
891
|
+
const summary = attrs.match(/summary="([^"]*)"/)?.[1];
|
|
892
|
+
const color = attrs.match(/color="([^"]*)"/)?.[1];
|
|
893
|
+
return { teammateId: id, summary, color };
|
|
894
|
+
}
|
|
895
|
+
function getShortProjectName(fullPath) {
|
|
896
|
+
const parts = fullPath.split("/").filter(Boolean);
|
|
897
|
+
return parts.slice(-3).join("/");
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
// src/tiers.ts
|
|
901
|
+
var DEFAULT_TIERS = {
|
|
902
|
+
standard: { name: "standard", previewMax: 200, snippetMax: 5e3 },
|
|
903
|
+
full: { name: "full", previewMax: 1200, snippetMax: 5e4 }
|
|
904
|
+
};
|
|
905
|
+
function resolveTier(tierName, customTiers) {
|
|
906
|
+
const tier = customTiers?.[tierName] ?? DEFAULT_TIERS[tierName];
|
|
907
|
+
if (!tier) {
|
|
908
|
+
throw new Error(
|
|
909
|
+
`Unknown tier "${tierName}". Available: ${Object.keys({ ...DEFAULT_TIERS, ...customTiers }).join(", ")}`
|
|
910
|
+
);
|
|
911
|
+
}
|
|
912
|
+
return tier;
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
// src/scanner.ts
|
|
916
|
+
var BATCH_SIZE = 12;
|
|
917
|
+
var DEFAULT_CONFIG_PATH = "~/.config/threadbase-scanner";
|
|
918
|
+
var ConversationScanner = class {
|
|
919
|
+
metadataCache = /* @__PURE__ */ new Map();
|
|
920
|
+
conversationLRU;
|
|
921
|
+
sessionIdIndex = /* @__PURE__ */ new Map();
|
|
922
|
+
projects = /* @__PURE__ */ new Set();
|
|
923
|
+
indexer = new SearchIndexer();
|
|
924
|
+
// Tier the most recent scan() ran with, so refreshFile() re-parses a single
|
|
925
|
+
// file at the same content depth. Defaults to the standard tier.
|
|
926
|
+
lastTier = resolveTier("standard");
|
|
927
|
+
constructor(options) {
|
|
928
|
+
this.conversationLRU = new LRUCache(options?.conversationCacheSize ?? 5);
|
|
929
|
+
}
|
|
930
|
+
async scan(options = {}) {
|
|
931
|
+
const log = getLogger();
|
|
932
|
+
const startedAt = Date.now();
|
|
933
|
+
const profiles = await this.resolveProfiles(options.profiles);
|
|
934
|
+
const activeProfiles = profiles.filter((p) => p.enabled && p.scanHistory !== false);
|
|
935
|
+
const tier = resolveTier(options.tier ?? "standard", options.tiers);
|
|
936
|
+
this.lastTier = tier;
|
|
937
|
+
log.info(
|
|
938
|
+
{
|
|
939
|
+
activeProfiles: activeProfiles.length,
|
|
940
|
+
tier: tier.name,
|
|
941
|
+
sort: options.sort ?? "recent",
|
|
942
|
+
include: options.include ?? "all",
|
|
943
|
+
view: options.view ?? "flat"
|
|
944
|
+
},
|
|
945
|
+
"scan: start"
|
|
946
|
+
);
|
|
947
|
+
this.metadataCache.clear();
|
|
948
|
+
this.conversationLRU.clear();
|
|
949
|
+
this.sessionIdIndex.clear();
|
|
950
|
+
this.projects.clear();
|
|
951
|
+
this.indexer.clear();
|
|
952
|
+
const configDirs = activeProfiles.map((p) => ({
|
|
953
|
+
projectsDir: getProjectsDir(p),
|
|
954
|
+
account: p.id
|
|
955
|
+
}));
|
|
956
|
+
const files = await discoverJsonlFiles(configDirs);
|
|
957
|
+
const totalFiles = files.length;
|
|
958
|
+
let scanned = 0;
|
|
959
|
+
let parseFailures = 0;
|
|
960
|
+
const allMetas = [];
|
|
961
|
+
const gitBranchMemo = /* @__PURE__ */ new Map();
|
|
962
|
+
const resolveGitBranch = (projectPath) => {
|
|
963
|
+
let branch = gitBranchMemo.get(projectPath);
|
|
964
|
+
if (branch === void 0) {
|
|
965
|
+
branch = readGitBranch(projectPath);
|
|
966
|
+
gitBranchMemo.set(projectPath, branch);
|
|
967
|
+
}
|
|
968
|
+
return branch;
|
|
969
|
+
};
|
|
970
|
+
const { statCache } = options;
|
|
971
|
+
for (let i = 0; i < files.length; i += BATCH_SIZE) {
|
|
972
|
+
const batch = files.slice(i, i + BATCH_SIZE);
|
|
973
|
+
const results = await Promise.all(
|
|
974
|
+
batch.map(async ({ filePath, account }) => {
|
|
975
|
+
if (statCache) {
|
|
976
|
+
const cached = statCache.get(filePath);
|
|
977
|
+
if (cached) {
|
|
978
|
+
try {
|
|
979
|
+
const s = (0, import_fs3.statSync)(filePath);
|
|
980
|
+
if (s.mtimeMs === cached.stat.mtimeMs && s.size === cached.stat.size) {
|
|
981
|
+
return cached.meta;
|
|
982
|
+
}
|
|
983
|
+
} catch {
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
try {
|
|
988
|
+
const meta = await parseMeta(filePath, account, tier);
|
|
989
|
+
if (meta) {
|
|
990
|
+
meta.gitBranch = resolveGitBranch(meta.projectPath);
|
|
991
|
+
}
|
|
992
|
+
return meta;
|
|
993
|
+
} catch (err) {
|
|
994
|
+
parseFailures++;
|
|
995
|
+
log.warn({ filePath, account, err }, "scan: parseMeta threw");
|
|
996
|
+
return null;
|
|
997
|
+
}
|
|
998
|
+
})
|
|
999
|
+
);
|
|
1000
|
+
const batchMetas = [];
|
|
1001
|
+
for (const meta of results) {
|
|
1002
|
+
if (meta && meta.messageCount > 0) {
|
|
1003
|
+
this.metadataCache.set(meta.id, meta);
|
|
1004
|
+
this.sessionIdIndex.set(meta.sessionId, meta);
|
|
1005
|
+
this.projects.add(meta.projectPath);
|
|
1006
|
+
allMetas.push(meta);
|
|
1007
|
+
batchMetas.push(meta);
|
|
1008
|
+
this.indexer.addDocument(meta);
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
if (batchMetas.length > 0) {
|
|
1012
|
+
options.onBatch?.(batchMetas);
|
|
1013
|
+
}
|
|
1014
|
+
scanned += batch.length;
|
|
1015
|
+
log.debug({ scanned, totalFiles, batchKept: batchMetas.length }, "scan: batch complete");
|
|
1016
|
+
options.onProgress?.(scanned, totalFiles);
|
|
1017
|
+
}
|
|
1018
|
+
let filtered = allMetas;
|
|
1019
|
+
if (options.include && options.include !== "all") {
|
|
1020
|
+
filtered = applyIncludeFilter(filtered, options.include);
|
|
1021
|
+
}
|
|
1022
|
+
if (options.project) {
|
|
1023
|
+
filtered = applyProjectFilter(filtered, options.project);
|
|
1024
|
+
}
|
|
1025
|
+
if (options.account) {
|
|
1026
|
+
filtered = applyAccountFilter(filtered, options.account);
|
|
1027
|
+
}
|
|
1028
|
+
if (options.since) {
|
|
1029
|
+
filtered = applySinceFilter(filtered, options.since);
|
|
1030
|
+
}
|
|
1031
|
+
filtered = applySort(filtered, options.sort ?? "recent");
|
|
1032
|
+
const total = filtered.length;
|
|
1033
|
+
const conversations = this.transformView(filtered, options);
|
|
1034
|
+
const elapsedMs = Date.now() - startedAt;
|
|
1035
|
+
log.info(
|
|
1036
|
+
{
|
|
1037
|
+
totalFiles,
|
|
1038
|
+
scanned,
|
|
1039
|
+
kept: allMetas.length,
|
|
1040
|
+
filteredTotal: total,
|
|
1041
|
+
parseFailures,
|
|
1042
|
+
elapsedMs
|
|
1043
|
+
},
|
|
1044
|
+
"scan: complete"
|
|
1045
|
+
);
|
|
1046
|
+
if (Array.isArray(conversations)) {
|
|
1047
|
+
const limit = options.limit ?? 50;
|
|
1048
|
+
const offset = options.offset ?? 0;
|
|
1049
|
+
const paginated = applyPagination(conversations, limit, offset);
|
|
1050
|
+
return { conversations: paginated.items, total, scanned };
|
|
1051
|
+
}
|
|
1052
|
+
return { conversations, total, scanned };
|
|
1053
|
+
}
|
|
1054
|
+
async search(query, options = {}) {
|
|
1055
|
+
const log = getLogger();
|
|
1056
|
+
log.debug({ query, indexSize: this.indexer.getDocumentCount() }, "search: start");
|
|
1057
|
+
if (this.indexer.getDocumentCount() === 0) {
|
|
1058
|
+
log.debug("search: index empty, triggering scan");
|
|
1059
|
+
await this.scan({ ...options, limit: void 0, offset: void 0 });
|
|
1060
|
+
}
|
|
1061
|
+
let results = this.indexer.search(query, {
|
|
1062
|
+
fields: options.fields,
|
|
1063
|
+
limit: (options.limit ?? 50) * 2
|
|
1064
|
+
});
|
|
1065
|
+
if (options.include && options.include !== "all") {
|
|
1066
|
+
results = results.filter((r) => {
|
|
1067
|
+
switch (options.include) {
|
|
1068
|
+
case "conversations":
|
|
1069
|
+
return !r.meta.isSubagent && !r.meta.isTeammate;
|
|
1070
|
+
case "subagents":
|
|
1071
|
+
return r.meta.isSubagent;
|
|
1072
|
+
case "teammates":
|
|
1073
|
+
return r.meta.isTeammate;
|
|
1074
|
+
default:
|
|
1075
|
+
return true;
|
|
1076
|
+
}
|
|
1077
|
+
});
|
|
1078
|
+
}
|
|
1079
|
+
if (options.project) {
|
|
1080
|
+
const lower = options.project.toLowerCase();
|
|
1081
|
+
results = results.filter(
|
|
1082
|
+
(r) => r.meta.projectPath.toLowerCase().includes(lower) || r.meta.projectName.toLowerCase().includes(lower)
|
|
1083
|
+
);
|
|
1084
|
+
}
|
|
1085
|
+
if (options.account) {
|
|
1086
|
+
results = results.filter((r) => r.meta.account === options.account);
|
|
1087
|
+
}
|
|
1088
|
+
if (options.since) {
|
|
1089
|
+
const cutoff = parseSinceCutoff(options.since);
|
|
1090
|
+
results = results.filter((r) => new Date(r.meta.timestamp).getTime() >= cutoff.getTime());
|
|
1091
|
+
}
|
|
1092
|
+
const limit = options.limit ?? 50;
|
|
1093
|
+
const offset = options.offset ?? 0;
|
|
1094
|
+
const sliced = results.slice(offset, offset + limit);
|
|
1095
|
+
log.debug({ query, matched: results.length, returned: sliced.length }, "search: complete");
|
|
1096
|
+
return sliced;
|
|
1097
|
+
}
|
|
1098
|
+
async getConversation(id, _options) {
|
|
1099
|
+
const log = getLogger();
|
|
1100
|
+
const cached = this.conversationLRU.get(id);
|
|
1101
|
+
if (cached) {
|
|
1102
|
+
log.debug({ id }, "getConversation: cache hit");
|
|
1103
|
+
return cached;
|
|
1104
|
+
}
|
|
1105
|
+
const meta = this.metadataCache.get(id) ?? this.sessionIdIndex.get(id);
|
|
1106
|
+
if (!meta) {
|
|
1107
|
+
log.debug({ id }, "getConversation: not found in metadata");
|
|
1108
|
+
return null;
|
|
1109
|
+
}
|
|
1110
|
+
log.debug({ id, filePath: meta.filePath }, "getConversation: cache miss, parsing");
|
|
1111
|
+
try {
|
|
1112
|
+
const conversation = await parseConversation(meta.filePath, meta.account);
|
|
1113
|
+
if (conversation) {
|
|
1114
|
+
this.conversationLRU.set(id, conversation);
|
|
1115
|
+
}
|
|
1116
|
+
return conversation;
|
|
1117
|
+
} catch (err) {
|
|
1118
|
+
log.warn({ id, filePath: meta.filePath, err }, "getConversation: parse failed");
|
|
1119
|
+
return null;
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
// Return one bounded window of a conversation's messages plus the total
|
|
1123
|
+
// message count, so a caller can serve the last page and scroll back without
|
|
1124
|
+
// holding the whole conversation itself.
|
|
1125
|
+
//
|
|
1126
|
+
// The window is `[max(0, beforeIndex - limit), beforeIndex)` in chronological
|
|
1127
|
+
// order; `beforeIndex` defaults to `total` (the newest page). `fromIndex` is
|
|
1128
|
+
// the window's start index, so the caller can derive `has_more_older`
|
|
1129
|
+
// (fromIndex > 0). Returns null when the id can't be resolved/parsed — the
|
|
1130
|
+
// same contract as getConversation.
|
|
1131
|
+
//
|
|
1132
|
+
// Strategy: parse-once-then-slice. This delegates to getConversation, which
|
|
1133
|
+
// parses the full conversation and caches it in conversationLRU, then slices
|
|
1134
|
+
// the window. Message indices are therefore identical to a full
|
|
1135
|
+
// parseConversation() by construction (same parse, same messages array).
|
|
1136
|
+
// Repeated page requests for the same id reuse the single cached parse. The
|
|
1137
|
+
// bounded-memory win (not holding all messages) is deferred — see
|
|
1138
|
+
// docs/plans/2026-06-10-paged-conversation-parse.md.
|
|
1139
|
+
async getConversationPage(id, options) {
|
|
1140
|
+
const conversation = await this.getConversation(id);
|
|
1141
|
+
if (!conversation) return null;
|
|
1142
|
+
const { messages } = conversation;
|
|
1143
|
+
const total = messages.length;
|
|
1144
|
+
const { limit } = options;
|
|
1145
|
+
const beforeIndex = options.beforeIndex ?? total;
|
|
1146
|
+
const fromIndex = Math.max(0, beforeIndex - limit);
|
|
1147
|
+
const window = messages.slice(fromIndex, beforeIndex);
|
|
1148
|
+
return { messages: window, total, fromIndex };
|
|
1149
|
+
}
|
|
1150
|
+
// Parse one JSONL file directly and slice a page window — without any prior
|
|
1151
|
+
// scan() or metadata index. This is the cold-start fast path: a single
|
|
1152
|
+
// conversation can be served from one file parse (~ms) instead of waiting
|
|
1153
|
+
// for a full filesystem scan. The window is the same
|
|
1154
|
+
// `[max(0, beforeIndex - limit), beforeIndex)` slice as getConversationPage,
|
|
1155
|
+
// and the parsed Conversation is returned alongside so the caller can build
|
|
1156
|
+
// the response meta (projectPath, timestamp, messageCount, …) without a
|
|
1157
|
+
// second parse. Returns null when the file can't be parsed. `account` only
|
|
1158
|
+
// feeds the conversation's account field; "default" is the single-profile
|
|
1159
|
+
// fallback, matching refreshFile.
|
|
1160
|
+
async parseSingleFilePage(filePath, account, options) {
|
|
1161
|
+
const conversation = await parseConversation(filePath, account ?? "default");
|
|
1162
|
+
if (!conversation) return null;
|
|
1163
|
+
const { messages } = conversation;
|
|
1164
|
+
const total = messages.length;
|
|
1165
|
+
const { limit } = options;
|
|
1166
|
+
const beforeIndex = options.beforeIndex ?? total;
|
|
1167
|
+
const fromIndex = Math.max(0, beforeIndex - limit);
|
|
1168
|
+
const window = messages.slice(fromIndex, beforeIndex);
|
|
1169
|
+
return { messages: window, total, fromIndex, conversation };
|
|
1170
|
+
}
|
|
1171
|
+
// Re-parse a single JSONL file and update every in-memory index in place —
|
|
1172
|
+
// metadata cache, sessionId index, project set, search index — and evict the
|
|
1173
|
+
// file's parsed conversation from the LRU so the next getConversation()
|
|
1174
|
+
// re-reads it. This lets a long-lived scanner stay current with a file that
|
|
1175
|
+
// grew after the initial scan() without paying for a full rescan.
|
|
1176
|
+
//
|
|
1177
|
+
// `account` defaults to the account already recorded for this file (the id
|
|
1178
|
+
// is the file path), falling back to "default" for a file the scanner has
|
|
1179
|
+
// not seen before. Returns the fresh ConversationMeta, or null when the file
|
|
1180
|
+
// no longer parses (missing/empty) — in which case any prior entry for it is
|
|
1181
|
+
// dropped from all indexes.
|
|
1182
|
+
async refreshFile(filePath, account) {
|
|
1183
|
+
const log = getLogger();
|
|
1184
|
+
const previous = this.metadataCache.get(filePath);
|
|
1185
|
+
const resolvedAccount = account ?? previous?.account ?? "default";
|
|
1186
|
+
let meta = null;
|
|
1187
|
+
try {
|
|
1188
|
+
meta = await parseMeta(filePath, resolvedAccount, this.lastTier);
|
|
1189
|
+
} catch (err) {
|
|
1190
|
+
log.warn({ filePath, err }, "refreshFile: parseMeta threw");
|
|
1191
|
+
meta = null;
|
|
1192
|
+
}
|
|
1193
|
+
const evict = (m) => {
|
|
1194
|
+
if (!m) return;
|
|
1195
|
+
this.conversationLRU.delete(m.id);
|
|
1196
|
+
this.conversationLRU.delete(m.sessionId);
|
|
1197
|
+
};
|
|
1198
|
+
evict(previous);
|
|
1199
|
+
evict(meta);
|
|
1200
|
+
if (!meta || meta.messageCount === 0) {
|
|
1201
|
+
if (previous) {
|
|
1202
|
+
this.metadataCache.delete(previous.id);
|
|
1203
|
+
this.sessionIdIndex.delete(previous.sessionId);
|
|
1204
|
+
this.indexer.removeDocument(previous.id);
|
|
1205
|
+
}
|
|
1206
|
+
log.debug({ filePath }, "refreshFile: dropped (no parseable messages)");
|
|
1207
|
+
return null;
|
|
1208
|
+
}
|
|
1209
|
+
meta.gitBranch = readGitBranch(meta.projectPath);
|
|
1210
|
+
if (previous && previous.sessionId !== meta.sessionId) {
|
|
1211
|
+
this.sessionIdIndex.delete(previous.sessionId);
|
|
1212
|
+
}
|
|
1213
|
+
this.metadataCache.set(meta.id, meta);
|
|
1214
|
+
this.sessionIdIndex.set(meta.sessionId, meta);
|
|
1215
|
+
this.projects.add(meta.projectPath);
|
|
1216
|
+
if (previous) {
|
|
1217
|
+
this.indexer.updateDocument(meta);
|
|
1218
|
+
} else {
|
|
1219
|
+
this.indexer.addDocument(meta);
|
|
1220
|
+
}
|
|
1221
|
+
log.debug(
|
|
1222
|
+
{ filePath, messageCount: meta.messageCount },
|
|
1223
|
+
"refreshFile: updated in-memory indexes"
|
|
1224
|
+
);
|
|
1225
|
+
return meta;
|
|
1226
|
+
}
|
|
1227
|
+
getMetadataCache() {
|
|
1228
|
+
return this.metadataCache;
|
|
1229
|
+
}
|
|
1230
|
+
getProjects() {
|
|
1231
|
+
const normalized = /* @__PURE__ */ new Set();
|
|
1232
|
+
for (const p of this.projects) {
|
|
1233
|
+
normalized.add(p.replace(/\/+$/, ""));
|
|
1234
|
+
}
|
|
1235
|
+
return Array.from(normalized).sort();
|
|
1236
|
+
}
|
|
1237
|
+
async resolveProfiles(profiles) {
|
|
1238
|
+
if (profiles && profiles.length > 0) return profiles;
|
|
1239
|
+
return loadProfiles(DEFAULT_CONFIG_PATH);
|
|
1240
|
+
}
|
|
1241
|
+
transformView(metas, options) {
|
|
1242
|
+
switch (options.view) {
|
|
1243
|
+
case "tree":
|
|
1244
|
+
return this.toTree(metas);
|
|
1245
|
+
case "grouped":
|
|
1246
|
+
return this.toGrouped(metas);
|
|
1247
|
+
default:
|
|
1248
|
+
return metas;
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
toTree(metas) {
|
|
1252
|
+
const parents = [];
|
|
1253
|
+
const subagents = [];
|
|
1254
|
+
for (const meta of metas) {
|
|
1255
|
+
if (meta.isSubagent) {
|
|
1256
|
+
subagents.push(meta);
|
|
1257
|
+
} else {
|
|
1258
|
+
parents.push({ ...meta, subagents: [] });
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
const parentById = new Map(parents.map((p) => [p.id, p]));
|
|
1262
|
+
for (const sub of subagents) {
|
|
1263
|
+
const parent = sub.parentSessionId ? parentById.get(sub.parentSessionId) : void 0;
|
|
1264
|
+
if (parent) {
|
|
1265
|
+
parent.subagents.push(sub);
|
|
1266
|
+
} else {
|
|
1267
|
+
parents.push({ ...sub, subagents: [] });
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
return parents;
|
|
1271
|
+
}
|
|
1272
|
+
toGrouped(metas) {
|
|
1273
|
+
const groups = {};
|
|
1274
|
+
for (const meta of metas) {
|
|
1275
|
+
const key = meta.teamName || "_default";
|
|
1276
|
+
if (!groups[key]) groups[key] = [];
|
|
1277
|
+
groups[key].push(meta);
|
|
1278
|
+
}
|
|
1279
|
+
return groups;
|
|
1280
|
+
}
|
|
1281
|
+
};
|
|
1282
|
+
|
|
1283
|
+
// src/types.ts
|
|
1284
|
+
var VALID_SORT_ORDERS = [
|
|
1285
|
+
"recent",
|
|
1286
|
+
"oldest",
|
|
1287
|
+
"messages-desc",
|
|
1288
|
+
"messages-asc",
|
|
1289
|
+
"alpha"
|
|
1290
|
+
];
|
|
1291
|
+
|
|
1292
|
+
// src/index.ts
|
|
1293
|
+
var defaultScanner;
|
|
1294
|
+
function getDefaultScanner() {
|
|
1295
|
+
if (!defaultScanner) {
|
|
1296
|
+
defaultScanner = new ConversationScanner();
|
|
1297
|
+
}
|
|
1298
|
+
return defaultScanner;
|
|
1299
|
+
}
|
|
1300
|
+
function resetDefaultScanner() {
|
|
1301
|
+
defaultScanner = void 0;
|
|
1302
|
+
}
|
|
1303
|
+
async function scan(options, scanner) {
|
|
1304
|
+
return (scanner ?? getDefaultScanner()).scan(options);
|
|
1305
|
+
}
|
|
1306
|
+
async function search(query, options, scanner) {
|
|
1307
|
+
return (scanner ?? getDefaultScanner()).search(query, options);
|
|
1308
|
+
}
|
|
1309
|
+
async function getConversation(id, options, scanner) {
|
|
1310
|
+
return (scanner ?? getDefaultScanner()).getConversation(id, options);
|
|
1311
|
+
}
|
|
1312
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
1313
|
+
0 && (module.exports = {
|
|
1314
|
+
ConversationScanner,
|
|
1315
|
+
DEFAULT_TIERS,
|
|
1316
|
+
SearchIndexer,
|
|
1317
|
+
VALID_SORT_ORDERS,
|
|
1318
|
+
applyAccountFilter,
|
|
1319
|
+
applyIncludeFilter,
|
|
1320
|
+
applyPagination,
|
|
1321
|
+
applyProjectFilter,
|
|
1322
|
+
applySinceFilter,
|
|
1323
|
+
applySort,
|
|
1324
|
+
cleanSystemTags,
|
|
1325
|
+
createLogger,
|
|
1326
|
+
detectDefaultProfile,
|
|
1327
|
+
getConversation,
|
|
1328
|
+
getLogger,
|
|
1329
|
+
getProjectsDir,
|
|
1330
|
+
loadProfiles,
|
|
1331
|
+
readGitBranch,
|
|
1332
|
+
resetDefaultScanner,
|
|
1333
|
+
resolveConfigDir,
|
|
1334
|
+
resolveTier,
|
|
1335
|
+
saveProfiles,
|
|
1336
|
+
scan,
|
|
1337
|
+
search,
|
|
1338
|
+
setLogger
|
|
1339
|
+
});
|
|
1340
|
+
//# sourceMappingURL=index.cjs.map
|