@openturtle/cli 0.3.2 → 0.3.4
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/README.md +52 -152
- package/RELEASE_RUNBOOK.md +193 -0
- package/dist/commands/collaboration.js +88 -4
- package/dist/commands/local-knowledge.js +129 -0
- package/dist/commands/resources.js +2 -5
- package/dist/core/auth.js +4 -2
- package/dist/core/auto-update.js +30 -2
- package/dist/core/installations.js +41 -0
- package/dist/core/{mcp-config.js → legacy-agent-cleanup.js} +10 -42
- package/dist/core/store.js +1 -20
- package/dist/index.js +39 -85
- package/dist/installers/agents.js +79 -222
- package/dist/installers/legacy-git-cleanup.js +29 -0
- package/dist/local-knowledge/index.js +398 -0
- package/dist/local-knowledge/library.js +328 -0
- package/dist/local-knowledge/root.js +5 -0
- package/package.json +5 -3
- package/skill/openturtle-cli/SKILL.md +81 -0
- package/dist/commands/hook.js +0 -162
- package/dist/installers/git.js +0 -63
- package/dist/mcp/server.js +0 -185
- package/dist/watchers/importers.js +0 -69
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { localKnowledgeStoreRoot } from './root.js';
|
|
5
|
+
const MAX_INDEXED_FILE_BYTES = 20 * 1024 * 1024;
|
|
6
|
+
const MAX_CHUNK_CHARACTERS = 6_000;
|
|
7
|
+
function indexPath(userDataPath = localKnowledgeStoreRoot()) {
|
|
8
|
+
return path.join(userDataPath, 'knowledge', 'index.db');
|
|
9
|
+
}
|
|
10
|
+
async function openIndex(userDataPath) {
|
|
11
|
+
const filePath = indexPath(userDataPath);
|
|
12
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
13
|
+
const importBuiltin = new Function('specifier', 'return import(specifier)');
|
|
14
|
+
const sqlite = await importBuiltin('node:sqlite');
|
|
15
|
+
if (!sqlite.DatabaseSync)
|
|
16
|
+
throw new Error('当前 Node.js 运行时不支持本地知识索引');
|
|
17
|
+
const db = new sqlite.DatabaseSync(filePath);
|
|
18
|
+
db.exec(`
|
|
19
|
+
PRAGMA busy_timeout = 5000;
|
|
20
|
+
PRAGMA journal_mode = WAL;
|
|
21
|
+
PRAGMA synchronous = NORMAL;
|
|
22
|
+
CREATE TABLE IF NOT EXISTS indexed_assets (
|
|
23
|
+
asset_id TEXT PRIMARY KEY,
|
|
24
|
+
content_hash TEXT NOT NULL,
|
|
25
|
+
index_status TEXT NOT NULL,
|
|
26
|
+
indexed_at TEXT NOT NULL
|
|
27
|
+
);
|
|
28
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS knowledge_chunks_fts USING fts5(
|
|
29
|
+
asset_id UNINDEXED,
|
|
30
|
+
chunk_index UNINDEXED,
|
|
31
|
+
line_start UNINDEXED,
|
|
32
|
+
line_end UNINDEXED,
|
|
33
|
+
content,
|
|
34
|
+
tokenize='trigram'
|
|
35
|
+
);
|
|
36
|
+
`);
|
|
37
|
+
return db;
|
|
38
|
+
}
|
|
39
|
+
function isIndexableText(asset) {
|
|
40
|
+
return (asset.mimeType.startsWith('text/') ||
|
|
41
|
+
/\.(?:md|markdown|txt|log|csv|jsonl?|ya?ml|xml|html?|tsx?|jsx?|py|sh|sql|toml|ini|conf)$/i.test(asset.name));
|
|
42
|
+
}
|
|
43
|
+
function expectedIndexStatus(asset) {
|
|
44
|
+
if (asset.syncState === 'missing')
|
|
45
|
+
return 'missing';
|
|
46
|
+
if (!isIndexableText(asset) || asset.sizeBytes > MAX_INDEXED_FILE_BYTES)
|
|
47
|
+
return 'unsupported';
|
|
48
|
+
return 'indexed';
|
|
49
|
+
}
|
|
50
|
+
function indexFingerprint(asset) {
|
|
51
|
+
return crypto
|
|
52
|
+
.createHash('sha256')
|
|
53
|
+
.update(`${asset.contentHash}\n${asset.entry?.title || ''}\n${asset.entry?.kind || ''}\n${asset.entry?.projectId || ''}`)
|
|
54
|
+
.digest('hex');
|
|
55
|
+
}
|
|
56
|
+
function textChunks(content) {
|
|
57
|
+
const lines = content.replace(/\r\n/g, '\n').split('\n');
|
|
58
|
+
const chunks = [];
|
|
59
|
+
let start = 0;
|
|
60
|
+
while (start < lines.length) {
|
|
61
|
+
let end = start;
|
|
62
|
+
let size = 0;
|
|
63
|
+
while (end < lines.length) {
|
|
64
|
+
const nextSize = lines[end].length + 1;
|
|
65
|
+
if (end > start && size + nextSize > MAX_CHUNK_CHARACTERS)
|
|
66
|
+
break;
|
|
67
|
+
size += nextSize;
|
|
68
|
+
end += 1;
|
|
69
|
+
}
|
|
70
|
+
const chunk = lines.slice(start, end).join('\n').trim();
|
|
71
|
+
if (chunk)
|
|
72
|
+
chunks.push({ content: chunk, lineStart: start + 1, lineEnd: end });
|
|
73
|
+
start = end;
|
|
74
|
+
}
|
|
75
|
+
return chunks;
|
|
76
|
+
}
|
|
77
|
+
function prepareIndexStatements(db) {
|
|
78
|
+
return {
|
|
79
|
+
deleteChunks: db.prepare('DELETE FROM knowledge_chunks_fts WHERE asset_id = ?'),
|
|
80
|
+
deleteAsset: db.prepare('DELETE FROM indexed_assets WHERE asset_id = ?'),
|
|
81
|
+
insertChunk: db.prepare('INSERT INTO knowledge_chunks_fts(asset_id, chunk_index, line_start, line_end, content) VALUES (?, ?, ?, ?, ?)'),
|
|
82
|
+
upsertStatus: db.prepare(`INSERT INTO indexed_assets(asset_id, content_hash, index_status, indexed_at)
|
|
83
|
+
VALUES (?, ?, ?, ?)
|
|
84
|
+
ON CONFLICT(asset_id) DO UPDATE SET
|
|
85
|
+
content_hash = excluded.content_hash,
|
|
86
|
+
index_status = excluded.index_status,
|
|
87
|
+
indexed_at = excluded.indexed_at`),
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
function removeIndexedAsset(statements, assetId) {
|
|
91
|
+
statements.deleteChunks.run(assetId);
|
|
92
|
+
statements.deleteAsset.run(assetId);
|
|
93
|
+
}
|
|
94
|
+
function setIndexStatus(statements, asset, status) {
|
|
95
|
+
statements.upsertStatus.run(asset.id, indexFingerprint(asset), status, new Date().toISOString());
|
|
96
|
+
}
|
|
97
|
+
async function indexAsset(asset, existing, statements) {
|
|
98
|
+
if (existing?.contentHash === indexFingerprint(asset) && existing.indexStatus === expectedIndexStatus(asset))
|
|
99
|
+
return;
|
|
100
|
+
removeIndexedAsset(statements, asset.id);
|
|
101
|
+
if (asset.syncState === 'missing') {
|
|
102
|
+
setIndexStatus(statements, asset, 'missing');
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
if (!isIndexableText(asset) || asset.sizeBytes > MAX_INDEXED_FILE_BYTES) {
|
|
106
|
+
setIndexStatus(statements, asset, 'unsupported');
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
try {
|
|
110
|
+
const bytes = await fs.promises.readFile(asset.path);
|
|
111
|
+
if (bytes.includes(0)) {
|
|
112
|
+
setIndexStatus(statements, asset, 'unsupported');
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
const content = bytes.toString('utf8');
|
|
116
|
+
const chunks = textChunks(asset.entry ? `${asset.entry.title}\n\n${content}` : content);
|
|
117
|
+
chunks.forEach((chunk, index) => statements.insertChunk.run(asset.id, index, chunk.lineStart, chunk.lineEnd, chunk.content));
|
|
118
|
+
setIndexStatus(statements, asset, 'indexed');
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
removeIndexedAsset(statements, asset.id);
|
|
122
|
+
setIndexStatus(statements, asset, 'failed');
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
export async function syncLocalKnowledgeIndex(assets, userDataPath) {
|
|
126
|
+
const db = await openIndex(userDataPath);
|
|
127
|
+
try {
|
|
128
|
+
const statements = prepareIndexStatements(db);
|
|
129
|
+
const assetIds = new Set(assets.map((asset) => asset.id));
|
|
130
|
+
const indexed = new Map();
|
|
131
|
+
const staleAssetIds = [];
|
|
132
|
+
for (const row of db.prepare('SELECT asset_id, content_hash, index_status FROM indexed_assets').all()) {
|
|
133
|
+
const assetId = String(row.asset_id || '');
|
|
134
|
+
if (!assetId)
|
|
135
|
+
continue;
|
|
136
|
+
if (!assetIds.has(assetId)) {
|
|
137
|
+
staleAssetIds.push(assetId);
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
indexed.set(assetId, {
|
|
141
|
+
contentHash: String(row.content_hash || ''),
|
|
142
|
+
indexStatus: String(row.index_status || ''),
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
const changedAssets = assets.filter((asset) => {
|
|
146
|
+
const existing = indexed.get(asset.id);
|
|
147
|
+
return existing?.contentHash !== indexFingerprint(asset) || existing.indexStatus !== expectedIndexStatus(asset);
|
|
148
|
+
});
|
|
149
|
+
if (staleAssetIds.length === 0 && changedAssets.length === 0)
|
|
150
|
+
return;
|
|
151
|
+
db.exec('BEGIN IMMEDIATE');
|
|
152
|
+
try {
|
|
153
|
+
for (const assetId of staleAssetIds)
|
|
154
|
+
removeIndexedAsset(statements, assetId);
|
|
155
|
+
for (const asset of changedAssets)
|
|
156
|
+
await indexAsset(asset, indexed.get(asset.id), statements);
|
|
157
|
+
db.exec('COMMIT');
|
|
158
|
+
}
|
|
159
|
+
catch (error) {
|
|
160
|
+
db.exec('ROLLBACK');
|
|
161
|
+
throw error;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
finally {
|
|
165
|
+
db.close();
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
function excerpt(content, query, maxCharacters = 240) {
|
|
169
|
+
const normalized = content.toLocaleLowerCase('zh-CN');
|
|
170
|
+
const index = normalized.indexOf(query.toLocaleLowerCase('zh-CN'));
|
|
171
|
+
if (index < 0 && content.length <= maxCharacters)
|
|
172
|
+
return content;
|
|
173
|
+
const start = Math.max(0, index < 0 ? 0 : index - Math.floor(maxCharacters / 3));
|
|
174
|
+
const value = content
|
|
175
|
+
.slice(start, start + maxCharacters)
|
|
176
|
+
.replace(/\s+/g, ' ')
|
|
177
|
+
.trim();
|
|
178
|
+
return `${start > 0 ? '…' : ''}${value}${start + maxCharacters < content.length ? '…' : ''}`;
|
|
179
|
+
}
|
|
180
|
+
function searchQueries(value, matchMode) {
|
|
181
|
+
const values = Array.isArray(value) ? value : [value];
|
|
182
|
+
const queries = [];
|
|
183
|
+
const seen = new Set();
|
|
184
|
+
for (const candidate of values) {
|
|
185
|
+
const query = candidate.trim();
|
|
186
|
+
const key = query.toLocaleLowerCase('zh-CN');
|
|
187
|
+
if (!query || seen.has(key))
|
|
188
|
+
continue;
|
|
189
|
+
seen.add(key);
|
|
190
|
+
queries.push(query);
|
|
191
|
+
if (queries.length >= 12)
|
|
192
|
+
break;
|
|
193
|
+
}
|
|
194
|
+
return matchMode === 'exact' && queries.length > 1 ? [queries.join(' ')] : queries;
|
|
195
|
+
}
|
|
196
|
+
function escapedLikePattern(value) {
|
|
197
|
+
return `%${value.replace(/([%_\\])/g, '\\$1')}%`;
|
|
198
|
+
}
|
|
199
|
+
function lineNumberAt(content, index) {
|
|
200
|
+
return content.slice(0, Math.max(0, index)).split('\n').length;
|
|
201
|
+
}
|
|
202
|
+
export async function searchLocalKnowledgeIndex(assets, query, limit, userDataPath, matchMode = 'any') {
|
|
203
|
+
await syncLocalKnowledgeIndex(assets, userDataPath);
|
|
204
|
+
const queries = searchQueries(query, matchMode);
|
|
205
|
+
const normalizedQueries = queries.map((item) => item.toLocaleLowerCase('zh-CN'));
|
|
206
|
+
if (queries.length === 0) {
|
|
207
|
+
return assets.slice(0, limit).map((asset) => ({
|
|
208
|
+
assetId: asset.id,
|
|
209
|
+
matchedQueries: [],
|
|
210
|
+
matches: [],
|
|
211
|
+
}));
|
|
212
|
+
}
|
|
213
|
+
const db = await openIndex(userDataPath);
|
|
214
|
+
try {
|
|
215
|
+
const byAsset = new Map();
|
|
216
|
+
const stateFor = (assetId, index) => {
|
|
217
|
+
const existing = byAsset.get(assetId);
|
|
218
|
+
if (existing)
|
|
219
|
+
return existing;
|
|
220
|
+
const created = {
|
|
221
|
+
index,
|
|
222
|
+
labelQueries: new Set(),
|
|
223
|
+
matchedQueries: new Set(),
|
|
224
|
+
matches: [],
|
|
225
|
+
};
|
|
226
|
+
byAsset.set(assetId, created);
|
|
227
|
+
return created;
|
|
228
|
+
};
|
|
229
|
+
const assetIndex = new Map(assets.map((asset, index) => [asset.id, index]));
|
|
230
|
+
for (const [index, asset] of assets.entries()) {
|
|
231
|
+
const label = `${asset.name}\n${asset.relativePath}`.toLocaleLowerCase('zh-CN');
|
|
232
|
+
normalizedQueries.forEach((needle, queryIndex) => {
|
|
233
|
+
if (!label.includes(needle))
|
|
234
|
+
return;
|
|
235
|
+
const state = stateFor(asset.id, index);
|
|
236
|
+
state.matchedQueries.add(queries[queryIndex]);
|
|
237
|
+
state.labelQueries.add(queries[queryIndex]);
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
const visibleAssets = new Set(assets.map((asset) => asset.id));
|
|
241
|
+
const candidateLimit = Math.min(2_000, Math.max(limit * 40, 200));
|
|
242
|
+
const longQueries = normalizedQueries.filter((item) => Array.from(item).length >= 3);
|
|
243
|
+
const shortQueries = normalizedQueries.filter((item) => Array.from(item).length < 3);
|
|
244
|
+
const rows = [];
|
|
245
|
+
if (longQueries.length > 0) {
|
|
246
|
+
const expression = longQueries.map((item) => `"${item.replaceAll('"', '""')}"`).join(' OR ');
|
|
247
|
+
rows.push(...db
|
|
248
|
+
.prepare(`SELECT asset_id, line_start, line_end, content
|
|
249
|
+
FROM knowledge_chunks_fts
|
|
250
|
+
WHERE knowledge_chunks_fts MATCH ?
|
|
251
|
+
ORDER BY bm25(knowledge_chunks_fts)
|
|
252
|
+
LIMIT ?`)
|
|
253
|
+
.all(expression, candidateLimit));
|
|
254
|
+
}
|
|
255
|
+
const globallySearchShortQueries = shortQueries.length > 0 && (matchMode !== 'all' || longQueries.length === 0);
|
|
256
|
+
if (globallySearchShortQueries) {
|
|
257
|
+
const predicates = shortQueries.map(() => "lower(content) LIKE ? ESCAPE '\\'").join(' OR ');
|
|
258
|
+
rows.push(...db
|
|
259
|
+
.prepare(`SELECT asset_id, line_start, line_end, content
|
|
260
|
+
FROM knowledge_chunks_fts
|
|
261
|
+
WHERE ${predicates}
|
|
262
|
+
LIMIT ?`)
|
|
263
|
+
.all(...shortQueries.map(escapedLikePattern), candidateLimit));
|
|
264
|
+
}
|
|
265
|
+
const seenMatches = new Set();
|
|
266
|
+
for (const row of rows) {
|
|
267
|
+
const assetId = String(row.asset_id || '');
|
|
268
|
+
if (!visibleAssets.has(assetId))
|
|
269
|
+
continue;
|
|
270
|
+
const content = String(row.content || '');
|
|
271
|
+
const normalizedContent = content.toLocaleLowerCase('zh-CN');
|
|
272
|
+
const matchedIndexes = normalizedQueries.flatMap((needle, index) => normalizedContent.includes(needle) ? [index] : []);
|
|
273
|
+
if (matchedIndexes.length === 0)
|
|
274
|
+
continue;
|
|
275
|
+
const state = stateFor(assetId, assetIndex.get(assetId) || 0);
|
|
276
|
+
for (const queryIndex of matchedIndexes) {
|
|
277
|
+
const matchedQuery = queries[queryIndex];
|
|
278
|
+
state.matchedQueries.add(matchedQuery);
|
|
279
|
+
const matchKey = `${assetId}:${row.line_start}:${row.line_end}:${matchedQuery}`;
|
|
280
|
+
if (state.matches.length >= 3 || seenMatches.has(matchKey))
|
|
281
|
+
continue;
|
|
282
|
+
seenMatches.add(matchKey);
|
|
283
|
+
state.matches.push({
|
|
284
|
+
query: matchedQuery,
|
|
285
|
+
excerpt: excerpt(content, matchedQuery),
|
|
286
|
+
lineStart: Number(row.line_start || 0),
|
|
287
|
+
lineEnd: Number(row.line_end || 0),
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
if (matchMode === 'all' && longQueries.length > 0 && shortQueries.length > 0) {
|
|
292
|
+
const longOriginalQueries = queries.filter((_, index) => Array.from(normalizedQueries[index]).length >= 3);
|
|
293
|
+
for (const [assetId, state] of byAsset) {
|
|
294
|
+
if (!longOriginalQueries.every((item) => state.matchedQueries.has(item)))
|
|
295
|
+
continue;
|
|
296
|
+
const asset = assets[assetIndex.get(assetId) ?? -1];
|
|
297
|
+
if (!asset || expectedIndexStatus(asset) !== 'indexed')
|
|
298
|
+
continue;
|
|
299
|
+
let content;
|
|
300
|
+
try {
|
|
301
|
+
content = fs.readFileSync(asset.path, 'utf8');
|
|
302
|
+
}
|
|
303
|
+
catch {
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
const normalizedContent = content.toLocaleLowerCase('zh-CN');
|
|
307
|
+
normalizedQueries.forEach((needle, queryIndex) => {
|
|
308
|
+
if (Array.from(needle).length >= 3 || !normalizedContent.includes(needle))
|
|
309
|
+
return;
|
|
310
|
+
const matchedQuery = queries[queryIndex];
|
|
311
|
+
state.matchedQueries.add(matchedQuery);
|
|
312
|
+
if (state.matches.length >= 3)
|
|
313
|
+
return;
|
|
314
|
+
const matchIndex = normalizedContent.indexOf(needle);
|
|
315
|
+
const lineNumber = lineNumberAt(content, matchIndex);
|
|
316
|
+
state.matches.push({
|
|
317
|
+
query: matchedQuery,
|
|
318
|
+
excerpt: excerpt(content, matchedQuery),
|
|
319
|
+
lineStart: lineNumber,
|
|
320
|
+
lineEnd: lineNumber,
|
|
321
|
+
});
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
return [...byAsset.entries()]
|
|
326
|
+
.filter(([, state]) => matchMode === 'all' ? state.matchedQueries.size === queries.length : state.matchedQueries.size > 0)
|
|
327
|
+
.sort(([, left], [, right]) => {
|
|
328
|
+
const leftScore = left.matchedQueries.size * 100 + left.labelQueries.size * 10 + left.matches.length;
|
|
329
|
+
const rightScore = right.matchedQueries.size * 100 + right.labelQueries.size * 10 + right.matches.length;
|
|
330
|
+
return rightScore - leftScore || left.index - right.index;
|
|
331
|
+
})
|
|
332
|
+
.slice(0, limit)
|
|
333
|
+
.map(([assetId, state]) => ({
|
|
334
|
+
assetId,
|
|
335
|
+
matchedQueries: [...state.matchedQueries],
|
|
336
|
+
matches: state.matches,
|
|
337
|
+
}));
|
|
338
|
+
}
|
|
339
|
+
finally {
|
|
340
|
+
db.close();
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
export async function readLocalKnowledgeIndexedContent(asset, truncate, userDataPath) {
|
|
344
|
+
const db = await openIndex(userDataPath);
|
|
345
|
+
try {
|
|
346
|
+
const statements = prepareIndexStatements(db);
|
|
347
|
+
const existingRow = db
|
|
348
|
+
.prepare('SELECT content_hash, index_status FROM indexed_assets WHERE asset_id = ?')
|
|
349
|
+
.get(asset.id);
|
|
350
|
+
const existing = existingRow
|
|
351
|
+
? {
|
|
352
|
+
contentHash: String(existingRow.content_hash || ''),
|
|
353
|
+
indexStatus: String(existingRow.index_status || ''),
|
|
354
|
+
}
|
|
355
|
+
: undefined;
|
|
356
|
+
if (existing?.contentHash !== indexFingerprint(asset) || existing.indexStatus !== expectedIndexStatus(asset)) {
|
|
357
|
+
db.exec('BEGIN IMMEDIATE');
|
|
358
|
+
try {
|
|
359
|
+
await indexAsset(asset, existing, statements);
|
|
360
|
+
db.exec('COMMIT');
|
|
361
|
+
}
|
|
362
|
+
catch (error) {
|
|
363
|
+
db.exec('ROLLBACK');
|
|
364
|
+
throw error;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
const statusRow = db.prepare('SELECT index_status FROM indexed_assets WHERE asset_id = ?').get(asset.id);
|
|
368
|
+
const indexStatus = String(statusRow?.index_status || 'failed');
|
|
369
|
+
if (indexStatus !== 'indexed') {
|
|
370
|
+
return { content: null, contentAvailable: false, truncated: false, indexStatus };
|
|
371
|
+
}
|
|
372
|
+
if (asset.entry) {
|
|
373
|
+
const content = await fs.promises.readFile(asset.path, 'utf8');
|
|
374
|
+
return {
|
|
375
|
+
content: content.slice(0, truncate),
|
|
376
|
+
contentAvailable: true,
|
|
377
|
+
truncated: content.length > truncate,
|
|
378
|
+
indexStatus,
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
const rows = db
|
|
382
|
+
.prepare('SELECT content FROM knowledge_chunks_fts WHERE asset_id = ? ORDER BY CAST(chunk_index AS INTEGER)')
|
|
383
|
+
.all(asset.id);
|
|
384
|
+
const content = rows
|
|
385
|
+
.map((row) => String(row.content || ''))
|
|
386
|
+
.filter(Boolean)
|
|
387
|
+
.join('\n\n');
|
|
388
|
+
return {
|
|
389
|
+
content: content.slice(0, truncate),
|
|
390
|
+
contentAvailable: true,
|
|
391
|
+
truncated: content.length > truncate,
|
|
392
|
+
indexStatus,
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
finally {
|
|
396
|
+
db.close();
|
|
397
|
+
}
|
|
398
|
+
}
|