@autorag/librarian 2.10.3 → 2.10.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 +23 -3
- package/dist/agent/agent.d.ts +1 -1
- package/dist/agent/answer-guidelines.d.ts +17 -0
- package/dist/cli/commands/types.d.ts +2 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +897 -589
- package/dist/core.js +1613 -1324
- package/dist/datasource/sidecar/descriptors.d.ts +1 -0
- package/dist/datasource/skills/lazykatok/skill.d.ts +13 -0
- package/dist/everything/client.d.ts +5 -0
- package/dist/filesystem/path-exclusions.d.ts +2 -0
- package/dist/fsearch/client.d.ts +3 -2
- package/dist/fsearch/walk.d.ts +2 -0
- package/dist/index.js +1614 -1325
- package/dist/mcp/index.js +1614 -1325
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -2109,6 +2109,25 @@ function errorMessage(error) {
|
|
|
2109
2109
|
var MANIFEST_FILENAME = "manifest.json";
|
|
2110
2110
|
var init_bundle = () => {};
|
|
2111
2111
|
|
|
2112
|
+
// src/filesystem/path-exclusions.ts
|
|
2113
|
+
import { posix as posix2, win32 as win322 } from "node:path";
|
|
2114
|
+
function createPathExclusionMatcher(excludedPaths, platform = process.platform) {
|
|
2115
|
+
const paths = platform === "win32" ? win322 : posix2;
|
|
2116
|
+
const normalize = (path) => {
|
|
2117
|
+
const absolute = paths.resolve(path);
|
|
2118
|
+
return platform === "win32" ? absolute.toLowerCase() : absolute;
|
|
2119
|
+
};
|
|
2120
|
+
const excluded = excludedPaths.map((path) => {
|
|
2121
|
+
const absolute = normalize(path);
|
|
2122
|
+
return { absolute, prefix: absolute.endsWith(paths.sep) ? absolute : `${absolute}${paths.sep}` };
|
|
2123
|
+
});
|
|
2124
|
+
return (path) => {
|
|
2125
|
+
const absolute = normalize(path);
|
|
2126
|
+
return excluded.some((entry) => absolute === entry.absolute || absolute.startsWith(entry.prefix));
|
|
2127
|
+
};
|
|
2128
|
+
}
|
|
2129
|
+
var init_path_exclusions = () => {};
|
|
2130
|
+
|
|
2112
2131
|
// src/everything/client.ts
|
|
2113
2132
|
import { spawn as spawn2 } from "node:child_process";
|
|
2114
2133
|
import { createHash as createHash4 } from "node:crypto";
|
|
@@ -2226,6 +2245,7 @@ class EverythingClient {
|
|
|
2226
2245
|
run;
|
|
2227
2246
|
launch;
|
|
2228
2247
|
stateDir;
|
|
2248
|
+
isExcludedPath;
|
|
2229
2249
|
binaries;
|
|
2230
2250
|
operation = Promise.resolve();
|
|
2231
2251
|
constructor(options) {
|
|
@@ -2235,6 +2255,7 @@ class EverythingClient {
|
|
|
2235
2255
|
this.run = options.run ?? runProcess;
|
|
2236
2256
|
this.launch = options.launch ?? launchDetached;
|
|
2237
2257
|
this.stateDir = join6(options.root, ".autorag", "everything");
|
|
2258
|
+
this.isExcludedPath = createPathExclusionMatcher(options.excludeFolders ?? [], "win32");
|
|
2238
2259
|
}
|
|
2239
2260
|
isSupported() {
|
|
2240
2261
|
return this.platform === "win32";
|
|
@@ -2254,6 +2275,9 @@ class EverythingClient {
|
|
|
2254
2275
|
}
|
|
2255
2276
|
const es = { ok: true, esPath: binaries.esPath };
|
|
2256
2277
|
const timeoutMs = this.options.timeoutMs ?? DEFAULT_TIMEOUT_MS2;
|
|
2278
|
+
if ((this.options.excludeFolders?.length ?? 0) > 0) {
|
|
2279
|
+
return this.searchWithExclusions(es.esPath, request, timeoutMs);
|
|
2280
|
+
}
|
|
2257
2281
|
const args = buildEverythingSearchArgs(this.instanceName, request, timeoutMs);
|
|
2258
2282
|
const result = await this.run(es.esPath, args, timeoutMs + 5000);
|
|
2259
2283
|
if (result.code !== 0)
|
|
@@ -2265,6 +2289,50 @@ class EverythingClient {
|
|
|
2265
2289
|
}
|
|
2266
2290
|
});
|
|
2267
2291
|
}
|
|
2292
|
+
async searchWithExclusions(esPath, request, timeoutMs) {
|
|
2293
|
+
const deadline = Date.now() + timeoutMs;
|
|
2294
|
+
const offset = request.offset ?? 0;
|
|
2295
|
+
const maxResults = request.maxResults ?? DEFAULT_MAX_RESULTS;
|
|
2296
|
+
const results = [];
|
|
2297
|
+
let skipped = 0;
|
|
2298
|
+
let nativeOffset = 0;
|
|
2299
|
+
const timedOut = () => ({
|
|
2300
|
+
ok: false,
|
|
2301
|
+
reason: "search-failed",
|
|
2302
|
+
message: `Everything search exceeded ${timeoutMs}ms while filtering excluded paths; narrow the query or increase the Everything timeout.`
|
|
2303
|
+
});
|
|
2304
|
+
for (;; ) {
|
|
2305
|
+
const remainingMs = deadline - Date.now();
|
|
2306
|
+
if (remainingMs <= 0)
|
|
2307
|
+
return timedOut();
|
|
2308
|
+
const args = buildEverythingSearchArgs(this.instanceName, { ...request, offset: nativeOffset, maxResults: MAX_FILTER_CANDIDATES }, remainingMs);
|
|
2309
|
+
const response = await this.run(esPath, args, remainingMs + 5000);
|
|
2310
|
+
if (response.code !== 0)
|
|
2311
|
+
return this.failure("search-failed", esPath, args, response);
|
|
2312
|
+
if (Date.now() >= deadline)
|
|
2313
|
+
return timedOut();
|
|
2314
|
+
let entries;
|
|
2315
|
+
try {
|
|
2316
|
+
entries = parseEverythingJson(response.stdout);
|
|
2317
|
+
} catch (error) {
|
|
2318
|
+
return { ok: false, reason: "search-failed", message: error.message };
|
|
2319
|
+
}
|
|
2320
|
+
for (const entry of entries) {
|
|
2321
|
+
if (this.isExcludedPath(entry.path))
|
|
2322
|
+
continue;
|
|
2323
|
+
if (skipped < offset) {
|
|
2324
|
+
skipped += 1;
|
|
2325
|
+
continue;
|
|
2326
|
+
}
|
|
2327
|
+
results.push(entry);
|
|
2328
|
+
if (results.length >= maxResults)
|
|
2329
|
+
return { ok: true, results };
|
|
2330
|
+
}
|
|
2331
|
+
if (entries.length < MAX_FILTER_CANDIDATES)
|
|
2332
|
+
return { ok: true, results };
|
|
2333
|
+
nativeOffset += entries.length;
|
|
2334
|
+
}
|
|
2335
|
+
}
|
|
2268
2336
|
index() {
|
|
2269
2337
|
return this.serialize(async () => {
|
|
2270
2338
|
const es = await this.ensureRunning(true);
|
|
@@ -2338,7 +2406,8 @@ class EverythingClient {
|
|
|
2338
2406
|
excludeFolders: [
|
|
2339
2407
|
...new Set([
|
|
2340
2408
|
join6(this.options.root, ".autorag"),
|
|
2341
|
-
...this.options.folders.map((folder) => join6(folder, ".autorag"))
|
|
2409
|
+
...this.options.folders.map((folder) => join6(folder, ".autorag")),
|
|
2410
|
+
...this.options.excludeFolders ?? []
|
|
2342
2411
|
])
|
|
2343
2412
|
]
|
|
2344
2413
|
}));
|
|
@@ -2432,8 +2501,9 @@ function launchDetached(command, args) {
|
|
|
2432
2501
|
child.on("error", () => {});
|
|
2433
2502
|
child.unref();
|
|
2434
2503
|
}
|
|
2435
|
-
var DEFAULT_TIMEOUT_MS2 = 30000, DEFAULT_STARTUP_TIMEOUT_MS = 20000, DEFAULT_INDEX_TIMEOUT_MS, DEFAULT_POLL_INTERVAL_MS = 250, DEFAULT_MAX_RESULTS = 100, MAX_OUTPUT_BYTES, VERSION_PATTERN;
|
|
2504
|
+
var DEFAULT_TIMEOUT_MS2 = 30000, DEFAULT_STARTUP_TIMEOUT_MS = 20000, DEFAULT_INDEX_TIMEOUT_MS, DEFAULT_POLL_INTERVAL_MS = 250, DEFAULT_MAX_RESULTS = 100, MAX_FILTER_CANDIDATES = 1000, MAX_OUTPUT_BYTES, VERSION_PATTERN;
|
|
2436
2505
|
var init_client = __esm(() => {
|
|
2506
|
+
init_path_exclusions();
|
|
2437
2507
|
init_bundle();
|
|
2438
2508
|
DEFAULT_INDEX_TIMEOUT_MS = 10 * 60000;
|
|
2439
2509
|
MAX_OUTPUT_BYTES = 8 * 1024 * 1024;
|
|
@@ -2453,6 +2523,7 @@ function compare(a, b) {
|
|
|
2453
2523
|
return a < b ? -1 : a > b ? 1 : 0;
|
|
2454
2524
|
}
|
|
2455
2525
|
async function walkFileSearch(folders, request, options = {}) {
|
|
2526
|
+
const isExcluded = createPathExclusionMatcher(options.excludeFolders ?? []);
|
|
2456
2527
|
const maxVisited = options.maxVisited ?? DEFAULT_MAX_VISITED;
|
|
2457
2528
|
const maxMatches = options.maxMatches ?? DEFAULT_MAX_MATCHES;
|
|
2458
2529
|
const deadline = Date.now() + (options.deadlineMs ?? DEFAULT_DEADLINE_MS);
|
|
@@ -2475,6 +2546,8 @@ async function walkFileSearch(folders, request, options = {}) {
|
|
|
2475
2546
|
outer:
|
|
2476
2547
|
while (pending.length > 0) {
|
|
2477
2548
|
const dir = pending.pop();
|
|
2549
|
+
if (isExcluded(dir))
|
|
2550
|
+
continue;
|
|
2478
2551
|
if (Date.now() >= deadline) {
|
|
2479
2552
|
truncated = true;
|
|
2480
2553
|
break;
|
|
@@ -2492,6 +2565,8 @@ async function walkFileSearch(folders, request, options = {}) {
|
|
|
2492
2565
|
}
|
|
2493
2566
|
visited += 1;
|
|
2494
2567
|
const path = join7(dir, dirent.name);
|
|
2568
|
+
if (isExcluded(path))
|
|
2569
|
+
continue;
|
|
2495
2570
|
if (dirent.isDirectory()) {
|
|
2496
2571
|
if (dirent.name === ".autorag")
|
|
2497
2572
|
continue;
|
|
@@ -2526,6 +2601,8 @@ async function walkFileSearch(folders, request, options = {}) {
|
|
|
2526
2601
|
}
|
|
2527
2602
|
}
|
|
2528
2603
|
for (const folder of folders) {
|
|
2604
|
+
if (isExcluded(folder))
|
|
2605
|
+
continue;
|
|
2529
2606
|
if (!underPath(folder) || !matches(folder))
|
|
2530
2607
|
continue;
|
|
2531
2608
|
if (request.kind === "files")
|
|
@@ -2552,6 +2629,7 @@ async function walkFileSearch(folders, request, options = {}) {
|
|
|
2552
2629
|
}
|
|
2553
2630
|
var DEFAULT_MAX_VISITED = 500000, DEFAULT_DEADLINE_MS = 20000, DEFAULT_MAX_MATCHES = 1e4, SORT_ORDERS;
|
|
2554
2631
|
var init_walk = __esm(() => {
|
|
2632
|
+
init_path_exclusions();
|
|
2555
2633
|
SORT_ORDERS = {
|
|
2556
2634
|
"name-ascending": (a, b) => compare(a.name, b.name),
|
|
2557
2635
|
"name-descending": (a, b) => compare(b.name, a.name),
|
|
@@ -2661,6 +2739,7 @@ class FSearchClient {
|
|
|
2661
2739
|
launch;
|
|
2662
2740
|
isAlive;
|
|
2663
2741
|
kill;
|
|
2742
|
+
isExcludedPath;
|
|
2664
2743
|
stateDir;
|
|
2665
2744
|
dbPath;
|
|
2666
2745
|
pidPath;
|
|
@@ -2673,6 +2752,7 @@ class FSearchClient {
|
|
|
2673
2752
|
this.launch = options.launch ?? launchDetached2;
|
|
2674
2753
|
this.isAlive = options.isProcessAlive ?? defaultIsProcessAlive;
|
|
2675
2754
|
this.kill = options.killProcess ?? ((pid) => process.kill(pid, "SIGTERM"));
|
|
2755
|
+
this.isExcludedPath = createPathExclusionMatcher(options.excludeFolders ?? []);
|
|
2676
2756
|
this.stateDir = join8(options.root, ".autorag", "fsearch");
|
|
2677
2757
|
this.dbPath = join8(this.stateDir, "fsearch.db");
|
|
2678
2758
|
this.pidPath = join8(this.stateDir, "watch.pid");
|
|
@@ -2690,6 +2770,7 @@ class FSearchClient {
|
|
|
2690
2770
|
if (unavailable !== undefined || !binary.ok) {
|
|
2691
2771
|
try {
|
|
2692
2772
|
const walked = await walkFileSearch(this.options.folders, request, {
|
|
2773
|
+
excludeFolders: this.options.excludeFolders,
|
|
2693
2774
|
maxVisited: this.options.walkMaxVisited,
|
|
2694
2775
|
deadlineMs: this.options.walkDeadlineMs
|
|
2695
2776
|
});
|
|
@@ -2704,7 +2785,9 @@ class FSearchClient {
|
|
|
2704
2785
|
return { ok: false, reason: "search-failed", message: error.message };
|
|
2705
2786
|
}
|
|
2706
2787
|
}
|
|
2707
|
-
const
|
|
2788
|
+
const hasExclusions = (this.options.excludeFolders?.length ?? 0) > 0;
|
|
2789
|
+
const nativeRequest = hasExclusions ? { ...request, offset: 0, maxResults: MAX_RESULTS_CAP } : request;
|
|
2790
|
+
const args = buildFSearchSearchArgs(this.dbPath, this.socketPath(), nativeRequest);
|
|
2708
2791
|
const result = await this.run(binary.path, args, this.options.timeoutMs ?? DEFAULT_TIMEOUT_MS3);
|
|
2709
2792
|
if (result.code !== 0)
|
|
2710
2793
|
return this.failure("search-failed", binary.path, args, result);
|
|
@@ -2715,6 +2798,10 @@ class FSearchClient {
|
|
|
2715
2798
|
return { ok: false, reason: "search-failed", message: error.message };
|
|
2716
2799
|
}
|
|
2717
2800
|
let entries = parsed.entries;
|
|
2801
|
+
if (hasExclusions) {
|
|
2802
|
+
const isExcluded = this.isExcludedPath;
|
|
2803
|
+
entries = entries.filter((entry) => !isExcluded(entry.path));
|
|
2804
|
+
}
|
|
2718
2805
|
if (request.path !== undefined) {
|
|
2719
2806
|
const prefix = request.path.endsWith(sep3) ? request.path : `${request.path}${sep3}`;
|
|
2720
2807
|
entries = entries.filter((entry) => entry.path === request.path || entry.path.startsWith(prefix));
|
|
@@ -2724,7 +2811,16 @@ class FSearchClient {
|
|
|
2724
2811
|
const maxResults = Math.min(request.maxResults ?? DEFAULT_MAX_RESULTS2, MAX_RESULTS_CAP);
|
|
2725
2812
|
if (entries.length > maxResults)
|
|
2726
2813
|
entries = entries.slice(0, maxResults);
|
|
2727
|
-
return {
|
|
2814
|
+
return {
|
|
2815
|
+
ok: true,
|
|
2816
|
+
backend: "fsearch-cli",
|
|
2817
|
+
results: entries,
|
|
2818
|
+
total: parsed.total,
|
|
2819
|
+
...hasExclusions && parsed.total !== undefined && parsed.total > parsed.entries.length && entries.length < maxResults ? {
|
|
2820
|
+
truncated: true,
|
|
2821
|
+
note: `FSearch checked ${parsed.entries.length} of ${parsed.total} native matches before exclusions (candidate limit ${MAX_RESULTS_CAP}); results may be incomplete. Narrow the query.`
|
|
2822
|
+
} : {}
|
|
2823
|
+
};
|
|
2728
2824
|
});
|
|
2729
2825
|
}
|
|
2730
2826
|
index() {
|
|
@@ -2918,6 +3014,7 @@ function defaultIsProcessAlive(pid) {
|
|
|
2918
3014
|
}
|
|
2919
3015
|
var DEFAULT_TIMEOUT_MS3 = 30000, DEFAULT_STARTUP_TIMEOUT_MS2 = 1e4, STOP_TIMEOUT_MS = 2000, DEFAULT_INDEX_TIMEOUT_MS2, DEFAULT_POLL_INTERVAL_MS2 = 250, DEFAULT_MAX_RESULTS2 = 100, MAX_RESULTS_CAP = 1000, MAX_OUTPUT_BYTES2, SORT_KEYS;
|
|
2920
3016
|
var init_client2 = __esm(() => {
|
|
3017
|
+
init_path_exclusions();
|
|
2921
3018
|
init_walk();
|
|
2922
3019
|
DEFAULT_INDEX_TIMEOUT_MS2 = 10 * 60000;
|
|
2923
3020
|
MAX_OUTPUT_BYTES2 = 8 * 1024 * 1024;
|
|
@@ -9186,12 +9283,43 @@ function digitsEndAt(text, from) {
|
|
|
9186
9283
|
end += 1;
|
|
9187
9284
|
return end;
|
|
9188
9285
|
}
|
|
9189
|
-
function continuesAsProse(answer, from) {
|
|
9190
|
-
|
|
9286
|
+
function continuesAsProse(answer, openIndex, from) {
|
|
9287
|
+
let cursor = from;
|
|
9288
|
+
let leadingEmphasis = 0;
|
|
9289
|
+
while (openIndex - 1 - leadingEmphasis >= 0 && (answer[openIndex - 1 - leadingEmphasis] === "*" || answer[openIndex - 1 - leadingEmphasis] === "_")) {
|
|
9290
|
+
leadingEmphasis += 1;
|
|
9291
|
+
}
|
|
9292
|
+
if (leadingEmphasis > 0) {
|
|
9293
|
+
let trailingEmphasis = 0;
|
|
9294
|
+
while (cursor + trailingEmphasis < answer.length && trailingEmphasis < leadingEmphasis && answer[cursor + trailingEmphasis] === answer[openIndex - 1 - trailingEmphasis]) {
|
|
9295
|
+
trailingEmphasis += 1;
|
|
9296
|
+
}
|
|
9297
|
+
if (trailingEmphasis === leadingEmphasis)
|
|
9298
|
+
cursor += trailingEmphasis;
|
|
9299
|
+
}
|
|
9300
|
+
const tokenStart = openIndex - leadingEmphasis;
|
|
9301
|
+
const linePrefix = answer.slice(answer.lastIndexOf(`
|
|
9302
|
+
`, tokenStart - 1) + 1, tokenStart);
|
|
9303
|
+
const atHeadingStart = /^[ \t>*-]*$/u.test(linePrefix);
|
|
9304
|
+
if (atHeadingStart && answer[cursor] === ":")
|
|
9305
|
+
cursor += 1;
|
|
9306
|
+
if (answer[cursor] !== " " && answer[cursor] !== " ")
|
|
9191
9307
|
return false;
|
|
9192
|
-
|
|
9308
|
+
cursor += 1;
|
|
9309
|
+
while (cursor < answer.length && (answer[cursor] === "*" || answer[cursor] === "_" || answer[cursor] === '"' || answer[cursor] === "'" || answer[cursor] === "“" || answer[cursor] === "‘")) {
|
|
9310
|
+
cursor += 1;
|
|
9311
|
+
}
|
|
9312
|
+
const next = answer.codePointAt(cursor);
|
|
9193
9313
|
return next !== undefined && /[\p{L}\p{N}]/u.test(String.fromCodePoint(next));
|
|
9194
9314
|
}
|
|
9315
|
+
function isLegalPinpoint(answer, openIndex, afterCloseIndex) {
|
|
9316
|
+
if (PINPOINT_RANGE_AHEAD.test(answer.slice(afterCloseIndex, afterCloseIndex + 16)))
|
|
9317
|
+
return true;
|
|
9318
|
+
if (PINPOINT_RANGE_BEHIND.test(answer.slice(Math.max(0, openIndex - 16), openIndex)))
|
|
9319
|
+
return true;
|
|
9320
|
+
const prefixWindow = answer.slice(Math.max(0, openIndex - 120), openIndex);
|
|
9321
|
+
return PARA_PINPOINT_PREFIX.test(prefixWindow) || CLAUSE_START_AT_PREFIX.test(prefixWindow) || CASE_CITATION_AT_PREFIX.test(prefixWindow);
|
|
9322
|
+
}
|
|
9195
9323
|
function evidenceMarkerAt(answer, index) {
|
|
9196
9324
|
const ids = [];
|
|
9197
9325
|
let cursor = index + 1;
|
|
@@ -9257,7 +9385,7 @@ function answerMarkers(answer, isFile = () => false) {
|
|
|
9257
9385
|
}
|
|
9258
9386
|
if (char === "[") {
|
|
9259
9387
|
const digitsEnd = digitsEndAt(answer, index + 1);
|
|
9260
|
-
if (digitsEnd > index + 1 && answer[digitsEnd] === "]" && answer[digitsEnd + 1] !== "(" && !continuesAsProse(answer, digitsEnd + 1)) {
|
|
9388
|
+
if (digitsEnd > index + 1 && answer[digitsEnd] === "]" && answer[digitsEnd + 1] !== "(" && !continuesAsProse(answer, index, digitsEnd + 1) && !isLegalPinpoint(answer, index, digitsEnd + 1)) {
|
|
9261
9389
|
markers.push({
|
|
9262
9390
|
kind: "number",
|
|
9263
9391
|
start: leadingStart(index),
|
|
@@ -9329,7 +9457,14 @@ function assertResultsMappingOneToOne(label, results, mapping) {
|
|
|
9329
9457
|
].filter((note) => note !== undefined);
|
|
9330
9458
|
throw new Error(`${label}: result numbers and mapping numbers must be one-to-one, but results contain ${formatCitationList(resultNumbers)} ` + `and mapping contains ${formatCitationList(mappingNumbers)}` + (duplicateNotes.length > 0 ? ` (${duplicateNotes.join("; ")})` : "") + ". Give every result a unique number with exactly one mapping entry carrying the same number and no mapping entry without a result.");
|
|
9331
9459
|
}
|
|
9332
|
-
var MAX_FILE_MARKER_CLOSERS = 8, FILE_MARKER_PREFIX = "[file:";
|
|
9460
|
+
var MAX_FILE_MARKER_CLOSERS = 8, FILE_MARKER_PREFIX = "[file:", PARA_PINPOINT_PREFIX, CLAUSE_START_AT_PREFIX, CASE_CITATION_AT_PREFIX, PINPOINT_RANGE_AHEAD, PINPOINT_RANGE_BEHIND;
|
|
9461
|
+
var init_citations = __esm(() => {
|
|
9462
|
+
PARA_PINPOINT_PREFIX = /(?:^|[^\p{L}\p{N}])(?:paras?\.?|paragraphs?)(?:[ \t\u00a0]+\[\d+\][ \t\u00a0]*(?:[-–—]|,(?:[ \t\u00a0]*(?:and|or)\b)?|and\b|or\b))*[ \t\u00a0]*$/iu;
|
|
9463
|
+
CLAUSE_START_AT_PREFIX = /(?:^|[\n.!?؛;:()]|[>*-])[ \t\u00a0]*At(?:[ \t\u00a0]+\[\d+\][ \t\u00a0]*(?:[-–—]|,(?:[ \t\u00a0]*(?:and|or)\b)?|and\b|or\b))*[ \t\u00a0]+$/u;
|
|
9464
|
+
CASE_CITATION_AT_PREFIX = /(?:\[\d{4}\][^\n.;]*?|\b(?:v|vs\.?)\s+[^\n.;]{1,60}?|\bs(?:ection)?[ \t\u00a0]+\d+[A-Za-z]?(?:\(\d+\))?[^\n.;]{0,30}?)[ \t\u00a0]+at(?:[ \t\u00a0]+\[\d+\][ \t\u00a0]*(?:[-–—]|,(?:[ \t\u00a0]*(?:and|or)\b)?|and\b|or\b))*[ \t\u00a0]+$/iu;
|
|
9465
|
+
PINPOINT_RANGE_AHEAD = /^[ \t\u00a0]*[-–—][ \t\u00a0]*\[\d+\]/u;
|
|
9466
|
+
PINPOINT_RANGE_BEHIND = /\[\d+\][ \t\u00a0]*[-–—][ \t\u00a0]*$/u;
|
|
9467
|
+
});
|
|
9333
9468
|
|
|
9334
9469
|
// src/agent/evidence-ledger.ts
|
|
9335
9470
|
import { createHash as createHash10 } from "node:crypto";
|
|
@@ -9613,6 +9748,7 @@ function deriveResultsFromAnswer(text, ledger, options) {
|
|
|
9613
9748
|
}
|
|
9614
9749
|
var MAX_EXCERPT_CHARS = 400, MAX_CLAIM_CHARS = 280, CLAIM_LOOKBACK_CHARS = 600, SENTENCE_END, LEADING_LIST_MARK, EVIDENCE_MARKER_TEXT, LEADING_BLANK;
|
|
9615
9750
|
var init_answer_citations = __esm(() => {
|
|
9751
|
+
init_citations();
|
|
9616
9752
|
init_evidence_ledger();
|
|
9617
9753
|
SENTENCE_END = /[.!?。!?]/u;
|
|
9618
9754
|
LEADING_LIST_MARK = /^[\s\-*•>]+/u;
|
|
@@ -9621,7 +9757,7 @@ var init_answer_citations = __esm(() => {
|
|
|
9621
9757
|
});
|
|
9622
9758
|
|
|
9623
9759
|
// src/agent/answer-guidelines.ts
|
|
9624
|
-
var ANSWER_IMAGE_EMBED_RULE = "**Images**: when a retrieved result is itself an image file (png, jpg, jpeg, gif, webp, svg, bmp, avif, heic, tiff) and showing it answers the question, embed it in the answer as a markdown image on its own line: ``, followed by its citation, e.g. ` [e2]`. Use only the real source path of retrieved evidence. Never invent a path, use relative paths, or embed remote URLs. Apart from these image embeds, the no-file-paths rule stands.", ANSWER_IMAGE_DELTA_RULE = "An image already embedded in the first answer must not be embedded again.", ANSWER_CITATION_RULE = "**Citations**: cite supporting evidence inline with the evidence id shown next to each retrieved result, in square brackets (e.g. [e3], or [e3][e7] for two). Copy ids exactly; never invent one. For a local file you opened yourself with `bash`, cite `[file:<absolute path>]`. Cite the evidence that backs a sentence at the end of that sentence. Do not cite numbers, paths, or anything else.";
|
|
9760
|
+
var ANSWER_IMAGE_EMBED_RULE = "**Images**: when a retrieved result is itself an image file (png, jpg, jpeg, gif, webp, svg, bmp, avif, heic, tiff) and showing it answers the question, embed it in the answer as a markdown image on its own line: ``, followed by its citation, e.g. ` [e2]`. Use only the real source path of retrieved evidence. Never invent a path, use relative paths, or embed remote URLs. Apart from these image embeds, the no-file-paths rule stands.", ANSWER_IMAGE_DELTA_RULE = "An image already embedded in the first answer must not be embedded again.", ANSWER_CITATION_RULE = "**Citations**: cite supporting evidence inline with the evidence id shown next to each retrieved result, in square brackets (e.g. [e3], or [e3][e7] for two). Copy ids exactly; never invent one. For a local file you opened yourself with `bash`, cite `[file:<absolute path>]`. Cite the evidence that backs a sentence at the end of that sentence. Do not cite numbers, paths, or anything else.", ANSWER_DELIVERABLE_RULE = '**Deliverables and artifacts**: if the user asks you to draft, write, generate, or prepare a deliverable or artifact (a reply email, letter, legal opinion, summary document, standard-format case citation block, checklist, report, or workflow document — including requests phrased as "how should I reply..." to a specific message), produce the finished, ready-to-use artifact as a file (using `write`, `edit`, or `bash` in local sessions, without overwriting existing source files) in a file format, extension, and structure aligned with the context and domain (e.g. `.eml` or `.md`/`.txt` with headers for an email reply; `.md`, `.txt`, `.docx`, `.hwp`, or `.pdf` matching workspace conventions for a legal opinion, insertable case-citation block, or summary document; `.csv`, `.xlsx`, or `.json` for structured tabular data), containing the complete unabridged artifact (including required headers, precedent passages, pinpoint paragraphs, and standard-format citations) inside the file instead of merely describing how to produce it. Do NOT dump the full artifact text into your reply: unless the user explicitly requests otherwise, keep your reply concise within the at-most-5-bullet direct-answer format, clearly stating the created file\'s exact path/location and briefly summarizing what was generated with inline citations to the underlying source evidence (`[eN]` or `[file:<source path>]`); never cite the newly created deliverable file itself as `[file:...]`.', ANSWER_DELIVERABLE_DELTA_RULE = "If a deliverable or artifact was requested and the first answer could not write the file yet (the fast phase runs without tool calls) or only gave advice about it, create the complete context-aligned deliverable file now during verification and report the created file's location and a brief summary concisely.";
|
|
9625
9761
|
|
|
9626
9762
|
// src/agent/datasource-skill.ts
|
|
9627
9763
|
import { Type as Type3 } from "typebox";
|
|
@@ -13957,6 +14093,7 @@ function recordStructuredResultsSession(sessionId, query, details, memory, compo
|
|
|
13957
14093
|
}
|
|
13958
14094
|
var init_search_documents = __esm(() => {
|
|
13959
14095
|
init_memory();
|
|
14096
|
+
init_citations();
|
|
13960
14097
|
});
|
|
13961
14098
|
|
|
13962
14099
|
// src/agent/search-all-tool.ts
|
|
@@ -14273,6 +14410,10 @@ function buildSystemPrompt(config) {
|
|
|
14273
14410
|
const modelId = config.modelId ?? "the configured model";
|
|
14274
14411
|
const toolLines = [
|
|
14275
14412
|
toolLine(config, "bash", "read and inspect configured document collections with ls, find, grep, cat, and similar tools, and — in local sessions — organize local files when the user asks (rename, move, copy, create folders, move to trash)"),
|
|
14413
|
+
...config.remoteSession ? [] : [
|
|
14414
|
+
toolLine(config, "write", "create a new file in local sessions — use this to produce requested deliverables and artifacts in a file format aligned with the request and context without overwriting existing source files"),
|
|
14415
|
+
toolLine(config, "edit", "surgically update an existing deliverable or config file when the user asks (local sessions only)")
|
|
14416
|
+
],
|
|
14276
14417
|
toolLine(config, "jikji_find", "local discovery through Jikji answer packs"),
|
|
14277
14418
|
toolLine(config, "everything_search", "instant Windows file/folder name search (voidtools Everything) across the configured search folders"),
|
|
14278
14419
|
toolLine(config, "fsearch_search", "instant macOS/Linux file/folder name search (fsearch-cli/FSearch) across the configured search folders, with a slow filesystem-walk fallback"),
|
|
@@ -14289,6 +14430,12 @@ function buildSystemPrompt(config) {
|
|
|
14289
14430
|
...config.toolNames.filter((name) => name.startsWith("search_datasource_")).map((name) => `- **${name}**: search only the ${name.slice("search_datasource_".length).replace(/_/g, "-")} datasource connection, spawning no other datasource CLIs`),
|
|
14290
14431
|
...config.toolNames.filter((name) => ![
|
|
14291
14432
|
"bash",
|
|
14433
|
+
"read",
|
|
14434
|
+
"write",
|
|
14435
|
+
"edit",
|
|
14436
|
+
"grep",
|
|
14437
|
+
"find",
|
|
14438
|
+
"ls",
|
|
14292
14439
|
"jikji_find",
|
|
14293
14440
|
"everything_search",
|
|
14294
14441
|
"fsearch_search",
|
|
@@ -14421,8 +14568,9 @@ Your answer is your final reply, written as plain text, never as a tool call. Ci
|
|
|
14421
14568
|
## Answer Guidelines
|
|
14422
14569
|
|
|
14423
14570
|
- **Complete vs. delta answer**: When no first answer reached the caller, give the complete core answer. When a first answer was already delivered, return only the delta against it — corrections and newly verified findings — and never repeat its unchanged content; if nothing changed, confirm the first answer in one short line.
|
|
14424
|
-
- **Bullet-point core answer**:
|
|
14425
|
-
- **Direct answer only**:
|
|
14571
|
+
- **Bullet-point core answer**: Unless the user explicitly requests a different format or length, provide the core answer to the user's question in at most 5 bullet points. If additional explanation or context is necessary, append it after the bullet points.
|
|
14572
|
+
- **Direct answer only**: Unless the user explicitly requests otherwise, the caller only needs the direct answer to their question. Never include specific file paths, datasource descriptions, or retrieval mechanics/principles in the answer (source paths are attached to each citation from the evidence ids you cite). Do not quote raw chunk text.
|
|
14573
|
+
- ${ANSWER_DELIVERABLE_RULE} ${ANSWER_DELIVERABLE_DELTA_RULE}
|
|
14426
14574
|
- ${ANSWER_CITATION_RULE}
|
|
14427
14575
|
- ${ANSWER_IMAGE_EMBED_RULE} ${ANSWER_IMAGE_DELTA_RULE}
|
|
14428
14576
|
- **No per-source negative reports**: Never report individual negative findings per source (e.g. "no information found in Slack" or "checked Drive but found nothing"). Simply omit unproductive sources from the answer and focus on what was found or provide a concise overall conclusion.
|
|
@@ -20379,7 +20527,8 @@ class AutoRAGAgent {
|
|
|
20379
20527
|
const everythingClient = new EverythingClient({
|
|
20380
20528
|
...options.everything ?? {},
|
|
20381
20529
|
root: this.workspaceProjectRoot,
|
|
20382
|
-
folders: this.searchPaths
|
|
20530
|
+
folders: this.searchPaths,
|
|
20531
|
+
excludeFolders: this.excludePaths
|
|
20383
20532
|
});
|
|
20384
20533
|
if (everythingClient.isSupported())
|
|
20385
20534
|
this.everythingClient = everythingClient;
|
|
@@ -20485,8 +20634,9 @@ class AutoRAGAgent {
|
|
|
20485
20634
|
return true;
|
|
20486
20635
|
});
|
|
20487
20636
|
this.tools = tools;
|
|
20637
|
+
const builtinToolNames = this.remoteSession ? PI_BUILTIN_TOOL_NAMES.filter((name) => name !== "write" && name !== "edit") : PI_BUILTIN_TOOL_NAMES;
|
|
20488
20638
|
const toolNames = [
|
|
20489
|
-
...
|
|
20639
|
+
...builtinToolNames,
|
|
20490
20640
|
...tools.map((tool) => tool.name),
|
|
20491
20641
|
...this.jevExtension !== undefined ? [JEV_TOOL_NAME] : []
|
|
20492
20642
|
];
|
|
@@ -20502,7 +20652,7 @@ class AutoRAGAgent {
|
|
|
20502
20652
|
this.innerAgent = {
|
|
20503
20653
|
state: {
|
|
20504
20654
|
systemPrompt,
|
|
20505
|
-
tools: [...
|
|
20655
|
+
tools: [...builtinToolNames.map((name) => ({ name })), ...tools],
|
|
20506
20656
|
messages: []
|
|
20507
20657
|
},
|
|
20508
20658
|
transformContext: (messages) => this.withMemoryContext(messages)
|
|
@@ -20819,6 +20969,7 @@ ${context.text}
|
|
|
20819
20969
|
...this.jevExtension !== undefined ? [this.jevExtension] : []
|
|
20820
20970
|
],
|
|
20821
20971
|
...this.jevExtension !== undefined ? { extensionToolNames: [JEV_TOOL_NAME] } : {},
|
|
20972
|
+
remoteSession: this.remoteSession,
|
|
20822
20973
|
...this.updateNotice === undefined ? {} : { updateNotice: this.updateNotice }
|
|
20823
20974
|
});
|
|
20824
20975
|
this.boundPiRuntime = runtime.runtime;
|
|
@@ -21745,8 +21896,9 @@ ${baseline}
|
|
|
21745
21896
|
` + `Produce the best complete, self-contained answer you can RIGHT NOW from this evidence. Do NOT call any search, retrieval, or file-reading tools and do NOT wait for more evidence. ` + `If the query is answerable from general knowledge alone, answer directly.
|
|
21746
21897
|
|
|
21747
21898
|
` + `Formatting and content rules for the answer:
|
|
21748
|
-
` + `-
|
|
21749
|
-
` + `-
|
|
21899
|
+
` + `- Unless the user explicitly requests a different format or length, provide the core answer to the user's question in at most 5 bullet points. If additional explanation is necessary, append it after the bullet points.
|
|
21900
|
+
` + `- Unless the user explicitly requests otherwise, answer the question directly. Do not include specific file paths, datasource descriptions, or retrieval mechanics in the answer text.
|
|
21901
|
+
` + `- ${ANSWER_DELIVERABLE_RULE}
|
|
21750
21902
|
` + `- ${ANSWER_CITATION_RULE}
|
|
21751
21903
|
` + `- ${ANSWER_IMAGE_EMBED_RULE}
|
|
21752
21904
|
` + `- Do not report per-source negative findings (e.g. "no information found in Slack" or "checked Drive but found nothing").
|
|
@@ -21785,12 +21937,14 @@ ${baseline}
|
|
|
21785
21937
|
` + `- NEVER restate or re-list first-answer facts that remain correct, and never repeat its bullet list.
|
|
21786
21938
|
` + `- Mark each item clearly as a correction or as a new finding.
|
|
21787
21939
|
` + `- If verification changed nothing and found nothing new, say so in one short line (the first answer is confirmed as-is) instead of restating it.
|
|
21940
|
+
` + `- ${ANSWER_DELIVERABLE_RULE} ${ANSWER_DELIVERABLE_DELTA_RULE}
|
|
21788
21941
|
` + `- ${ANSWER_CITATION_RULE} A correction or new finding that relies on evidence the first answer cited must cite that evidence's id again.
|
|
21789
21942
|
` + `- ${ANSWER_IMAGE_EMBED_RULE} ${ANSWER_IMAGE_DELTA_RULE}
|
|
21790
21943
|
` + `- Do not report per-source negative findings (e.g. "no information found in Slack").
|
|
21791
21944
|
` + `- When evidence conflicts, treat the freshest (most recent) information as the correct source of truth.` : `Formatting and content rules for the final answer (COMPLETE — no first answer reached the caller):
|
|
21792
|
-
` + `-
|
|
21793
|
-
` + `-
|
|
21945
|
+
` + `- Unless the user explicitly requests a different format or length, provide the core answer to the user's question in at most 5 bullet points. If additional explanation is necessary, append it after the bullet points.
|
|
21946
|
+
` + `- Unless the user explicitly requests otherwise, answer the question directly. Do not include specific file paths, datasource descriptions, or retrieval mechanics in the answer text.
|
|
21947
|
+
` + `- ${ANSWER_DELIVERABLE_RULE}${fastAnswer !== undefined ? ` ${ANSWER_DELIVERABLE_DELTA_RULE}` : ""}
|
|
21794
21948
|
` + `- ${ANSWER_CITATION_RULE}
|
|
21795
21949
|
` + `- ${ANSWER_IMAGE_EMBED_RULE}
|
|
21796
21950
|
` + `- Do not report per-source negative findings (e.g. "no information found in Slack").
|
|
@@ -21818,8 +21972,9 @@ ${initialRetrievalContext ?? "Pending; continue only with a brief progress state
|
|
|
21818
21972
|
` + `Treat candidates as unverified evidence, verify important claims against source files when needed, and use additional tools when needed. ` + `Judge relevance, conflicts, freshness, and sufficiency in this agent loop.
|
|
21819
21973
|
|
|
21820
21974
|
` + `Formatting and content rules for the answer:
|
|
21821
|
-
` + `-
|
|
21822
|
-
` + `-
|
|
21975
|
+
` + `- Unless the user explicitly requests a different format or length, provide the core answer to the user's question in at most 5 bullet points. If additional explanation is necessary, append it after the bullet points.
|
|
21976
|
+
` + `- Unless the user explicitly requests otherwise, answer the question directly. Do not include specific file paths, datasource descriptions, or retrieval mechanics in the answer text.
|
|
21977
|
+
` + `- ${ANSWER_DELIVERABLE_RULE}
|
|
21823
21978
|
` + `- ${ANSWER_CITATION_RULE}
|
|
21824
21979
|
` + `- ${ANSWER_IMAGE_EMBED_RULE}
|
|
21825
21980
|
` + `- Do not report per-source negative findings (e.g. "no information found in Slack").
|
|
@@ -22837,7 +22992,7 @@ var init_results = __esm(() => {
|
|
|
22837
22992
|
confidence: Type18.Optional(Type18.Number({ description: "Evidence confidence, 0..1", minimum: 0, maximum: 1 }))
|
|
22838
22993
|
});
|
|
22839
22994
|
answerSchema = Type18.String({
|
|
22840
|
-
description: `Answer for the caller.
|
|
22995
|
+
description: `Answer for the caller. Unless the user explicitly requests otherwise, provide at most 5 bullet points (plus optional explanation) and answer directly; reference results by bracketed number (e.g. [1], [2]) without file paths or raw chunk text, except the image-embed exception below. ${ANSWER_DELIVERABLE_RULE} ${ANSWER_CITATION_RULE} ${ANSWER_IMAGE_EMBED_RULE}`
|
|
22841
22996
|
});
|
|
22842
22997
|
warningsSchema = Type18.Optional(Type18.Array(Type18.String(), { description: "Optional warnings about this result set" }));
|
|
22843
22998
|
curatedResultFields = {
|
|
@@ -23135,7 +23290,7 @@ function chatCrawler(id) {
|
|
|
23135
23290
|
hierarchy: (metadata) => ["chats", str(metadata.chatName) ?? str(metadata.chatId) ?? "unknown"]
|
|
23136
23291
|
};
|
|
23137
23292
|
}
|
|
23138
|
-
var str = (value) => typeof value === "string" && value.length > 0 ? value : undefined, slack, CRAWLER_DESCRIPTORS;
|
|
23293
|
+
var str = (value) => typeof value === "string" && value.length > 0 ? value : undefined, slack, kakao, CRAWLER_DESCRIPTORS;
|
|
23139
23294
|
var init_descriptors = __esm(() => {
|
|
23140
23295
|
slack = {
|
|
23141
23296
|
id: "slack",
|
|
@@ -23166,10 +23321,32 @@ var init_descriptors = __esm(() => {
|
|
|
23166
23321
|
str(metadata.channelName) ?? str(metadata.channelId) ?? "unknown"
|
|
23167
23322
|
]
|
|
23168
23323
|
};
|
|
23324
|
+
kakao = {
|
|
23325
|
+
id: "kakao",
|
|
23326
|
+
keyExpression: "m.chunk_id",
|
|
23327
|
+
chunkId: (key) => key,
|
|
23328
|
+
fromClause: "chunks m",
|
|
23329
|
+
liveWhere: "length(trim(coalesce(m.text, ''))) > 0 and not (json_valid(m.text) and (json_extract(m.text, '$.feedType') is not null or json_extract(m.text, '$.csIP') is not null))",
|
|
23330
|
+
textExpression: "coalesce(m.text, '')",
|
|
23331
|
+
titleExpression: "m.chat_name",
|
|
23332
|
+
epochSecondsExpression: "unixepoch(m.started_at)",
|
|
23333
|
+
fts: { table: "chunks_fts", joinOn: "chunks_fts.rowid = m.rowid" },
|
|
23334
|
+
metadata: [
|
|
23335
|
+
{ sql: "m.chat_id", key: "chatId" },
|
|
23336
|
+
{ sql: "m.chat_name", key: "chatName" },
|
|
23337
|
+
{ sql: "m.sender_nickname", key: "senderNickname" },
|
|
23338
|
+
{ sql: "m.started_at", key: "startedAt" },
|
|
23339
|
+
{ sql: "m.ended_at", key: "endedAt" }
|
|
23340
|
+
],
|
|
23341
|
+
channelIdExpression: "m.chat_id",
|
|
23342
|
+
channelNameExpression: "m.chat_name",
|
|
23343
|
+
hierarchy: (metadata) => ["chats", str(metadata.chatName) ?? str(metadata.chatId) ?? "unknown"]
|
|
23344
|
+
};
|
|
23169
23345
|
CRAWLER_DESCRIPTORS = {
|
|
23170
23346
|
slack,
|
|
23171
23347
|
telegram: chatCrawler("telegram"),
|
|
23172
|
-
whatsapp: chatCrawler("whatsapp")
|
|
23348
|
+
whatsapp: chatCrawler("whatsapp"),
|
|
23349
|
+
kakao
|
|
23173
23350
|
};
|
|
23174
23351
|
});
|
|
23175
23352
|
|
|
@@ -27317,1436 +27494,1528 @@ var init_methods3 = __esm(() => {
|
|
|
27317
27494
|
DEFAULT_KAKAO_TAGS = ["kakaotalk", "personal", "pii"];
|
|
27318
27495
|
});
|
|
27319
27496
|
|
|
27320
|
-
// src/datasource/
|
|
27321
|
-
|
|
27322
|
-
|
|
27323
|
-
|
|
27324
|
-
|
|
27325
|
-
|
|
27326
|
-
|
|
27327
|
-
|
|
27328
|
-
|
|
27329
|
-
|
|
27330
|
-
|
|
27331
|
-
|
|
27332
|
-
|
|
27333
|
-
|
|
27334
|
-
|
|
27335
|
-
|
|
27336
|
-
|
|
27337
|
-
|
|
27338
|
-
|
|
27339
|
-
|
|
27340
|
-
|
|
27341
|
-
|
|
27342
|
-
capabilities: ["chat", "external-cli", "polling", "bm25", "semantic"],
|
|
27343
|
-
tags: this.tags,
|
|
27344
|
-
status: "active",
|
|
27345
|
-
requiresExternalCli: true,
|
|
27346
|
-
datasourceId: KAKAO_DATASOURCE_ID2,
|
|
27347
|
-
instanceId: this.instanceId,
|
|
27348
|
-
instances: this.instances
|
|
27349
|
-
};
|
|
27497
|
+
// src/datasource/sidecar/fusion.ts
|
|
27498
|
+
function encodeVector(vector) {
|
|
27499
|
+
const floats = Float32Array.from(vector);
|
|
27500
|
+
return new Uint8Array(floats.buffer, floats.byteOffset, floats.byteLength);
|
|
27501
|
+
}
|
|
27502
|
+
function decodeVector(blob) {
|
|
27503
|
+
if (blob.byteLength % Float32Array.BYTES_PER_ELEMENT !== 0)
|
|
27504
|
+
return;
|
|
27505
|
+
return new Float32Array(new Uint8Array(blob).buffer);
|
|
27506
|
+
}
|
|
27507
|
+
function cosine3(a, b) {
|
|
27508
|
+
if (a.length === 0 || a.length !== b.length)
|
|
27509
|
+
return 0;
|
|
27510
|
+
let dot = 0;
|
|
27511
|
+
let normA = 0;
|
|
27512
|
+
let normB = 0;
|
|
27513
|
+
for (let index = 0;index < a.length; index += 1) {
|
|
27514
|
+
const x = a[index] ?? 0;
|
|
27515
|
+
const y = b[index] ?? 0;
|
|
27516
|
+
dot += x * y;
|
|
27517
|
+
normA += x * x;
|
|
27518
|
+
normB += y * y;
|
|
27350
27519
|
}
|
|
27351
|
-
|
|
27352
|
-
return
|
|
27353
|
-
|
|
27354
|
-
|
|
27355
|
-
|
|
27356
|
-
|
|
27520
|
+
if (normA === 0 || normB === 0)
|
|
27521
|
+
return 0;
|
|
27522
|
+
return dot / Math.sqrt(normA * normB);
|
|
27523
|
+
}
|
|
27524
|
+
function topKByCosine(query, rows, k) {
|
|
27525
|
+
if (k <= 0)
|
|
27526
|
+
return [];
|
|
27527
|
+
const scored = [];
|
|
27528
|
+
for (const row of rows) {
|
|
27529
|
+
const score = cosine3(query, row.vector);
|
|
27530
|
+
if (score > 0)
|
|
27531
|
+
scored.push({ key: row.key, score });
|
|
27357
27532
|
}
|
|
27358
|
-
|
|
27359
|
-
|
|
27360
|
-
|
|
27361
|
-
|
|
27362
|
-
|
|
27363
|
-
|
|
27364
|
-
|
|
27365
|
-
|
|
27366
|
-
|
|
27367
|
-
|
|
27368
|
-
|
|
27369
|
-
|
|
27370
|
-
const chunkCount = "data" in indexResult && typeof indexResult.data === "object" && indexResult.data !== null && "chunkCount" in indexResult.data && typeof indexResult.data.chunkCount === "number" ? indexResult.data.chunkCount : 0;
|
|
27371
|
-
return {
|
|
27372
|
-
ok: true,
|
|
27373
|
-
instanceId: this.instanceId,
|
|
27374
|
-
skill: KAKAO_DATASOURCE_ID2,
|
|
27375
|
-
chunkCount,
|
|
27376
|
-
indexedAt: this.lastIndexedAt,
|
|
27377
|
-
diagnostics: []
|
|
27378
|
-
};
|
|
27379
|
-
} catch (error) {
|
|
27380
|
-
return this.fail("datasource-unavailable", {
|
|
27381
|
-
ok: false,
|
|
27382
|
-
reason: "spawn-error",
|
|
27383
|
-
stdout: "",
|
|
27384
|
-
stderr: describeRetrievalError(error),
|
|
27385
|
-
code: null
|
|
27386
|
-
});
|
|
27533
|
+
scored.sort((a, b) => b.score - a.score || a.key.localeCompare(b.key));
|
|
27534
|
+
return scored.slice(0, k);
|
|
27535
|
+
}
|
|
27536
|
+
function reciprocalRankFusion(lists) {
|
|
27537
|
+
const scores = new Map;
|
|
27538
|
+
for (const list of lists) {
|
|
27539
|
+
const seen = new Set;
|
|
27540
|
+
for (const [index, key] of list.entries()) {
|
|
27541
|
+
if (seen.has(key))
|
|
27542
|
+
continue;
|
|
27543
|
+
seen.add(key);
|
|
27544
|
+
scores.set(key, (scores.get(key) ?? 0) + 1 / (RRF_K2 + index + 1));
|
|
27387
27545
|
}
|
|
27388
27546
|
}
|
|
27389
|
-
|
|
27390
|
-
|
|
27391
|
-
|
|
27392
|
-
|
|
27547
|
+
return [...scores.entries()].map(([key, score]) => ({ key, score })).sort((a, b) => b.score - a.score);
|
|
27548
|
+
}
|
|
27549
|
+
var RRF_K2 = 60;
|
|
27550
|
+
|
|
27551
|
+
// src/datasource/sidecar/index.ts
|
|
27552
|
+
import { createHash as createHash14 } from "node:crypto";
|
|
27553
|
+
import { mkdirSync as mkdirSync22, statSync as statSync3 } from "node:fs";
|
|
27554
|
+
import { dirname as dirname24 } from "node:path";
|
|
27555
|
+
import { DatabaseSync as DatabaseSync2 } from "node:sqlite";
|
|
27556
|
+
import { pathToFileURL as pathToFileURL2 } from "node:url";
|
|
27557
|
+
|
|
27558
|
+
class CrawlerSidecar {
|
|
27559
|
+
descriptor;
|
|
27560
|
+
options;
|
|
27561
|
+
select;
|
|
27562
|
+
where;
|
|
27563
|
+
opened;
|
|
27564
|
+
attached;
|
|
27565
|
+
lastSemantic;
|
|
27566
|
+
constructor(options) {
|
|
27567
|
+
this.options = options;
|
|
27568
|
+
const { descriptor } = options;
|
|
27569
|
+
this.descriptor = descriptor;
|
|
27570
|
+
const metadataColumns = descriptor.metadata.map((column, index) => `${column.sql} as m${index}`);
|
|
27571
|
+
this.select = [
|
|
27572
|
+
`${descriptor.keyExpression} as k`,
|
|
27573
|
+
`${descriptor.textExpression} as content`,
|
|
27574
|
+
`${descriptor.titleExpression ?? "null"} as title`,
|
|
27575
|
+
`${descriptor.epochSecondsExpression ?? "null"} as epoch`,
|
|
27576
|
+
...metadataColumns
|
|
27577
|
+
].join(", ");
|
|
27578
|
+
const scope = descriptor.scopeExpression !== undefined && options.workspaceId !== undefined;
|
|
27579
|
+
const allowlist = [
|
|
27580
|
+
...(options.channelIds?.length ?? 0) > 0 ? [`${descriptor.channelIdExpression} in (${options.channelIds?.map(() => "?").join(", ")})`] : [],
|
|
27581
|
+
...(options.channelNames?.length ?? 0) > 0 ? [`${descriptor.channelNameExpression} in (${options.channelNames?.map(() => "?").join(", ")})`] : []
|
|
27393
27582
|
];
|
|
27583
|
+
this.where = [
|
|
27584
|
+
descriptor.liveWhere,
|
|
27585
|
+
...scope ? [`${descriptor.scopeExpression} = ?`] : [],
|
|
27586
|
+
...allowlist.length > 0 ? [`(${allowlist.join(" or ")})`] : []
|
|
27587
|
+
].join(" and ");
|
|
27394
27588
|
}
|
|
27395
|
-
|
|
27396
|
-
|
|
27397
|
-
|
|
27398
|
-
|
|
27399
|
-
|
|
27400
|
-
|
|
27401
|
-
|
|
27402
|
-
|
|
27403
|
-
|
|
27404
|
-
|
|
27405
|
-
|
|
27406
|
-
|
|
27407
|
-
|
|
27589
|
+
get db() {
|
|
27590
|
+
if (this.opened !== undefined)
|
|
27591
|
+
return this.opened;
|
|
27592
|
+
mkdirSync22(dirname24(this.options.sidecarDbPath), { recursive: true });
|
|
27593
|
+
const db = new DatabaseSync2(this.options.sidecarDbPath);
|
|
27594
|
+
db.exec(`pragma busy_timeout = ${BUSY_TIMEOUT_MS}`);
|
|
27595
|
+
db.exec("pragma journal_mode = wal");
|
|
27596
|
+
db.exec(`
|
|
27597
|
+
create table if not exists sidecar_meta (k text primary key, v text not null);
|
|
27598
|
+
create table if not exists sidecar_vec (message_key text primary key, hash text not null, vec blob not null);
|
|
27599
|
+
`);
|
|
27600
|
+
this.opened = db;
|
|
27601
|
+
return db;
|
|
27408
27602
|
}
|
|
27409
|
-
|
|
27410
|
-
|
|
27411
|
-
|
|
27412
|
-
|
|
27413
|
-
|
|
27414
|
-
|
|
27415
|
-
|
|
27416
|
-
|
|
27417
|
-
`# KakaoTalk datasource (${KAKAO_SKILL_TYPE})`,
|
|
27418
|
-
"",
|
|
27419
|
-
"This skill searches KakaoTalk chats that are indexed through the external `lazykatok` CLI. AutoRAG never reads KakaoTalk databases directly.",
|
|
27420
|
-
"",
|
|
27421
|
-
"## When to use",
|
|
27422
|
-
"Use this skill when the question is about KakaoTalk conversations, chat participants, or content shared inside chats.",
|
|
27423
|
-
"",
|
|
27424
|
-
"## Indexing",
|
|
27425
|
-
`Indexing is server-managed and refreshed ${cadence}. You do not trigger indexing; just search.`,
|
|
27426
|
-
"",
|
|
27427
|
-
"## How to search",
|
|
27428
|
-
`Call the dedicated \`${datasourceSearchToolName(KAKAO_DATASOURCE_ID2)}\` tool with a natural-language \`query\` and \`topK\`. This datasource does not support per-source scope narrowing. Configured datasource instances:`,
|
|
27429
|
-
instanceSources.length > 0 ? instanceSources : "- (no configured instances)",
|
|
27430
|
-
"",
|
|
27431
|
-
"Chat/channel filtering is owned by lazykatok.",
|
|
27432
|
-
"",
|
|
27433
|
-
"## Native CLI",
|
|
27434
|
-
"The external `lazykatok` CLI owns the archive, index, and credentials. When the dedicated tool cannot express what you need, call lazykatok directly through `bash`:",
|
|
27435
|
-
'- `lazykatok search bm25 "<query>" --json --limit 20` — lexical search (`semantic` mode also available)',
|
|
27436
|
-
"- `lazykatok chunk get <chunkId> --json` — fetch one chunk by id (chunk ids appear in result metadata)",
|
|
27437
|
-
"- `lazykatok chunk context <chunkId> --json` — surrounding messages of a chunk",
|
|
27438
|
-
"Never pass datasource virtual paths (`/kakao/...`) to bash; they are not OS paths.",
|
|
27439
|
-
"",
|
|
27440
|
-
"## Output rules",
|
|
27441
|
-
"Datasource source identifiers such as `/kakao/<instance>/chunks/<id>` are internal and opaque. Never put them, real file paths, account IDs, or phone numbers in the visible answer."
|
|
27442
|
-
].join(`
|
|
27443
|
-
`)
|
|
27444
|
-
};
|
|
27445
|
-
}
|
|
27446
|
-
fail(code, result) {
|
|
27447
|
-
const base = result.stderr.length > 0 ? `${result.reason}: ${result.stderr.trim().slice(0, 4000)}` : result.reason;
|
|
27448
|
-
const message = code === "datasource-index-failed" && result.reason === "timeout" ? `${base} — ${connectorSyncTimeoutHint()}` : base;
|
|
27449
|
-
const diagnostic = {
|
|
27450
|
-
code,
|
|
27451
|
-
severity: code === "datasource-unavailable" ? "warning" : "error",
|
|
27452
|
-
message,
|
|
27453
|
-
instanceId: this.instanceId,
|
|
27454
|
-
source: KAKAO_DATASOURCE_ID2
|
|
27455
|
-
};
|
|
27456
|
-
return {
|
|
27457
|
-
ok: false,
|
|
27458
|
-
instanceId: this.instanceId,
|
|
27459
|
-
skill: KAKAO_DATASOURCE_ID2,
|
|
27460
|
-
indexedAt: Date.now(),
|
|
27461
|
-
diagnostics: [diagnostic],
|
|
27462
|
-
error: result.reason,
|
|
27463
|
-
code,
|
|
27464
|
-
message
|
|
27465
|
-
};
|
|
27466
|
-
}
|
|
27467
|
-
}
|
|
27468
|
-
var KAKAO_DATASOURCE_ID2 = "kakao", KAKAO_SKILL_TYPE = "kakaotalk", DEFAULT_INSTANCE_ID4 = "default", DEFAULT_POLLING_INTERVAL_MS3, DEFAULT_KAKAO_TAGS2;
|
|
27469
|
-
var init_skill7 = __esm(() => {
|
|
27470
|
-
init_skip();
|
|
27471
|
-
init_scope2();
|
|
27472
|
-
init_methods3();
|
|
27473
|
-
DEFAULT_POLLING_INTERVAL_MS3 = 15 * 60 * 1000;
|
|
27474
|
-
DEFAULT_KAKAO_TAGS2 = ["kakaotalk", "personal", "pii"];
|
|
27475
|
-
});
|
|
27476
|
-
|
|
27477
|
-
// src/datasource/skills/lazykatok/index.ts
|
|
27478
|
-
var init_lazykatok = __esm(() => {
|
|
27479
|
-
init_client8();
|
|
27480
|
-
init_methods3();
|
|
27481
|
-
init_skill7();
|
|
27482
|
-
init_types5();
|
|
27483
|
-
});
|
|
27484
|
-
|
|
27485
|
-
// src/datasource/skills/mail-export/connector.ts
|
|
27486
|
-
import { readdir as readdir4, readFile as readFile5, stat as stat6 } from "node:fs/promises";
|
|
27487
|
-
import { basename as basename12, dirname as dirname24, extname as extname6, join as join37 } from "node:path";
|
|
27488
|
-
import { simpleParser as simpleParser2 } from "mailparser";
|
|
27489
|
-
|
|
27490
|
-
class MailExportConnector {
|
|
27491
|
-
options;
|
|
27492
|
-
constructor(options = {}) {
|
|
27493
|
-
this.options = options;
|
|
27494
|
-
}
|
|
27495
|
-
async fetch() {
|
|
27496
|
-
const paths = this.options.paths ?? [];
|
|
27497
|
-
if (paths.length === 0)
|
|
27498
|
-
return { ok: false, reason: "not-configured", message: "no mail export paths configured" };
|
|
27499
|
-
const maxDocuments = this.options.maxDocuments ?? DEFAULT_MAX_DOCUMENTS3;
|
|
27500
|
-
const maxBytes = this.options.maxBytesPerFile ?? DEFAULT_MAX_BYTES_PER_FILE2;
|
|
27501
|
-
const files = [];
|
|
27502
|
-
let missingPaths = 0;
|
|
27503
|
-
for (const path of paths) {
|
|
27504
|
-
try {
|
|
27505
|
-
const info = await stat6(path);
|
|
27506
|
-
if (info.isDirectory()) {
|
|
27507
|
-
const entries = await readdir4(path, { recursive: true, withFileTypes: true });
|
|
27508
|
-
for (const entry of entries) {
|
|
27509
|
-
if (!entry.isFile())
|
|
27510
|
-
continue;
|
|
27511
|
-
const extension = extname6(entry.name).toLowerCase();
|
|
27512
|
-
if (extension === ".eml" || extension === ".mbox") {
|
|
27513
|
-
files.push(join37(entry.parentPath, entry.name));
|
|
27514
|
-
}
|
|
27515
|
-
}
|
|
27516
|
-
} else if (info.isFile()) {
|
|
27517
|
-
files.push(path);
|
|
27518
|
-
}
|
|
27519
|
-
} catch {
|
|
27520
|
-
missingPaths += 1;
|
|
27521
|
-
}
|
|
27603
|
+
attachCrawler() {
|
|
27604
|
+
let file;
|
|
27605
|
+
try {
|
|
27606
|
+
const { dev, ino } = statSync3(this.options.crawlerDbPath);
|
|
27607
|
+
file = { dev, ino };
|
|
27608
|
+
} catch {
|
|
27609
|
+
this.detachCrawler();
|
|
27610
|
+
return false;
|
|
27522
27611
|
}
|
|
27523
|
-
if (
|
|
27524
|
-
|
|
27612
|
+
if (this.attached !== undefined) {
|
|
27613
|
+
if (this.attached.dev === file.dev && this.attached.ino === file.ino)
|
|
27614
|
+
return true;
|
|
27615
|
+
this.detachCrawler();
|
|
27525
27616
|
}
|
|
27526
|
-
const
|
|
27527
|
-
|
|
27528
|
-
|
|
27529
|
-
|
|
27530
|
-
|
|
27531
|
-
let skippedLarge = 0;
|
|
27532
|
-
for (const file of files.sort()) {
|
|
27533
|
-
if (documents.length >= maxDocuments)
|
|
27534
|
-
break;
|
|
27535
|
-
let bytes;
|
|
27536
|
-
try {
|
|
27537
|
-
const info = await stat6(file);
|
|
27538
|
-
if (info.size > maxBytes) {
|
|
27539
|
-
skippedLarge += 1;
|
|
27540
|
-
continue;
|
|
27541
|
-
}
|
|
27542
|
-
bytes = await readFile5(file);
|
|
27543
|
-
} catch {
|
|
27544
|
-
parseFailures += 1;
|
|
27545
|
-
continue;
|
|
27546
|
-
}
|
|
27547
|
-
const stem = basename12(file, extname6(file));
|
|
27548
|
-
const mailboxDir = basename12(dirname24(file));
|
|
27549
|
-
const hierarchy = ["mailboxes", mailboxDir, stem];
|
|
27550
|
-
const extension = extname6(file).toLowerCase();
|
|
27551
|
-
const rawMessages = extension === ".mbox" ? splitMbox(bytes.toString("utf8")) : [bytes.toString("utf8")];
|
|
27552
|
-
for (const [index, raw] of rawMessages.entries()) {
|
|
27553
|
-
if (documents.length >= maxDocuments)
|
|
27554
|
-
break;
|
|
27555
|
-
try {
|
|
27556
|
-
const parsed = await simpleParser2(raw);
|
|
27557
|
-
const subject = parsed.subject ?? "(no subject)";
|
|
27558
|
-
const from = Array.isArray(parsed.from) ? parsed.from[0]?.text : parsed.from?.text;
|
|
27559
|
-
const to = Array.isArray(parsed.to) ? parsed.to[0]?.text : parsed.to?.text;
|
|
27560
|
-
const body = (parsed.text ?? "").trim().slice(0, MAX_BODY_CHARS);
|
|
27561
|
-
const headerBlock = [
|
|
27562
|
-
`Subject: ${subject}`,
|
|
27563
|
-
...from !== undefined ? [`From: ${from}`] : [],
|
|
27564
|
-
...to !== undefined ? [`To: ${to}`] : [],
|
|
27565
|
-
...parsed.date !== undefined ? [`Date: ${parsed.date.toISOString()}`] : []
|
|
27566
|
-
].join(`
|
|
27567
|
-
`);
|
|
27568
|
-
const content = `${headerBlock}
|
|
27569
|
-
|
|
27570
|
-
${body}`.trim();
|
|
27571
|
-
if (content.length === 0)
|
|
27572
|
-
continue;
|
|
27573
|
-
documents.push({
|
|
27574
|
-
docId: extension === ".mbox" ? `${stem}-${index + 1}` : stem,
|
|
27575
|
-
hierarchy,
|
|
27576
|
-
title: subject,
|
|
27577
|
-
content,
|
|
27578
|
-
...parsed.date !== undefined ? { publishedAt: parsed.date.getTime() } : {},
|
|
27579
|
-
metadata: {
|
|
27580
|
-
path: file,
|
|
27581
|
-
subject,
|
|
27582
|
-
...from !== undefined ? { from } : {},
|
|
27583
|
-
messageIndex: index
|
|
27584
|
-
}
|
|
27585
|
-
});
|
|
27586
|
-
} catch {
|
|
27587
|
-
parseFailures += 1;
|
|
27588
|
-
}
|
|
27589
|
-
}
|
|
27617
|
+
const crawlerUri = `${pathToFileURL2(this.options.crawlerDbPath).href}?mode=ro`.replaceAll("'", "''");
|
|
27618
|
+
try {
|
|
27619
|
+
this.db.exec(`attach database '${crawlerUri}' as crawler`);
|
|
27620
|
+
} catch {
|
|
27621
|
+
return false;
|
|
27590
27622
|
}
|
|
27591
|
-
|
|
27592
|
-
|
|
27593
|
-
|
|
27594
|
-
|
|
27595
|
-
|
|
27596
|
-
|
|
27597
|
-
|
|
27598
|
-
function splitMbox(text) {
|
|
27599
|
-
const lines = text.split(/\r?\n/u);
|
|
27600
|
-
const messages = [];
|
|
27601
|
-
let current = [];
|
|
27602
|
-
for (const line of lines) {
|
|
27603
|
-
if (MBOX_SEPARATOR.test(line) && line.startsWith("From ")) {
|
|
27604
|
-
if (current.length > 0)
|
|
27605
|
-
messages.push(current.join(`
|
|
27606
|
-
`));
|
|
27607
|
-
current = [];
|
|
27608
|
-
continue;
|
|
27623
|
+
const { descriptor } = this.options;
|
|
27624
|
+
try {
|
|
27625
|
+
this.db.prepare(`select ${this.select} from ${descriptor.fromClause} where ${this.where} limit 0`).all(...this.whereParams());
|
|
27626
|
+
this.db.prepare(`select 1 from ${descriptor.fts.table} limit 0`).all();
|
|
27627
|
+
} catch {
|
|
27628
|
+
this.db.exec("detach database crawler");
|
|
27629
|
+
return false;
|
|
27609
27630
|
}
|
|
27610
|
-
|
|
27631
|
+
this.attached = file;
|
|
27632
|
+
this.lastSemantic = undefined;
|
|
27633
|
+
return true;
|
|
27611
27634
|
}
|
|
27612
|
-
|
|
27613
|
-
|
|
27614
|
-
|
|
27615
|
-
|
|
27616
|
-
|
|
27617
|
-
|
|
27618
|
-
|
|
27619
|
-
|
|
27620
|
-
MBOX_SEPARATOR = /^From .*\d{4}$/mu;
|
|
27621
|
-
});
|
|
27622
|
-
|
|
27623
|
-
// src/datasource/skills/mail-export/skill.ts
|
|
27624
|
-
var MAIL_EXPORT_SKILL_DEFINITION, MailExportSkill;
|
|
27625
|
-
var init_skill8 = __esm(() => {
|
|
27626
|
-
init_connector_skill();
|
|
27627
|
-
init_connector4();
|
|
27628
|
-
MAIL_EXPORT_SKILL_DEFINITION = {
|
|
27629
|
-
skillName: "mail-export",
|
|
27630
|
-
skillType: "mail-export",
|
|
27631
|
-
description: "Local mail export datasource",
|
|
27632
|
-
capabilities: ["email", "filesystem"],
|
|
27633
|
-
defaultTags: ["mail-export", "email", "pii"],
|
|
27634
|
-
contentType: "email",
|
|
27635
|
-
manifestDescription: "Search indexed local mail exports (mbox/eml archives). Use for questions about archived email conversations.",
|
|
27636
|
-
manifestNotes: [
|
|
27637
|
-
"Only the server-configured export paths are indexed; archives are read locally and never uploaded."
|
|
27638
|
-
]
|
|
27639
|
-
};
|
|
27640
|
-
MailExportSkill = class MailExportSkill extends ConnectorDatasourceSkill {
|
|
27641
|
-
constructor(options = {}) {
|
|
27642
|
-
const { connector, connectorOptions, ...rest } = options;
|
|
27643
|
-
super(MAIL_EXPORT_SKILL_DEFINITION, {
|
|
27644
|
-
...rest,
|
|
27645
|
-
connector: connector ?? new MailExportConnector(connectorOptions ?? {})
|
|
27646
|
-
});
|
|
27647
|
-
}
|
|
27648
|
-
};
|
|
27649
|
-
});
|
|
27650
|
-
|
|
27651
|
-
// src/datasource/skills/mail-export/index.ts
|
|
27652
|
-
var init_mail_export = __esm(() => {
|
|
27653
|
-
init_connector4();
|
|
27654
|
-
init_skill8();
|
|
27655
|
-
});
|
|
27656
|
-
|
|
27657
|
-
// src/datasource/skills/mailcrawl/config.ts
|
|
27658
|
-
import { chmodSync, mkdirSync as mkdirSync22 } from "node:fs";
|
|
27659
|
-
function validateMailcrawlInstanceId(instanceId) {
|
|
27660
|
-
if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/u.test(instanceId)) {
|
|
27661
|
-
throw new Error("mailcrawl instanceId must be a safe single path segment");
|
|
27635
|
+
detachCrawler() {
|
|
27636
|
+
if (this.attached === undefined)
|
|
27637
|
+
return;
|
|
27638
|
+
this.attached = undefined;
|
|
27639
|
+
this.lastSemantic = undefined;
|
|
27640
|
+
try {
|
|
27641
|
+
this.opened?.exec("detach database crawler");
|
|
27642
|
+
} catch {}
|
|
27662
27643
|
}
|
|
27663
|
-
|
|
27664
|
-
|
|
27665
|
-
mkdirSync22(path, { recursive: true, mode: 448 });
|
|
27666
|
-
if (process.platform !== "win32")
|
|
27667
|
-
chmodSync(path, 448);
|
|
27668
|
-
}
|
|
27669
|
-
var init_config3 = () => {};
|
|
27670
|
-
|
|
27671
|
-
// src/datasource/skills/mailcrawl/client.ts
|
|
27672
|
-
import { spawn as spawn15 } from "node:child_process";
|
|
27673
|
-
|
|
27674
|
-
class MailcrawlClient {
|
|
27675
|
-
options;
|
|
27676
|
-
constructor(options = {}) {
|
|
27677
|
-
this.options = options;
|
|
27678
|
-
if (options.instanceId !== undefined)
|
|
27679
|
-
validateMailcrawlInstanceId(options.instanceId);
|
|
27644
|
+
get semanticEnabled() {
|
|
27645
|
+
return this.options.embedder !== undefined;
|
|
27680
27646
|
}
|
|
27681
|
-
|
|
27682
|
-
|
|
27683
|
-
|
|
27684
|
-
|
|
27685
|
-
|
|
27686
|
-
...this.options.mailbox === undefined ? [] : ["--mailbox", this.options.mailbox],
|
|
27687
|
-
...this.options.backend === undefined ? [] : ["--backend", this.options.backend],
|
|
27688
|
-
...this.options.source === undefined ? [] : ["--source", this.options.source],
|
|
27689
|
-
...this.options.fixture === undefined ? [] : ["--fixture", this.options.fixture],
|
|
27690
|
-
...this.options.himalayaConfig === undefined ? [] : ["--himalaya-config", this.options.himalayaConfig]
|
|
27691
|
-
];
|
|
27692
|
-
const result = await this.run(args, signal, this.indexTimeoutMs());
|
|
27693
|
-
if (!result.ok)
|
|
27694
|
-
return failure3(result);
|
|
27695
|
-
const parsed = parseObject2(result.stdout);
|
|
27696
|
-
if (parsed === undefined || !hasAnyNumber(parsed, ["added", "updated", "deleted", "unchanged", "chunksAdded"])) {
|
|
27697
|
-
return failure3({ ...result, ok: false, reason: "invalid-output" });
|
|
27698
|
-
}
|
|
27699
|
-
const messages = number(parsed, "added") ?? number(parsed, "updated") ?? number(parsed, "unchanged") ?? 0;
|
|
27700
|
-
return ok4({
|
|
27701
|
-
messages,
|
|
27702
|
-
added: number(parsed, "added"),
|
|
27703
|
-
updated: number(parsed, "updated"),
|
|
27704
|
-
deleted: number(parsed, "deleted"),
|
|
27705
|
-
unchanged: number(parsed, "unchanged"),
|
|
27706
|
-
chunksAdded: number(parsed, "chunksAdded"),
|
|
27707
|
-
archiveRevision: string(parsed, "archiveRevision")
|
|
27708
|
-
}, result);
|
|
27647
|
+
close() {
|
|
27648
|
+
this.opened?.close();
|
|
27649
|
+
this.opened = undefined;
|
|
27650
|
+
this.attached = undefined;
|
|
27651
|
+
this.lastSemantic = undefined;
|
|
27709
27652
|
}
|
|
27710
|
-
|
|
27711
|
-
|
|
27712
|
-
|
|
27713
|
-
|
|
27714
|
-
|
|
27715
|
-
|
|
27716
|
-
mode === "keyword" ? "bm25" : mode,
|
|
27717
|
-
...this.options.account === undefined ? [] : ["--account", this.options.account],
|
|
27718
|
-
...this.options.mailbox === undefined ? [] : ["--mailbox", this.options.mailbox],
|
|
27719
|
-
"--limit",
|
|
27720
|
-
String(options?.topK ?? 20),
|
|
27721
|
-
"--json",
|
|
27722
|
-
query.trim()
|
|
27653
|
+
whereParams() {
|
|
27654
|
+
const { descriptor, workspaceId, channelIds = [], channelNames = [] } = this.options;
|
|
27655
|
+
return [
|
|
27656
|
+
...descriptor.scopeExpression !== undefined && workspaceId !== undefined ? [workspaceId] : [],
|
|
27657
|
+
...channelIds,
|
|
27658
|
+
...channelNames
|
|
27723
27659
|
];
|
|
27724
|
-
const result = await this.run(args, options?.signal, this.options.timeoutMs ?? DEFAULT_SEARCH_TIMEOUT_MS2);
|
|
27725
|
-
if (!result.ok)
|
|
27726
|
-
return failure3(result);
|
|
27727
|
-
const parsed = parseJson3(result.stdout);
|
|
27728
|
-
if (parsed === undefined)
|
|
27729
|
-
return failure3({ ...result, ok: false, reason: "invalid-output" });
|
|
27730
|
-
const rows = Array.isArray(parsed) ? parsed : objectArray(parsed, ["hits", "results", "items"]);
|
|
27731
|
-
if (rows === undefined)
|
|
27732
|
-
return failure3({ ...result, ok: false, reason: "invalid-output" });
|
|
27733
|
-
const hits = rows.map((row, index) => normalizeHit2(row, mode, index));
|
|
27734
|
-
if (hits.some((hit) => hit === undefined)) {
|
|
27735
|
-
return failure3({ ...result, ok: false, reason: "invalid-output" });
|
|
27736
|
-
}
|
|
27737
|
-
return {
|
|
27738
|
-
ok: true,
|
|
27739
|
-
hits,
|
|
27740
|
-
stdout: result.stdout,
|
|
27741
|
-
stderr: result.stderr,
|
|
27742
|
-
code: result.code ?? 0
|
|
27743
|
-
};
|
|
27744
27660
|
}
|
|
27745
|
-
|
|
27746
|
-
|
|
27661
|
+
searchLexical(query, topK) {
|
|
27662
|
+
const match = toMatchQuery(query);
|
|
27663
|
+
if (match === undefined || topK <= 0 || !this.attachCrawler())
|
|
27664
|
+
return [];
|
|
27665
|
+
const { descriptor } = this.options;
|
|
27666
|
+
const rows = this.db.prepare(`select ${this.select}, bm25(${descriptor.fts.table}) as rank
|
|
27667
|
+
from ${descriptor.fromClause} join ${descriptor.fts.table} on ${descriptor.fts.joinOn}
|
|
27668
|
+
where ${descriptor.fts.table} match ? and ${this.where}
|
|
27669
|
+
order by rank limit ?`).all(match, ...this.whereParams(), topK);
|
|
27670
|
+
return rows.map((row) => this.toHit(this.toRow(row), -Number(row.rank)));
|
|
27747
27671
|
}
|
|
27748
|
-
async
|
|
27749
|
-
const
|
|
27750
|
-
if (
|
|
27751
|
-
return
|
|
27672
|
+
async searchSemantic(query, topK) {
|
|
27673
|
+
const trimmed = query.trim();
|
|
27674
|
+
if (this.options.embedder === undefined || trimmed.length === 0 || topK <= 0 || !this.attachCrawler())
|
|
27675
|
+
return [];
|
|
27676
|
+
const ranked = await this.rankSemantic(trimmed, topK * POOL_FACTOR);
|
|
27677
|
+
if (ranked.length === 0)
|
|
27678
|
+
return [];
|
|
27679
|
+
const live = this.hydrate(ranked.map((entry) => entry.key));
|
|
27680
|
+
const hits = [];
|
|
27681
|
+
for (const entry of ranked) {
|
|
27682
|
+
const row = live.get(entry.key);
|
|
27683
|
+
if (row !== undefined)
|
|
27684
|
+
hits.push(this.toHit(row, entry.score));
|
|
27685
|
+
if (hits.length >= topK)
|
|
27686
|
+
break;
|
|
27752
27687
|
}
|
|
27753
|
-
|
|
27754
|
-
|
|
27755
|
-
|
|
27756
|
-
const
|
|
27757
|
-
|
|
27758
|
-
|
|
27759
|
-
|
|
27688
|
+
return hits;
|
|
27689
|
+
}
|
|
27690
|
+
async rankSemantic(query, pool) {
|
|
27691
|
+
const now = Date.now();
|
|
27692
|
+
let cached = this.lastSemantic;
|
|
27693
|
+
if (cached === undefined || cached.query !== query || now - cached.at > SEMANTIC_REUSE_MS) {
|
|
27694
|
+
const embedder = this.options.embedder;
|
|
27695
|
+
if (embedder === undefined)
|
|
27696
|
+
return [];
|
|
27697
|
+
cached = {
|
|
27698
|
+
query,
|
|
27699
|
+
at: now,
|
|
27700
|
+
vector: embedder.embed([query]).then((vectors) => vectors[0], () => {
|
|
27701
|
+
return;
|
|
27702
|
+
}),
|
|
27703
|
+
ranked: undefined
|
|
27704
|
+
};
|
|
27705
|
+
this.lastSemantic = cached;
|
|
27760
27706
|
}
|
|
27761
|
-
|
|
27762
|
-
|
|
27763
|
-
|
|
27764
|
-
|
|
27765
|
-
|
|
27707
|
+
const vector = await cached.vector;
|
|
27708
|
+
if (vector === undefined) {
|
|
27709
|
+
if (this.lastSemantic === cached)
|
|
27710
|
+
this.lastSemantic = undefined;
|
|
27711
|
+
return [];
|
|
27766
27712
|
}
|
|
27767
|
-
if (
|
|
27768
|
-
|
|
27769
|
-
|
|
27713
|
+
if (cached.ranked === undefined || cached.ranked.pool < pool) {
|
|
27714
|
+
const wide = pool * POOL_FACTOR;
|
|
27715
|
+
cached.ranked = { pool: wide, keys: topKByCosine(vector, this.storedVectors(), wide) };
|
|
27716
|
+
}
|
|
27717
|
+
return cached.ranked.keys.slice(0, pool);
|
|
27770
27718
|
}
|
|
27771
|
-
async
|
|
27772
|
-
|
|
27773
|
-
|
|
27774
|
-
|
|
27775
|
-
const
|
|
27776
|
-
|
|
27777
|
-
|
|
27719
|
+
async searchHybrid(query, topK) {
|
|
27720
|
+
if (topK <= 0)
|
|
27721
|
+
return [];
|
|
27722
|
+
const pool = topK * POOL_FACTOR;
|
|
27723
|
+
const lexical = this.searchLexical(query, pool);
|
|
27724
|
+
const semantic = await this.searchSemantic(query, pool);
|
|
27725
|
+
if (semantic.length === 0)
|
|
27726
|
+
return lexical.slice(0, topK);
|
|
27727
|
+
const byKey = new Map;
|
|
27728
|
+
for (const hit of [...semantic, ...lexical])
|
|
27729
|
+
byKey.set(hit.key, hit);
|
|
27730
|
+
const fused = [];
|
|
27731
|
+
for (const entry of reciprocalRankFusion([lexical.map((hit) => hit.key), semantic.map((hit) => hit.key)])) {
|
|
27732
|
+
const hit = byKey.get(entry.key);
|
|
27733
|
+
if (hit !== undefined)
|
|
27734
|
+
fused.push({ ...hit, score: entry.score });
|
|
27735
|
+
if (fused.length >= topK)
|
|
27736
|
+
break;
|
|
27778
27737
|
}
|
|
27779
|
-
return
|
|
27780
|
-
embedded: number(parsed, "embedded"),
|
|
27781
|
-
reused: number(parsed, "reused"),
|
|
27782
|
-
generation: string(parsed, "generation"),
|
|
27783
|
-
archiveRevision: string(parsed, "archiveRevision"),
|
|
27784
|
-
rebuilt: typeof parsed.rebuilt === "boolean" ? parsed.rebuilt : undefined,
|
|
27785
|
-
embedder: string(parsed, "embedder")
|
|
27786
|
-
}, result);
|
|
27738
|
+
return fused;
|
|
27787
27739
|
}
|
|
27788
|
-
|
|
27789
|
-
|
|
27790
|
-
|
|
27791
|
-
|
|
27792
|
-
|
|
27793
|
-
|
|
27794
|
-
|
|
27795
|
-
|
|
27796
|
-
|
|
27797
|
-
|
|
27798
|
-
|
|
27799
|
-
|
|
27800
|
-
|
|
27801
|
-
|
|
27802
|
-
|
|
27803
|
-
const
|
|
27804
|
-
|
|
27805
|
-
|
|
27806
|
-
|
|
27807
|
-
|
|
27808
|
-
|
|
27809
|
-
|
|
27810
|
-
child.stdout.setEncoding("utf8");
|
|
27811
|
-
child.stderr.setEncoding("utf8");
|
|
27812
|
-
child.stdout.on("data", (chunk) => {
|
|
27813
|
-
stdout = appendOutput(stdout, chunk, max);
|
|
27814
|
-
if (stdout.exceeded && reason === undefined) {
|
|
27815
|
-
reason = "stdout-too-large";
|
|
27816
|
-
terminate4(child);
|
|
27740
|
+
async sync() {
|
|
27741
|
+
const { embedder } = this.options;
|
|
27742
|
+
if (embedder === undefined)
|
|
27743
|
+
return { ok: false, message: "no embedder configured" };
|
|
27744
|
+
if (!this.attachCrawler())
|
|
27745
|
+
return { ok: false, message: "crawler archive not available yet" };
|
|
27746
|
+
let identity;
|
|
27747
|
+
try {
|
|
27748
|
+
identity = await embedder.identity();
|
|
27749
|
+
} catch (error) {
|
|
27750
|
+
return { ok: false, message: errorMessage2(error) };
|
|
27751
|
+
}
|
|
27752
|
+
this.adoptIdentity(identity);
|
|
27753
|
+
const pruned = this.prune();
|
|
27754
|
+
const pending = this.pendingKeys();
|
|
27755
|
+
const batchSize = Math.max(1, this.options.batchSize ?? DEFAULT_BATCH_SIZE);
|
|
27756
|
+
const budget = Math.max(1, this.options.maxEmbedPerSync ?? DEFAULT_MAX_EMBED_PER_SYNC);
|
|
27757
|
+
const target = Math.min(pending.length, budget);
|
|
27758
|
+
const embed = async (rows) => {
|
|
27759
|
+
const vectors = await embedder.embed(rows.map((row) => row.input));
|
|
27760
|
+
if (vectors.length !== rows.length) {
|
|
27761
|
+
throw new Error(`embedder returned ${vectors.length} vectors for ${rows.length} messages`);
|
|
27817
27762
|
}
|
|
27818
|
-
|
|
27819
|
-
|
|
27820
|
-
|
|
27821
|
-
|
|
27822
|
-
|
|
27823
|
-
|
|
27763
|
+
const wrong = vectors.find((vector) => vector.length !== identity.dimension);
|
|
27764
|
+
if (wrong !== undefined) {
|
|
27765
|
+
throw new Error(`embedder returned a ${wrong.length}-dimension vector, expected ${identity.dimension}`);
|
|
27766
|
+
}
|
|
27767
|
+
return vectors;
|
|
27768
|
+
};
|
|
27769
|
+
let embedded = 0;
|
|
27770
|
+
let skipped = 0;
|
|
27771
|
+
for (let offset = 0;offset < target; offset += batchSize) {
|
|
27772
|
+
const rows = [...this.hydrate(pending.slice(offset, Math.min(offset + batchSize, target))).values()];
|
|
27773
|
+
if (rows.length === 0)
|
|
27774
|
+
continue;
|
|
27775
|
+
try {
|
|
27776
|
+
this.store(rows, await embed(rows));
|
|
27777
|
+
embedded += rows.length;
|
|
27778
|
+
} catch (batchError) {
|
|
27779
|
+
const rejected = [];
|
|
27780
|
+
let lastError = batchError;
|
|
27781
|
+
for (const row of rows) {
|
|
27782
|
+
try {
|
|
27783
|
+
this.store([row], await embed([row]));
|
|
27784
|
+
embedded += 1;
|
|
27785
|
+
} catch (error) {
|
|
27786
|
+
rejected.push(row);
|
|
27787
|
+
lastError = error;
|
|
27788
|
+
}
|
|
27789
|
+
}
|
|
27790
|
+
if (rejected.length === 0)
|
|
27791
|
+
continue;
|
|
27792
|
+
if (embedded === 0)
|
|
27793
|
+
return { ok: false, message: errorMessage2(lastError) };
|
|
27794
|
+
this.store(rejected, undefined);
|
|
27795
|
+
skipped += rejected.length;
|
|
27824
27796
|
}
|
|
27825
|
-
});
|
|
27826
|
-
child.on("error", (error) => {
|
|
27827
|
-
if (settled)
|
|
27828
|
-
return;
|
|
27829
|
-
settled = true;
|
|
27830
|
-
clearTimeout(timer);
|
|
27831
|
-
signal?.removeEventListener("abort", abort);
|
|
27832
|
-
resolveResult({
|
|
27833
|
-
ok: false,
|
|
27834
|
-
reason: error.code === "ENOENT" ? "binary-missing" : "spawn-error",
|
|
27835
|
-
stdout: stdout.text,
|
|
27836
|
-
stderr: "",
|
|
27837
|
-
code: null
|
|
27838
|
-
});
|
|
27839
|
-
});
|
|
27840
|
-
child.on("close", (code) => {
|
|
27841
|
-
if (settled)
|
|
27842
|
-
return;
|
|
27843
|
-
settled = true;
|
|
27844
|
-
clearTimeout(timer);
|
|
27845
|
-
signal?.removeEventListener("abort", abort);
|
|
27846
|
-
resolveResult({
|
|
27847
|
-
ok: code === 0 && reason === undefined,
|
|
27848
|
-
...reason === undefined ? code === 0 ? {} : { reason: "nonzero-exit" } : { reason },
|
|
27849
|
-
stdout: stdout.text,
|
|
27850
|
-
stderr: stderr.text,
|
|
27851
|
-
code
|
|
27852
|
-
});
|
|
27853
|
-
});
|
|
27854
|
-
});
|
|
27855
|
-
}
|
|
27856
|
-
function terminate4(child) {
|
|
27857
|
-
if (child.killed)
|
|
27858
|
-
return;
|
|
27859
|
-
if (process.platform !== "win32" && child.pid !== undefined) {
|
|
27860
|
-
try {
|
|
27861
|
-
process.kill(-child.pid, "SIGTERM");
|
|
27862
|
-
return;
|
|
27863
|
-
} catch {}
|
|
27864
|
-
}
|
|
27865
|
-
child.kill("SIGTERM");
|
|
27866
|
-
}
|
|
27867
|
-
function parseJson3(text) {
|
|
27868
|
-
try {
|
|
27869
|
-
return JSON.parse(text.trim());
|
|
27870
|
-
} catch {
|
|
27871
|
-
return;
|
|
27872
|
-
}
|
|
27873
|
-
}
|
|
27874
|
-
function parseObject2(text) {
|
|
27875
|
-
const value = parseJson3(text);
|
|
27876
|
-
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : undefined;
|
|
27877
|
-
}
|
|
27878
|
-
function objectArray(value, keys) {
|
|
27879
|
-
if (value === null || typeof value !== "object" || Array.isArray(value))
|
|
27880
|
-
return;
|
|
27881
|
-
for (const key of keys)
|
|
27882
|
-
if (Array.isArray(value[key]))
|
|
27883
|
-
return value[key];
|
|
27884
|
-
return;
|
|
27885
|
-
}
|
|
27886
|
-
function hasAnyNumber(value, keys) {
|
|
27887
|
-
return keys.some((key) => number(value, key) !== undefined);
|
|
27888
|
-
}
|
|
27889
|
-
function normalizeHit2(value, mode, index) {
|
|
27890
|
-
if (value === null || typeof value !== "object" || Array.isArray(value))
|
|
27891
|
-
return;
|
|
27892
|
-
const row = value;
|
|
27893
|
-
const text = (key) => typeof row[key] === "string" ? row[key] : "";
|
|
27894
|
-
const chunkId = text("chunkId") || text("chunk_id");
|
|
27895
|
-
const messageId = text("messageId") || text("message_id");
|
|
27896
|
-
const accountId = text("accountId") || text("account_id");
|
|
27897
|
-
const mailbox = text("mailbox");
|
|
27898
|
-
const snippet = text("snippet") || text("content") || text("text");
|
|
27899
|
-
if (!isSafeOpaqueId(chunkId) || !isSafeOpaqueId(messageId) || !snippet)
|
|
27900
|
-
return;
|
|
27901
|
-
const to = Array.isArray(row.to) ? row.to.filter((item) => typeof item === "string") : [];
|
|
27902
|
-
return {
|
|
27903
|
-
chunkId,
|
|
27904
|
-
messageId,
|
|
27905
|
-
threadId: text("threadId") || text("thread_id"),
|
|
27906
|
-
accountId,
|
|
27907
|
-
mailbox,
|
|
27908
|
-
subject: text("subject"),
|
|
27909
|
-
from: text("from"),
|
|
27910
|
-
to,
|
|
27911
|
-
date: text("date"),
|
|
27912
|
-
snippet,
|
|
27913
|
-
score: typeof row.score === "number" ? row.score : 1 / (index + 1),
|
|
27914
|
-
mode
|
|
27915
|
-
};
|
|
27916
|
-
}
|
|
27917
|
-
function number(value, key) {
|
|
27918
|
-
return typeof value[key] === "number" && Number.isFinite(value[key]) ? value[key] : undefined;
|
|
27919
|
-
}
|
|
27920
|
-
function string(value, key) {
|
|
27921
|
-
return typeof value[key] === "string" ? value[key] : undefined;
|
|
27922
|
-
}
|
|
27923
|
-
function ok4(data, result) {
|
|
27924
|
-
return { ok: true, data, stdout: result.stdout, stderr: result.stderr, code: result.code ?? 0 };
|
|
27925
|
-
}
|
|
27926
|
-
function failure3(result) {
|
|
27927
|
-
return {
|
|
27928
|
-
ok: false,
|
|
27929
|
-
reason: result.reason ?? "nonzero-exit",
|
|
27930
|
-
stdout: result.stdout,
|
|
27931
|
-
stderr: boundDiagnosticText6(result.stderr),
|
|
27932
|
-
code: result.code
|
|
27933
|
-
};
|
|
27934
|
-
}
|
|
27935
|
-
function boundDiagnosticText6(value) {
|
|
27936
|
-
if (value.length === 0)
|
|
27937
|
-
return "";
|
|
27938
|
-
return value.trim().slice(0, 4000);
|
|
27939
|
-
}
|
|
27940
|
-
function appendOutput(current, chunk, maxBytes) {
|
|
27941
|
-
if (current.exceeded)
|
|
27942
|
-
return current;
|
|
27943
|
-
const bytes = Buffer.from(chunk, "utf8");
|
|
27944
|
-
const remaining = Math.max(0, maxBytes - current.bytes);
|
|
27945
|
-
const prefix = truncateUtf8(chunk, remaining);
|
|
27946
|
-
const total = current.bytes + bytes.byteLength;
|
|
27947
|
-
return {
|
|
27948
|
-
text: current.text + prefix,
|
|
27949
|
-
bytes: Math.min(total, maxBytes),
|
|
27950
|
-
exceeded: total > maxBytes
|
|
27951
|
-
};
|
|
27952
|
-
}
|
|
27953
|
-
function isSafeOpaqueId(value) {
|
|
27954
|
-
return value.length > 0 && value !== "." && value !== ".." && !/[\\/#\u0000-\u001f\u007f]/u.test(value);
|
|
27955
|
-
}
|
|
27956
|
-
function findRemoteEmbeddingViolation(env) {
|
|
27957
|
-
for (const [key, value] of Object.entries(env)) {
|
|
27958
|
-
if (value === undefined)
|
|
27959
|
-
continue;
|
|
27960
|
-
const lower = key.toLowerCase();
|
|
27961
|
-
if (REMOTE_EMBEDDING_ENV_KEYS.has(lower))
|
|
27962
|
-
return key;
|
|
27963
|
-
if (lower === "mailcrawl_embedder" || lower === "mailcrawl_embed_url") {
|
|
27964
|
-
if (isRemoteHttpEndpoint(value))
|
|
27965
|
-
return key;
|
|
27966
27797
|
}
|
|
27798
|
+
return { ok: true, embedded, skipped, pruned, remaining: pending.length - embedded - skipped };
|
|
27967
27799
|
}
|
|
27968
|
-
|
|
27969
|
-
|
|
27970
|
-
|
|
27971
|
-
|
|
27972
|
-
|
|
27973
|
-
|
|
27974
|
-
|
|
27975
|
-
|
|
27976
|
-
|
|
27977
|
-
|
|
27800
|
+
adoptIdentity(identity) {
|
|
27801
|
+
const serialized = JSON.stringify(identity);
|
|
27802
|
+
const current = this.db.prepare("select v from sidecar_meta where k = ?").get(IDENTITY_KEY);
|
|
27803
|
+
if (current?.v === serialized)
|
|
27804
|
+
return;
|
|
27805
|
+
this.transaction(() => {
|
|
27806
|
+
this.db.exec("delete from sidecar_vec");
|
|
27807
|
+
this.db.prepare("insert or replace into sidecar_meta (k, v) values (?, ?)").run(IDENTITY_KEY, serialized);
|
|
27808
|
+
});
|
|
27809
|
+
this.lastSemantic = undefined;
|
|
27978
27810
|
}
|
|
27979
|
-
|
|
27980
|
-
|
|
27981
|
-
|
|
27982
|
-
|
|
27983
|
-
|
|
27984
|
-
|
|
27985
|
-
|
|
27811
|
+
prune() {
|
|
27812
|
+
const { descriptor } = this.options;
|
|
27813
|
+
const result = this.db.prepare(`delete from sidecar_vec where message_key not in (
|
|
27814
|
+
select ${descriptor.keyExpression} from ${descriptor.fromClause} where ${this.where}
|
|
27815
|
+
)`).run(...this.whereParams());
|
|
27816
|
+
if (Number(result.changes) > 0)
|
|
27817
|
+
this.lastSemantic = undefined;
|
|
27818
|
+
return Number(result.changes);
|
|
27819
|
+
}
|
|
27820
|
+
pendingKeys() {
|
|
27821
|
+
const { descriptor } = this.options;
|
|
27822
|
+
const rows = this.db.prepare(`select ${this.select}, v.hash as stored_hash
|
|
27823
|
+
from ${descriptor.fromClause}
|
|
27824
|
+
left join sidecar_vec v on v.message_key = ${descriptor.keyExpression}
|
|
27825
|
+
where ${this.where}`).iterate(...this.whereParams());
|
|
27826
|
+
const pending = [];
|
|
27827
|
+
for (const row of rows) {
|
|
27828
|
+
if (row.stored_hash !== this.toRow(row).hash)
|
|
27829
|
+
pending.push(String(row.k));
|
|
27830
|
+
}
|
|
27831
|
+
return pending;
|
|
27832
|
+
}
|
|
27833
|
+
hydrate(keys) {
|
|
27834
|
+
const { descriptor } = this.options;
|
|
27835
|
+
const out = new Map;
|
|
27836
|
+
if (keys.length === 0)
|
|
27837
|
+
return out;
|
|
27838
|
+
const rows = this.db.prepare(`select ${this.select} from ${descriptor.fromClause}
|
|
27839
|
+
where ${descriptor.keyExpression} in (${keys.map(() => "?").join(", ")}) and ${this.where}`).all(...keys, ...this.whereParams());
|
|
27840
|
+
const byKey = new Map(rows.map((row) => [String(row.k), this.toRow(row)]));
|
|
27841
|
+
for (const key of keys) {
|
|
27842
|
+
const row = byKey.get(key);
|
|
27843
|
+
if (row !== undefined)
|
|
27844
|
+
out.set(key, row);
|
|
27845
|
+
}
|
|
27846
|
+
return out;
|
|
27847
|
+
}
|
|
27848
|
+
store(rows, vectors) {
|
|
27849
|
+
const upsert = this.db.prepare("insert or replace into sidecar_vec (message_key, hash, vec) values (?, ?, ?)");
|
|
27850
|
+
this.transaction(() => {
|
|
27851
|
+
for (const [index, row] of rows.entries()) {
|
|
27852
|
+
upsert.run(row.key, row.hash, encodeVector(vectors?.[index] ?? []));
|
|
27853
|
+
}
|
|
27854
|
+
});
|
|
27855
|
+
this.lastSemantic = undefined;
|
|
27856
|
+
}
|
|
27857
|
+
*storedVectors() {
|
|
27858
|
+
const rows = this.db.prepare("select message_key, vec from sidecar_vec where length(vec) > 0").iterate();
|
|
27859
|
+
for (const row of rows) {
|
|
27860
|
+
const vector = row.vec instanceof Uint8Array ? decodeVector(row.vec) : undefined;
|
|
27861
|
+
if (vector !== undefined)
|
|
27862
|
+
yield { key: String(row.message_key), vector };
|
|
27863
|
+
}
|
|
27864
|
+
}
|
|
27865
|
+
transaction(work) {
|
|
27866
|
+
this.db.exec("begin immediate");
|
|
27986
27867
|
try {
|
|
27987
|
-
|
|
27988
|
-
|
|
27868
|
+
work();
|
|
27869
|
+
this.db.exec("commit");
|
|
27870
|
+
} catch (error) {
|
|
27871
|
+
this.db.exec("rollback");
|
|
27872
|
+
throw error;
|
|
27873
|
+
}
|
|
27874
|
+
}
|
|
27875
|
+
toRow(row) {
|
|
27876
|
+
const metadata = {};
|
|
27877
|
+
for (const [index, column] of this.options.descriptor.metadata.entries()) {
|
|
27878
|
+
const value = row[`m${index}`];
|
|
27879
|
+
if (typeof value === "string" && value.length > 0 || typeof value === "number")
|
|
27880
|
+
metadata[column.key] = String(value);
|
|
27881
|
+
}
|
|
27882
|
+
const title = typeof row.title === "string" && row.title.length > 0 ? row.title : undefined;
|
|
27883
|
+
const epoch = typeof row.epoch === "number" && Number.isFinite(row.epoch) ? row.epoch : undefined;
|
|
27884
|
+
const content = String(row.content ?? "");
|
|
27885
|
+
const input = `${title ?? ""}
|
|
27886
|
+
${content}`.slice(0, MAX_EMBED_INPUT_CHARS);
|
|
27887
|
+
return {
|
|
27888
|
+
key: String(row.k),
|
|
27889
|
+
content,
|
|
27890
|
+
title,
|
|
27891
|
+
epoch,
|
|
27892
|
+
metadata,
|
|
27893
|
+
input,
|
|
27894
|
+
hash: createHash14("sha1").update(input).digest("hex")
|
|
27895
|
+
};
|
|
27896
|
+
}
|
|
27897
|
+
toHit(row, score) {
|
|
27898
|
+
return {
|
|
27899
|
+
key: row.key,
|
|
27900
|
+
content: row.content,
|
|
27901
|
+
score,
|
|
27902
|
+
...row.title !== undefined ? { title: row.title } : {},
|
|
27903
|
+
...row.epoch !== undefined ? { publishedAt: Math.round(row.epoch * 1000) } : {},
|
|
27904
|
+
metadata: row.metadata
|
|
27905
|
+
};
|
|
27989
27906
|
}
|
|
27990
|
-
return "";
|
|
27991
27907
|
}
|
|
27992
|
-
|
|
27993
|
-
|
|
27994
|
-
|
|
27995
|
-
|
|
27996
|
-
|
|
27997
|
-
|
|
27998
|
-
|
|
27999
|
-
|
|
28000
|
-
|
|
28001
|
-
"MAILCRAWL_EMBEDDER_PROVIDER",
|
|
28002
|
-
"MAILCRAWL_EMBED_URL",
|
|
28003
|
-
"MAILCRAWL_EMBED_MODEL",
|
|
28004
|
-
"MAILCRAWL_EMBED_DIM",
|
|
28005
|
-
"MAILCRAWL_EMBED_TIMEOUT",
|
|
28006
|
-
"MAILCRAWL_EMBED_BATCH_SIZE",
|
|
28007
|
-
"MAILCRAWL_QUERY_PREFIX",
|
|
28008
|
-
"MAILCRAWL_PASSAGE_PREFIX",
|
|
28009
|
-
"MAILCRAWL_NATIVE_MODEL",
|
|
28010
|
-
"MAILCRAWL_NATIVE_DEVICE",
|
|
28011
|
-
"MAILCRAWL_NATIVE_DTYPE",
|
|
28012
|
-
"MAILCRAWL_KIWI_MODEL",
|
|
28013
|
-
"MAILCRAWL_JA_HELPER",
|
|
28014
|
-
"MAILCRAWL_ZH_HELPER"
|
|
28015
|
-
]);
|
|
28016
|
-
REMOTE_EMBEDDING_ENV_KEYS = new Set([
|
|
28017
|
-
"mailcrawl_embedder_base_url",
|
|
28018
|
-
"mailcrawl_embedding_base_url",
|
|
28019
|
-
"mailcrawl_allow_remote_embeddings"
|
|
28020
|
-
]);
|
|
28021
|
-
});
|
|
27908
|
+
function toMatchQuery(text) {
|
|
27909
|
+
const terms = [...new Set(text.match(/[\p{L}\p{N}_]+/gu) ?? [])];
|
|
27910
|
+
return terms.length === 0 ? undefined : terms.map((term) => `"${term}"`).join(" OR ");
|
|
27911
|
+
}
|
|
27912
|
+
function errorMessage2(error) {
|
|
27913
|
+
return error instanceof Error ? error.message : String(error);
|
|
27914
|
+
}
|
|
27915
|
+
var DEFAULT_BATCH_SIZE = 256, DEFAULT_MAX_EMBED_PER_SYNC = 2048, MAX_EMBED_INPUT_CHARS = 8192, BUSY_TIMEOUT_MS = 5000, POOL_FACTOR = 3, SEMANTIC_REUSE_MS = 1e4, IDENTITY_KEY = "identity";
|
|
27916
|
+
var init_sidecar = () => {};
|
|
28022
27917
|
|
|
28023
|
-
// src/datasource/
|
|
28024
|
-
|
|
28025
|
-
|
|
28026
|
-
|
|
28027
|
-
|
|
28028
|
-
|
|
28029
|
-
|
|
28030
|
-
|
|
28031
|
-
|
|
28032
|
-
this.client = client;
|
|
28033
|
-
this.mode = mode;
|
|
28034
|
-
this.instanceId = instanceId;
|
|
28035
|
-
this.tags = tags;
|
|
28036
|
-
this.account = account;
|
|
28037
|
-
this.mailbox = mailbox;
|
|
27918
|
+
// src/datasource/sidecar/methods.ts
|
|
27919
|
+
function sidecarMethodName(datasourceId, mode) {
|
|
27920
|
+
return mode === "bm25" ? `${datasourceId}-sidecar-bm25` : `${datasourceId}-${mode}`;
|
|
27921
|
+
}
|
|
27922
|
+
|
|
27923
|
+
class SidecarRetrievalMethod {
|
|
27924
|
+
options;
|
|
27925
|
+
constructor(options) {
|
|
27926
|
+
this.options = options;
|
|
28038
27927
|
}
|
|
28039
27928
|
describe() {
|
|
27929
|
+
const { datasourceId, skillType, mode, tags } = this.options;
|
|
27930
|
+
const { type, text, capabilities } = DESCRIPTIONS[mode];
|
|
28040
27931
|
return {
|
|
28041
|
-
name:
|
|
28042
|
-
type
|
|
28043
|
-
description: `${
|
|
27932
|
+
name: sidecarMethodName(datasourceId, mode),
|
|
27933
|
+
type,
|
|
27934
|
+
description: `${text} over ${skillType} via a read-only sidecar index`,
|
|
28044
27935
|
status: "active",
|
|
28045
|
-
capabilities: [
|
|
28046
|
-
datasourceId
|
|
28047
|
-
tags
|
|
27936
|
+
capabilities: [...capabilities, "scoped", "path-opaque-sources"],
|
|
27937
|
+
datasourceId,
|
|
27938
|
+
tags
|
|
27939
|
+
};
|
|
27940
|
+
}
|
|
27941
|
+
async retrieve(query, options) {
|
|
27942
|
+
const trimmed = query.trim();
|
|
27943
|
+
if (trimmed.length === 0)
|
|
27944
|
+
return [];
|
|
27945
|
+
const topK = options.topK ?? DEFAULT_TOP_K5;
|
|
27946
|
+
const { sidecar, mode } = this.options;
|
|
27947
|
+
const hits = mode === "bm25" ? sidecar.searchLexical(trimmed, topK) : mode === "semantic" ? await sidecar.searchSemantic(trimmed, topK) : await sidecar.searchHybrid(trimmed, topK);
|
|
27948
|
+
const results = [];
|
|
27949
|
+
for (const hit of hits) {
|
|
27950
|
+
const result = this.toResult(hit, options);
|
|
27951
|
+
if (result !== undefined)
|
|
27952
|
+
results.push(result);
|
|
27953
|
+
}
|
|
27954
|
+
return results;
|
|
27955
|
+
}
|
|
27956
|
+
toResult(hit, options) {
|
|
27957
|
+
const { descriptor, datasourceId, instanceId, mode } = this.options;
|
|
27958
|
+
const chunkId = descriptor.chunkId(hit.key, hit.metadata);
|
|
27959
|
+
const source = datasourceSourcePath(datasourceId, instanceId, chunkId);
|
|
27960
|
+
if (!matchesDatasourceScope(source, options.scope))
|
|
27961
|
+
return;
|
|
27962
|
+
const method = sidecarMethodName(datasourceId, mode);
|
|
27963
|
+
return {
|
|
27964
|
+
id: `${datasourceId}:${instanceId}:${chunkId}`,
|
|
27965
|
+
content: hit.content,
|
|
27966
|
+
source,
|
|
27967
|
+
score: hit.score,
|
|
27968
|
+
metadata: {
|
|
27969
|
+
...hit.metadata,
|
|
27970
|
+
method,
|
|
27971
|
+
datasourceId,
|
|
27972
|
+
instanceId,
|
|
27973
|
+
backend: "sidecar",
|
|
27974
|
+
...hit.title !== undefined ? { title: hit.title } : {},
|
|
27975
|
+
hierarchy: descriptor.hierarchy(hit.metadata).join("/"),
|
|
27976
|
+
...hit.publishedAt !== undefined ? { publishedAt: hit.publishedAt } : {}
|
|
27977
|
+
}
|
|
28048
27978
|
};
|
|
28049
27979
|
}
|
|
28050
|
-
async retrieve(query, options) {
|
|
28051
|
-
if (!query.trim())
|
|
28052
|
-
return [];
|
|
28053
|
-
const result = await this.client.search(this.mode, query, {
|
|
28054
|
-
topK: options.topK,
|
|
28055
|
-
signal: options.signal
|
|
28056
|
-
});
|
|
28057
|
-
if (!result.ok)
|
|
28058
|
-
throw datasourceCliError("mailcrawl", `search --mode ${this.mode}`, result);
|
|
28059
|
-
return result.hits.filter((hit) => {
|
|
28060
|
-
if (this.account !== undefined && hit.accountId !== this.account)
|
|
28061
|
-
return false;
|
|
28062
|
-
if (this.mailbox !== undefined && hit.mailbox !== this.mailbox)
|
|
28063
|
-
return false;
|
|
28064
|
-
const source = datasourceSourcePath("mailcrawl", this.instanceId, hit.chunkId);
|
|
28065
|
-
return matchesDatasourceScope(source, options.scope);
|
|
28066
|
-
}).map((hit) => {
|
|
28067
|
-
const source = datasourceSourcePath("mailcrawl", this.instanceId, hit.chunkId);
|
|
28068
|
-
return {
|
|
28069
|
-
id: `mailcrawl:${this.instanceId}:${hit.chunkId}`,
|
|
28070
|
-
content: hit.snippet,
|
|
28071
|
-
source,
|
|
28072
|
-
score: hit.score,
|
|
28073
|
-
metadata: {
|
|
28074
|
-
method: `mailcrawl-${this.mode}`,
|
|
28075
|
-
datasourceId: "mailcrawl",
|
|
28076
|
-
instanceId: this.instanceId,
|
|
28077
|
-
mode: this.mode,
|
|
28078
|
-
chunkId: hit.chunkId,
|
|
28079
|
-
messageId: hit.messageId,
|
|
28080
|
-
threadId: hit.threadId,
|
|
28081
|
-
accountId: hit.accountId,
|
|
28082
|
-
mailbox: hit.mailbox,
|
|
28083
|
-
subject: hit.subject,
|
|
28084
|
-
from: hit.from,
|
|
28085
|
-
to: hit.to,
|
|
28086
|
-
date: hit.date
|
|
28087
|
-
}
|
|
28088
|
-
};
|
|
28089
|
-
}).slice(0, options.topK ?? 20);
|
|
28090
|
-
}
|
|
28091
27980
|
}
|
|
28092
|
-
var
|
|
27981
|
+
var DEFAULT_TOP_K5 = 20, DESCRIPTIONS;
|
|
28093
27982
|
var init_methods4 = __esm(() => {
|
|
28094
|
-
init_errors4();
|
|
28095
27983
|
init_scope2();
|
|
28096
|
-
|
|
28097
|
-
|
|
27984
|
+
DESCRIPTIONS = {
|
|
27985
|
+
bm25: { type: "bm25", text: "BM25-ranked FTS5 retrieval", capabilities: ["lexical", "fts5", "bm25"] },
|
|
27986
|
+
semantic: { type: "vector", text: "Semantic (embedding) retrieval", capabilities: ["semantic"] },
|
|
27987
|
+
hybrid: {
|
|
27988
|
+
type: "hybrid",
|
|
27989
|
+
text: "Hybrid BM25 + semantic retrieval (reciprocal rank fusion)",
|
|
27990
|
+
capabilities: ["lexical", "semantic", "hybrid"]
|
|
27991
|
+
}
|
|
27992
|
+
};
|
|
28098
27993
|
});
|
|
28099
27994
|
|
|
28100
|
-
// src/datasource/skills/
|
|
28101
|
-
class
|
|
27995
|
+
// src/datasource/skills/lazykatok/skill.ts
|
|
27996
|
+
class LazykatokSkill {
|
|
28102
27997
|
client;
|
|
28103
27998
|
instanceId;
|
|
28104
27999
|
instances;
|
|
28105
|
-
|
|
28000
|
+
pollingIntervalMs;
|
|
28106
28001
|
tags;
|
|
28107
|
-
|
|
28108
|
-
mailbox;
|
|
28002
|
+
sidecar;
|
|
28109
28003
|
lastIndexedAt;
|
|
28110
|
-
constructor(options
|
|
28111
|
-
|
|
28112
|
-
this.instanceId = options.instanceId ??
|
|
28113
|
-
this.instances = options.instances
|
|
28114
|
-
|
|
28115
|
-
|
|
28116
|
-
|
|
28117
|
-
this.
|
|
28118
|
-
|
|
28119
|
-
|
|
28120
|
-
|
|
28121
|
-
|
|
28004
|
+
constructor(options) {
|
|
28005
|
+
this.client = options.client;
|
|
28006
|
+
this.instanceId = options.instanceId ?? DEFAULT_INSTANCE_ID4;
|
|
28007
|
+
this.instances = options.instances !== undefined && options.instances.length > 0 ? options.instances : [this.instanceId];
|
|
28008
|
+
this.pollingIntervalMs = options.pollingIntervalMs ?? DEFAULT_POLLING_INTERVAL_MS3;
|
|
28009
|
+
this.tags = options.tags ?? DEFAULT_KAKAO_TAGS2;
|
|
28010
|
+
this.lastIndexedAt = options.lastIndexedAt;
|
|
28011
|
+
this.sidecar = options.sidecar === undefined ? undefined : new CrawlerSidecar({
|
|
28012
|
+
...options.sidecar,
|
|
28013
|
+
channelIds: options.channelIds ?? options.sidecar.channelIds ?? [],
|
|
28014
|
+
channelNames: options.channelNames ?? options.sidecar.channelNames ?? []
|
|
28015
|
+
});
|
|
28122
28016
|
}
|
|
28123
28017
|
describe() {
|
|
28124
28018
|
return {
|
|
28125
|
-
name:
|
|
28126
|
-
id:
|
|
28127
|
-
type:
|
|
28128
|
-
description: "
|
|
28129
|
-
capabilities: ["
|
|
28019
|
+
name: KAKAO_DATASOURCE_ID2,
|
|
28020
|
+
id: KAKAO_DATASOURCE_ID2,
|
|
28021
|
+
type: KAKAO_SKILL_TYPE,
|
|
28022
|
+
description: "KakaoTalk datasource via the external lazykatok CLI",
|
|
28023
|
+
capabilities: ["chat", "external-cli", "polling", "bm25", "semantic"],
|
|
28130
28024
|
tags: this.tags,
|
|
28131
28025
|
status: "active",
|
|
28132
28026
|
requiresExternalCli: true,
|
|
28133
|
-
datasourceId:
|
|
28027
|
+
datasourceId: KAKAO_DATASOURCE_ID2,
|
|
28134
28028
|
instanceId: this.instanceId,
|
|
28135
28029
|
instances: this.instances
|
|
28136
28030
|
};
|
|
28137
28031
|
}
|
|
28138
28032
|
polling() {
|
|
28139
28033
|
return {
|
|
28140
|
-
mode:
|
|
28141
|
-
|
|
28034
|
+
mode: "poll",
|
|
28035
|
+
intervalMs: this.pollingIntervalMs,
|
|
28142
28036
|
lastIndexedAt: this.lastIndexedAt
|
|
28143
28037
|
};
|
|
28144
28038
|
}
|
|
28145
28039
|
async index(options) {
|
|
28146
28040
|
try {
|
|
28147
|
-
const
|
|
28148
|
-
if (!
|
|
28149
|
-
return this.fail(
|
|
28150
|
-
|
|
28151
|
-
|
|
28152
|
-
|
|
28153
|
-
if (
|
|
28154
|
-
|
|
28155
|
-
|
|
28156
|
-
|
|
28157
|
-
message: `mailcrawl semantic index unavailable (${indexed.reason}); BM25 remains available`,
|
|
28041
|
+
const doctor = await this.client.doctor(options?.signal);
|
|
28042
|
+
if (!doctor.ok)
|
|
28043
|
+
return this.fail("datasource-unavailable", doctor);
|
|
28044
|
+
const sync = await this.client.sync(options?.signal);
|
|
28045
|
+
if (!sync.ok)
|
|
28046
|
+
return this.fail("datasource-index-failed", sync);
|
|
28047
|
+
if (this.sidecar !== undefined) {
|
|
28048
|
+
this.lastIndexedAt = Date.now();
|
|
28049
|
+
return {
|
|
28050
|
+
ok: true,
|
|
28158
28051
|
instanceId: this.instanceId,
|
|
28159
|
-
|
|
28160
|
-
|
|
28052
|
+
skill: KAKAO_DATASOURCE_ID2,
|
|
28053
|
+
chunkCount: syncChunkCount(sync),
|
|
28054
|
+
indexedAt: this.lastIndexedAt,
|
|
28055
|
+
diagnostics: await this.syncSidecar()
|
|
28056
|
+
};
|
|
28161
28057
|
}
|
|
28058
|
+
const indexResult = await this.client.index(options?.signal);
|
|
28059
|
+
if (!indexResult.ok)
|
|
28060
|
+
return this.fail("datasource-index-failed", indexResult);
|
|
28162
28061
|
this.lastIndexedAt = Date.now();
|
|
28062
|
+
const chunkCount = "data" in indexResult && typeof indexResult.data === "object" && indexResult.data !== null && "chunkCount" in indexResult.data && typeof indexResult.data.chunkCount === "number" ? indexResult.data.chunkCount : 0;
|
|
28163
28063
|
return {
|
|
28164
28064
|
ok: true,
|
|
28165
28065
|
instanceId: this.instanceId,
|
|
28166
|
-
skill:
|
|
28167
|
-
chunkCount
|
|
28066
|
+
skill: KAKAO_DATASOURCE_ID2,
|
|
28067
|
+
chunkCount,
|
|
28168
28068
|
indexedAt: this.lastIndexedAt,
|
|
28169
|
-
diagnostics
|
|
28069
|
+
diagnostics: []
|
|
28170
28070
|
};
|
|
28171
|
-
} catch {
|
|
28172
|
-
return this.fail("datasource-unavailable",
|
|
28071
|
+
} catch (error) {
|
|
28072
|
+
return this.fail("datasource-unavailable", {
|
|
28073
|
+
ok: false,
|
|
28074
|
+
reason: "spawn-error",
|
|
28075
|
+
stdout: "",
|
|
28076
|
+
stderr: describeRetrievalError(error),
|
|
28077
|
+
code: null
|
|
28078
|
+
});
|
|
28173
28079
|
}
|
|
28174
28080
|
}
|
|
28081
|
+
close() {
|
|
28082
|
+
this.sidecar?.close();
|
|
28083
|
+
}
|
|
28175
28084
|
retrievalMethods() {
|
|
28085
|
+
const cliBm25 = new LazykatokBm25Method({ client: this.client, instanceId: this.instanceId, tags: this.tags });
|
|
28086
|
+
const { sidecar } = this;
|
|
28087
|
+
if (sidecar === undefined) {
|
|
28088
|
+
return [
|
|
28089
|
+
cliBm25,
|
|
28090
|
+
new LazykatokSemanticMethod({ client: this.client, instanceId: this.instanceId, tags: this.tags })
|
|
28091
|
+
];
|
|
28092
|
+
}
|
|
28093
|
+
const shared = {
|
|
28094
|
+
sidecar,
|
|
28095
|
+
descriptor: sidecar.descriptor,
|
|
28096
|
+
datasourceId: KAKAO_DATASOURCE_ID2,
|
|
28097
|
+
skillType: KAKAO_SKILL_TYPE,
|
|
28098
|
+
instanceId: this.instanceId,
|
|
28099
|
+
tags: this.tags
|
|
28100
|
+
};
|
|
28101
|
+
const bm25 = new SidecarRetrievalMethod({ ...shared, mode: "bm25" });
|
|
28102
|
+
if (!sidecar.semanticEnabled)
|
|
28103
|
+
return [cliBm25, bm25];
|
|
28176
28104
|
return [
|
|
28177
|
-
|
|
28178
|
-
|
|
28179
|
-
new
|
|
28105
|
+
cliBm25,
|
|
28106
|
+
bm25,
|
|
28107
|
+
new SidecarRetrievalMethod({ ...shared, mode: "semantic" }),
|
|
28108
|
+
new SidecarRetrievalMethod({ ...shared, mode: "hybrid" })
|
|
28180
28109
|
];
|
|
28181
28110
|
}
|
|
28111
|
+
async syncSidecar() {
|
|
28112
|
+
if (this.sidecar === undefined || !this.sidecar.semanticEnabled)
|
|
28113
|
+
return [];
|
|
28114
|
+
const base = { instanceId: this.instanceId, source: KAKAO_DATASOURCE_ID2 };
|
|
28115
|
+
let result;
|
|
28116
|
+
try {
|
|
28117
|
+
result = await this.sidecar.sync();
|
|
28118
|
+
} catch (error) {
|
|
28119
|
+
result = { ok: false, message: error instanceof Error ? error.message : String(error) };
|
|
28120
|
+
}
|
|
28121
|
+
if (!result.ok) {
|
|
28122
|
+
return [
|
|
28123
|
+
{
|
|
28124
|
+
code: "datasource-index-failed",
|
|
28125
|
+
severity: "warning",
|
|
28126
|
+
message: `semantic-unavailable: ${result.message}`.slice(0, 300),
|
|
28127
|
+
...base
|
|
28128
|
+
}
|
|
28129
|
+
];
|
|
28130
|
+
}
|
|
28131
|
+
const diagnostics = [];
|
|
28132
|
+
if (result.skipped > 0) {
|
|
28133
|
+
diagnostics.push({
|
|
28134
|
+
code: "datasource-index-partial",
|
|
28135
|
+
severity: "warning",
|
|
28136
|
+
message: `semantic index skipped ${result.skipped} message(s) the embedder rejected; they stay searchable by BM25 and are retried when edited`,
|
|
28137
|
+
...base
|
|
28138
|
+
});
|
|
28139
|
+
}
|
|
28140
|
+
if (result.remaining > 0) {
|
|
28141
|
+
diagnostics.push({
|
|
28142
|
+
code: "datasource-index-partial",
|
|
28143
|
+
severity: "info",
|
|
28144
|
+
message: `semantic index catching up: ${result.embedded} embedded this refresh, ${result.remaining} remaining; hybrid search uses BM25 for the rest until a later refresh`,
|
|
28145
|
+
...base
|
|
28146
|
+
});
|
|
28147
|
+
}
|
|
28148
|
+
return diagnostics;
|
|
28149
|
+
}
|
|
28182
28150
|
describeSources() {
|
|
28183
28151
|
return this.instances.map((instanceId) => ({
|
|
28184
|
-
source: datasourceSourcePath(
|
|
28185
|
-
datasourceId:
|
|
28186
|
-
skill:
|
|
28152
|
+
source: datasourceSourcePath(KAKAO_DATASOURCE_ID2, instanceId),
|
|
28153
|
+
datasourceId: KAKAO_DATASOURCE_ID2,
|
|
28154
|
+
skill: KAKAO_DATASOURCE_ID2,
|
|
28187
28155
|
instanceId,
|
|
28188
|
-
contentType: "
|
|
28189
|
-
metadata: {
|
|
28190
|
-
|
|
28191
|
-
|
|
28192
|
-
|
|
28193
|
-
|
|
28194
|
-
|
|
28195
|
-
|
|
28196
|
-
|
|
28197
|
-
|
|
28198
|
-
|
|
28199
|
-
|
|
28200
|
-
|
|
28201
|
-
|
|
28202
|
-
|
|
28203
|
-
|
|
28204
|
-
|
|
28205
|
-
|
|
28206
|
-
|
|
28207
|
-
|
|
28208
|
-
|
|
28209
|
-
|
|
28210
|
-
|
|
28211
|
-
##
|
|
28212
|
-
|
|
28213
|
-
|
|
28214
|
-
|
|
28156
|
+
contentType: "chat",
|
|
28157
|
+
metadata: {
|
|
28158
|
+
datasourceId: KAKAO_DATASOURCE_ID2,
|
|
28159
|
+
instanceId,
|
|
28160
|
+
tags: this.tags
|
|
28161
|
+
}
|
|
28162
|
+
}));
|
|
28163
|
+
}
|
|
28164
|
+
skillManifest() {
|
|
28165
|
+
const instanceSources = this.instances.map((instanceId) => `- \`${datasourceSourcePath(KAKAO_DATASOURCE_ID2, instanceId)}\``).join(`
|
|
28166
|
+
`);
|
|
28167
|
+
const cadence = this.pollingIntervalMs > 0 ? `roughly every ${Math.round(this.pollingIntervalMs / 60000)} minute(s) when auto-refresh runs` : "on manual refresh only";
|
|
28168
|
+
return {
|
|
28169
|
+
name: `datasource-${KAKAO_DATASOURCE_ID2}`,
|
|
28170
|
+
description: "Search indexed KakaoTalk chat history (messages, senders, room context). Use for questions about KakaoTalk conversations, decisions made in chats, or who said what.",
|
|
28171
|
+
content: [
|
|
28172
|
+
`# KakaoTalk datasource (${KAKAO_SKILL_TYPE})`,
|
|
28173
|
+
"",
|
|
28174
|
+
"This skill searches KakaoTalk chats that are indexed through the external `lazykatok` CLI. AutoRAG never reads KakaoTalk databases directly.",
|
|
28175
|
+
"",
|
|
28176
|
+
"## When to use",
|
|
28177
|
+
"Use this skill when the question is about KakaoTalk conversations, chat participants, or content shared inside chats.",
|
|
28178
|
+
"",
|
|
28179
|
+
"## Indexing",
|
|
28180
|
+
`Indexing is server-managed and refreshed ${cadence}. You do not trigger indexing; just search.`,
|
|
28181
|
+
"",
|
|
28182
|
+
"## How to search",
|
|
28183
|
+
`Call the dedicated \`${datasourceSearchToolName(KAKAO_DATASOURCE_ID2)}\` tool with a natural-language \`query\` and \`topK\`. This datasource does not support per-source scope narrowing. Configured datasource instances:`,
|
|
28184
|
+
instanceSources.length > 0 ? instanceSources : "- (no configured instances)",
|
|
28185
|
+
"",
|
|
28186
|
+
"Chat/channel filtering is owned by lazykatok.",
|
|
28187
|
+
"",
|
|
28188
|
+
"## Native CLI",
|
|
28189
|
+
"The external `lazykatok` CLI owns the archive, index, and credentials. When the dedicated tool cannot express what you need, call lazykatok directly through `bash`:",
|
|
28190
|
+
'- `lazykatok search bm25 "<query>" --json --limit 20` — lexical search (`semantic` mode also available)',
|
|
28191
|
+
"- `lazykatok chunk get <chunkId> --json` — fetch one chunk by id (chunk ids appear in result metadata)",
|
|
28192
|
+
"- `lazykatok chunk context <chunkId> --json` — surrounding messages of a chunk",
|
|
28193
|
+
"Never pass datasource virtual paths (`/kakao/...`) to bash; they are not OS paths.",
|
|
28194
|
+
"",
|
|
28195
|
+
"## Output rules",
|
|
28196
|
+
"Datasource source identifiers such as `/kakao/<instance>/chunks/<id>` are internal and opaque. Never put them, real file paths, account IDs, or phone numbers in the visible answer."
|
|
28197
|
+
].join(`
|
|
28198
|
+
`)
|
|
28215
28199
|
};
|
|
28216
28200
|
}
|
|
28217
|
-
fail(code,
|
|
28201
|
+
fail(code, result) {
|
|
28202
|
+
const base = result.stderr.length > 0 ? `${result.reason}: ${result.stderr.trim().slice(0, 4000)}` : result.reason;
|
|
28203
|
+
const message = code === "datasource-index-failed" && result.reason === "timeout" ? `${base} — ${connectorSyncTimeoutHint()}` : base;
|
|
28204
|
+
const diagnostic = {
|
|
28205
|
+
code,
|
|
28206
|
+
severity: code === "datasource-unavailable" ? "warning" : "error",
|
|
28207
|
+
message,
|
|
28208
|
+
instanceId: this.instanceId,
|
|
28209
|
+
source: KAKAO_DATASOURCE_ID2
|
|
28210
|
+
};
|
|
28218
28211
|
return {
|
|
28219
28212
|
ok: false,
|
|
28220
28213
|
instanceId: this.instanceId,
|
|
28221
|
-
skill:
|
|
28214
|
+
skill: KAKAO_DATASOURCE_ID2,
|
|
28222
28215
|
indexedAt: Date.now(),
|
|
28223
|
-
diagnostics: [
|
|
28224
|
-
|
|
28225
|
-
code,
|
|
28226
|
-
severity: code === "datasource-unavailable" ? "warning" : "error",
|
|
28227
|
-
message: `mailcrawl ${message}`,
|
|
28228
|
-
instanceId: this.instanceId,
|
|
28229
|
-
source: "mailcrawl"
|
|
28230
|
-
}
|
|
28231
|
-
],
|
|
28232
|
-
error: message,
|
|
28216
|
+
diagnostics: [diagnostic],
|
|
28217
|
+
error: result.reason,
|
|
28233
28218
|
code,
|
|
28234
28219
|
message
|
|
28235
28220
|
};
|
|
28236
28221
|
}
|
|
28237
28222
|
}
|
|
28238
|
-
|
|
28239
|
-
|
|
28223
|
+
function syncChunkCount(sync) {
|
|
28224
|
+
if (!("data" in sync) || typeof sync.data !== "object" || sync.data === null)
|
|
28225
|
+
return 0;
|
|
28226
|
+
const meta = "metadata" in sync.data && typeof sync.data.metadata === "object" && sync.data.metadata !== null ? sync.data.metadata : undefined;
|
|
28227
|
+
if (typeof meta?.chunks === "number")
|
|
28228
|
+
return meta.chunks;
|
|
28229
|
+
if ("messageCount" in sync.data && typeof sync.data.messageCount === "number")
|
|
28230
|
+
return sync.data.messageCount;
|
|
28231
|
+
return 0;
|
|
28232
|
+
}
|
|
28233
|
+
var KAKAO_DATASOURCE_ID2 = "kakao", KAKAO_SKILL_TYPE = "kakaotalk", DEFAULT_INSTANCE_ID4 = "default", DEFAULT_POLLING_INTERVAL_MS3, DEFAULT_KAKAO_TAGS2;
|
|
28234
|
+
var init_skill7 = __esm(() => {
|
|
28235
|
+
init_skip();
|
|
28240
28236
|
init_scope2();
|
|
28241
|
-
|
|
28242
|
-
init_config3();
|
|
28237
|
+
init_sidecar();
|
|
28243
28238
|
init_methods4();
|
|
28244
|
-
|
|
28239
|
+
init_methods3();
|
|
28240
|
+
DEFAULT_POLLING_INTERVAL_MS3 = 15 * 60 * 1000;
|
|
28241
|
+
DEFAULT_KAKAO_TAGS2 = ["kakaotalk", "personal", "pii"];
|
|
28245
28242
|
});
|
|
28246
28243
|
|
|
28247
|
-
// src/datasource/skills/
|
|
28248
|
-
var
|
|
28249
|
-
|
|
28250
|
-
|
|
28251
|
-
|
|
28244
|
+
// src/datasource/skills/lazykatok/index.ts
|
|
28245
|
+
var init_lazykatok = __esm(() => {
|
|
28246
|
+
init_client8();
|
|
28247
|
+
init_methods3();
|
|
28248
|
+
init_skill7();
|
|
28249
|
+
init_types5();
|
|
28250
|
+
});
|
|
28251
|
+
|
|
28252
|
+
// src/datasource/skills/mail-export/connector.ts
|
|
28253
|
+
import { readdir as readdir4, readFile as readFile5, stat as stat6 } from "node:fs/promises";
|
|
28254
|
+
import { basename as basename12, dirname as dirname25, extname as extname6, join as join37 } from "node:path";
|
|
28255
|
+
import { simpleParser as simpleParser2 } from "mailparser";
|
|
28256
|
+
|
|
28257
|
+
class MailExportConnector {
|
|
28258
|
+
options;
|
|
28259
|
+
constructor(options = {}) {
|
|
28260
|
+
this.options = options;
|
|
28261
|
+
}
|
|
28262
|
+
async fetch() {
|
|
28263
|
+
const paths = this.options.paths ?? [];
|
|
28264
|
+
if (paths.length === 0)
|
|
28265
|
+
return { ok: false, reason: "not-configured", message: "no mail export paths configured" };
|
|
28266
|
+
const maxDocuments = this.options.maxDocuments ?? DEFAULT_MAX_DOCUMENTS3;
|
|
28267
|
+
const maxBytes = this.options.maxBytesPerFile ?? DEFAULT_MAX_BYTES_PER_FILE2;
|
|
28268
|
+
const files = [];
|
|
28269
|
+
let missingPaths = 0;
|
|
28270
|
+
for (const path of paths) {
|
|
28271
|
+
try {
|
|
28272
|
+
const info = await stat6(path);
|
|
28273
|
+
if (info.isDirectory()) {
|
|
28274
|
+
const entries = await readdir4(path, { recursive: true, withFileTypes: true });
|
|
28275
|
+
for (const entry of entries) {
|
|
28276
|
+
if (!entry.isFile())
|
|
28277
|
+
continue;
|
|
28278
|
+
const extension = extname6(entry.name).toLowerCase();
|
|
28279
|
+
if (extension === ".eml" || extension === ".mbox") {
|
|
28280
|
+
files.push(join37(entry.parentPath, entry.name));
|
|
28281
|
+
}
|
|
28282
|
+
}
|
|
28283
|
+
} else if (info.isFile()) {
|
|
28284
|
+
files.push(path);
|
|
28285
|
+
}
|
|
28286
|
+
} catch {
|
|
28287
|
+
missingPaths += 1;
|
|
28288
|
+
}
|
|
28289
|
+
}
|
|
28290
|
+
if (missingPaths === paths.length) {
|
|
28291
|
+
return { ok: false, reason: "unavailable", message: "no configured mail export paths were readable" };
|
|
28292
|
+
}
|
|
28293
|
+
const documents = [];
|
|
28294
|
+
const warnings = [];
|
|
28295
|
+
if (missingPaths > 0)
|
|
28296
|
+
warnings.push(`${missingPaths} configured path(s) were unreadable`);
|
|
28297
|
+
let parseFailures = 0;
|
|
28298
|
+
let skippedLarge = 0;
|
|
28299
|
+
for (const file of files.sort()) {
|
|
28300
|
+
if (documents.length >= maxDocuments)
|
|
28301
|
+
break;
|
|
28302
|
+
let bytes;
|
|
28303
|
+
try {
|
|
28304
|
+
const info = await stat6(file);
|
|
28305
|
+
if (info.size > maxBytes) {
|
|
28306
|
+
skippedLarge += 1;
|
|
28307
|
+
continue;
|
|
28308
|
+
}
|
|
28309
|
+
bytes = await readFile5(file);
|
|
28310
|
+
} catch {
|
|
28311
|
+
parseFailures += 1;
|
|
28312
|
+
continue;
|
|
28313
|
+
}
|
|
28314
|
+
const stem = basename12(file, extname6(file));
|
|
28315
|
+
const mailboxDir = basename12(dirname25(file));
|
|
28316
|
+
const hierarchy = ["mailboxes", mailboxDir, stem];
|
|
28317
|
+
const extension = extname6(file).toLowerCase();
|
|
28318
|
+
const rawMessages = extension === ".mbox" ? splitMbox(bytes.toString("utf8")) : [bytes.toString("utf8")];
|
|
28319
|
+
for (const [index, raw] of rawMessages.entries()) {
|
|
28320
|
+
if (documents.length >= maxDocuments)
|
|
28321
|
+
break;
|
|
28322
|
+
try {
|
|
28323
|
+
const parsed = await simpleParser2(raw);
|
|
28324
|
+
const subject = parsed.subject ?? "(no subject)";
|
|
28325
|
+
const from = Array.isArray(parsed.from) ? parsed.from[0]?.text : parsed.from?.text;
|
|
28326
|
+
const to = Array.isArray(parsed.to) ? parsed.to[0]?.text : parsed.to?.text;
|
|
28327
|
+
const body = (parsed.text ?? "").trim().slice(0, MAX_BODY_CHARS);
|
|
28328
|
+
const headerBlock = [
|
|
28329
|
+
`Subject: ${subject}`,
|
|
28330
|
+
...from !== undefined ? [`From: ${from}`] : [],
|
|
28331
|
+
...to !== undefined ? [`To: ${to}`] : [],
|
|
28332
|
+
...parsed.date !== undefined ? [`Date: ${parsed.date.toISOString()}`] : []
|
|
28333
|
+
].join(`
|
|
28334
|
+
`);
|
|
28335
|
+
const content = `${headerBlock}
|
|
28336
|
+
|
|
28337
|
+
${body}`.trim();
|
|
28338
|
+
if (content.length === 0)
|
|
28339
|
+
continue;
|
|
28340
|
+
documents.push({
|
|
28341
|
+
docId: extension === ".mbox" ? `${stem}-${index + 1}` : stem,
|
|
28342
|
+
hierarchy,
|
|
28343
|
+
title: subject,
|
|
28344
|
+
content,
|
|
28345
|
+
...parsed.date !== undefined ? { publishedAt: parsed.date.getTime() } : {},
|
|
28346
|
+
metadata: {
|
|
28347
|
+
path: file,
|
|
28348
|
+
subject,
|
|
28349
|
+
...from !== undefined ? { from } : {},
|
|
28350
|
+
messageIndex: index
|
|
28351
|
+
}
|
|
28352
|
+
});
|
|
28353
|
+
} catch {
|
|
28354
|
+
parseFailures += 1;
|
|
28355
|
+
}
|
|
28356
|
+
}
|
|
28357
|
+
}
|
|
28358
|
+
if (skippedLarge > 0)
|
|
28359
|
+
warnings.push(`${skippedLarge} file(s) exceeded the size limit and were skipped`);
|
|
28360
|
+
if (parseFailures > 0)
|
|
28361
|
+
warnings.push(`${parseFailures} message(s) failed to parse`);
|
|
28362
|
+
return { ok: true, documents, ...warnings.length > 0 ? { warnings } : {} };
|
|
28363
|
+
}
|
|
28364
|
+
}
|
|
28365
|
+
function splitMbox(text) {
|
|
28366
|
+
const lines = text.split(/\r?\n/u);
|
|
28367
|
+
const messages = [];
|
|
28368
|
+
let current = [];
|
|
28369
|
+
for (const line of lines) {
|
|
28370
|
+
if (MBOX_SEPARATOR.test(line) && line.startsWith("From ")) {
|
|
28371
|
+
if (current.length > 0)
|
|
28372
|
+
messages.push(current.join(`
|
|
28373
|
+
`));
|
|
28374
|
+
current = [];
|
|
28375
|
+
continue;
|
|
28376
|
+
}
|
|
28377
|
+
current.push(line);
|
|
28378
|
+
}
|
|
28379
|
+
if (current.length > 0)
|
|
28380
|
+
messages.push(current.join(`
|
|
28381
|
+
`));
|
|
28382
|
+
return messages.filter((message) => message.trim().length > 0);
|
|
28383
|
+
}
|
|
28384
|
+
var DEFAULT_MAX_DOCUMENTS3 = 500, DEFAULT_MAX_BYTES_PER_FILE2, MAX_BODY_CHARS = 50000, MBOX_SEPARATOR;
|
|
28385
|
+
var init_connector4 = __esm(() => {
|
|
28386
|
+
DEFAULT_MAX_BYTES_PER_FILE2 = 50 * 1024 * 1024;
|
|
28387
|
+
MBOX_SEPARATOR = /^From .*\d{4}$/mu;
|
|
28388
|
+
});
|
|
28389
|
+
|
|
28390
|
+
// src/datasource/skills/mail-export/skill.ts
|
|
28391
|
+
var MAIL_EXPORT_SKILL_DEFINITION, MailExportSkill;
|
|
28392
|
+
var init_skill8 = __esm(() => {
|
|
28393
|
+
init_connector_skill();
|
|
28394
|
+
init_connector4();
|
|
28395
|
+
MAIL_EXPORT_SKILL_DEFINITION = {
|
|
28396
|
+
skillName: "mail-export",
|
|
28397
|
+
skillType: "mail-export",
|
|
28398
|
+
description: "Local mail export datasource",
|
|
28399
|
+
capabilities: ["email", "filesystem"],
|
|
28400
|
+
defaultTags: ["mail-export", "email", "pii"],
|
|
28401
|
+
contentType: "email",
|
|
28402
|
+
manifestDescription: "Search indexed local mail exports (mbox/eml archives). Use for questions about archived email conversations.",
|
|
28403
|
+
manifestNotes: [
|
|
28404
|
+
"Only the server-configured export paths are indexed; archives are read locally and never uploaded."
|
|
28405
|
+
]
|
|
28406
|
+
};
|
|
28407
|
+
MailExportSkill = class MailExportSkill extends ConnectorDatasourceSkill {
|
|
28408
|
+
constructor(options = {}) {
|
|
28409
|
+
const { connector, connectorOptions, ...rest } = options;
|
|
28410
|
+
super(MAIL_EXPORT_SKILL_DEFINITION, {
|
|
28411
|
+
...rest,
|
|
28412
|
+
connector: connector ?? new MailExportConnector(connectorOptions ?? {})
|
|
28413
|
+
});
|
|
28414
|
+
}
|
|
28415
|
+
};
|
|
28416
|
+
});
|
|
28417
|
+
|
|
28418
|
+
// src/datasource/skills/mail-export/index.ts
|
|
28419
|
+
var init_mail_export = __esm(() => {
|
|
28420
|
+
init_connector4();
|
|
28421
|
+
init_skill8();
|
|
28252
28422
|
});
|
|
28253
28423
|
|
|
28254
|
-
// src/datasource/
|
|
28255
|
-
|
|
28256
|
-
|
|
28257
|
-
|
|
28258
|
-
|
|
28259
|
-
function decodeVector(blob) {
|
|
28260
|
-
if (blob.byteLength % Float32Array.BYTES_PER_ELEMENT !== 0)
|
|
28261
|
-
return;
|
|
28262
|
-
return new Float32Array(new Uint8Array(blob).buffer);
|
|
28263
|
-
}
|
|
28264
|
-
function cosine3(a, b) {
|
|
28265
|
-
if (a.length === 0 || a.length !== b.length)
|
|
28266
|
-
return 0;
|
|
28267
|
-
let dot = 0;
|
|
28268
|
-
let normA = 0;
|
|
28269
|
-
let normB = 0;
|
|
28270
|
-
for (let index = 0;index < a.length; index += 1) {
|
|
28271
|
-
const x = a[index] ?? 0;
|
|
28272
|
-
const y = b[index] ?? 0;
|
|
28273
|
-
dot += x * y;
|
|
28274
|
-
normA += x * x;
|
|
28275
|
-
normB += y * y;
|
|
28276
|
-
}
|
|
28277
|
-
if (normA === 0 || normB === 0)
|
|
28278
|
-
return 0;
|
|
28279
|
-
return dot / Math.sqrt(normA * normB);
|
|
28280
|
-
}
|
|
28281
|
-
function topKByCosine(query, rows, k) {
|
|
28282
|
-
if (k <= 0)
|
|
28283
|
-
return [];
|
|
28284
|
-
const scored = [];
|
|
28285
|
-
for (const row of rows) {
|
|
28286
|
-
const score = cosine3(query, row.vector);
|
|
28287
|
-
if (score > 0)
|
|
28288
|
-
scored.push({ key: row.key, score });
|
|
28424
|
+
// src/datasource/skills/mailcrawl/config.ts
|
|
28425
|
+
import { chmodSync, mkdirSync as mkdirSync23 } from "node:fs";
|
|
28426
|
+
function validateMailcrawlInstanceId(instanceId) {
|
|
28427
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/u.test(instanceId)) {
|
|
28428
|
+
throw new Error("mailcrawl instanceId must be a safe single path segment");
|
|
28289
28429
|
}
|
|
28290
|
-
scored.sort((a, b) => b.score - a.score || a.key.localeCompare(b.key));
|
|
28291
|
-
return scored.slice(0, k);
|
|
28292
28430
|
}
|
|
28293
|
-
function
|
|
28294
|
-
|
|
28295
|
-
|
|
28296
|
-
|
|
28297
|
-
for (const [index, key] of list.entries()) {
|
|
28298
|
-
if (seen.has(key))
|
|
28299
|
-
continue;
|
|
28300
|
-
seen.add(key);
|
|
28301
|
-
scores.set(key, (scores.get(key) ?? 0) + 1 / (RRF_K2 + index + 1));
|
|
28302
|
-
}
|
|
28303
|
-
}
|
|
28304
|
-
return [...scores.entries()].map(([key, score]) => ({ key, score })).sort((a, b) => b.score - a.score);
|
|
28431
|
+
function ensurePrivateMailcrawlDataDir(path) {
|
|
28432
|
+
mkdirSync23(path, { recursive: true, mode: 448 });
|
|
28433
|
+
if (process.platform !== "win32")
|
|
28434
|
+
chmodSync(path, 448);
|
|
28305
28435
|
}
|
|
28306
|
-
var
|
|
28436
|
+
var init_config3 = () => {};
|
|
28307
28437
|
|
|
28308
|
-
// src/datasource/
|
|
28309
|
-
import {
|
|
28310
|
-
import { mkdirSync as mkdirSync23, statSync as statSync3 } from "node:fs";
|
|
28311
|
-
import { dirname as dirname25 } from "node:path";
|
|
28312
|
-
import { DatabaseSync as DatabaseSync2 } from "node:sqlite";
|
|
28313
|
-
import { pathToFileURL as pathToFileURL2 } from "node:url";
|
|
28438
|
+
// src/datasource/skills/mailcrawl/client.ts
|
|
28439
|
+
import { spawn as spawn15 } from "node:child_process";
|
|
28314
28440
|
|
|
28315
|
-
class
|
|
28316
|
-
descriptor;
|
|
28441
|
+
class MailcrawlClient {
|
|
28317
28442
|
options;
|
|
28318
|
-
|
|
28319
|
-
where;
|
|
28320
|
-
opened;
|
|
28321
|
-
attached;
|
|
28322
|
-
lastSemantic;
|
|
28323
|
-
constructor(options) {
|
|
28443
|
+
constructor(options = {}) {
|
|
28324
28444
|
this.options = options;
|
|
28325
|
-
|
|
28326
|
-
|
|
28327
|
-
const metadataColumns = descriptor.metadata.map((column, index) => `${column.sql} as m${index}`);
|
|
28328
|
-
this.select = [
|
|
28329
|
-
`${descriptor.keyExpression} as k`,
|
|
28330
|
-
`${descriptor.textExpression} as content`,
|
|
28331
|
-
`${descriptor.titleExpression ?? "null"} as title`,
|
|
28332
|
-
`${descriptor.epochSecondsExpression ?? "null"} as epoch`,
|
|
28333
|
-
...metadataColumns
|
|
28334
|
-
].join(", ");
|
|
28335
|
-
const scope = descriptor.scopeExpression !== undefined && options.workspaceId !== undefined;
|
|
28336
|
-
const allowlist = [
|
|
28337
|
-
...(options.channelIds?.length ?? 0) > 0 ? [`${descriptor.channelIdExpression} in (${options.channelIds?.map(() => "?").join(", ")})`] : [],
|
|
28338
|
-
...(options.channelNames?.length ?? 0) > 0 ? [`${descriptor.channelNameExpression} in (${options.channelNames?.map(() => "?").join(", ")})`] : []
|
|
28339
|
-
];
|
|
28340
|
-
this.where = [
|
|
28341
|
-
descriptor.liveWhere,
|
|
28342
|
-
...scope ? [`${descriptor.scopeExpression} = ?`] : [],
|
|
28343
|
-
...allowlist.length > 0 ? [`(${allowlist.join(" or ")})`] : []
|
|
28344
|
-
].join(" and ");
|
|
28345
|
-
}
|
|
28346
|
-
get db() {
|
|
28347
|
-
if (this.opened !== undefined)
|
|
28348
|
-
return this.opened;
|
|
28349
|
-
mkdirSync23(dirname25(this.options.sidecarDbPath), { recursive: true });
|
|
28350
|
-
const db = new DatabaseSync2(this.options.sidecarDbPath);
|
|
28351
|
-
db.exec(`pragma busy_timeout = ${BUSY_TIMEOUT_MS}`);
|
|
28352
|
-
db.exec("pragma journal_mode = wal");
|
|
28353
|
-
db.exec(`
|
|
28354
|
-
create table if not exists sidecar_meta (k text primary key, v text not null);
|
|
28355
|
-
create table if not exists sidecar_vec (message_key text primary key, hash text not null, vec blob not null);
|
|
28356
|
-
`);
|
|
28357
|
-
this.opened = db;
|
|
28358
|
-
return db;
|
|
28359
|
-
}
|
|
28360
|
-
attachCrawler() {
|
|
28361
|
-
let file;
|
|
28362
|
-
try {
|
|
28363
|
-
const { dev, ino } = statSync3(this.options.crawlerDbPath);
|
|
28364
|
-
file = { dev, ino };
|
|
28365
|
-
} catch {
|
|
28366
|
-
this.detachCrawler();
|
|
28367
|
-
return false;
|
|
28368
|
-
}
|
|
28369
|
-
if (this.attached !== undefined) {
|
|
28370
|
-
if (this.attached.dev === file.dev && this.attached.ino === file.ino)
|
|
28371
|
-
return true;
|
|
28372
|
-
this.detachCrawler();
|
|
28373
|
-
}
|
|
28374
|
-
const crawlerUri = `${pathToFileURL2(this.options.crawlerDbPath).href}?mode=ro`.replaceAll("'", "''");
|
|
28375
|
-
try {
|
|
28376
|
-
this.db.exec(`attach database '${crawlerUri}' as crawler`);
|
|
28377
|
-
} catch {
|
|
28378
|
-
return false;
|
|
28379
|
-
}
|
|
28380
|
-
const { descriptor } = this.options;
|
|
28381
|
-
try {
|
|
28382
|
-
this.db.prepare(`select ${this.select} from ${descriptor.fromClause} where ${this.where} limit 0`).all(...this.whereParams());
|
|
28383
|
-
this.db.prepare(`select 1 from ${descriptor.fts.table} limit 0`).all();
|
|
28384
|
-
} catch {
|
|
28385
|
-
this.db.exec("detach database crawler");
|
|
28386
|
-
return false;
|
|
28387
|
-
}
|
|
28388
|
-
this.attached = file;
|
|
28389
|
-
this.lastSemantic = undefined;
|
|
28390
|
-
return true;
|
|
28391
|
-
}
|
|
28392
|
-
detachCrawler() {
|
|
28393
|
-
if (this.attached === undefined)
|
|
28394
|
-
return;
|
|
28395
|
-
this.attached = undefined;
|
|
28396
|
-
this.lastSemantic = undefined;
|
|
28397
|
-
try {
|
|
28398
|
-
this.opened?.exec("detach database crawler");
|
|
28399
|
-
} catch {}
|
|
28400
|
-
}
|
|
28401
|
-
get semanticEnabled() {
|
|
28402
|
-
return this.options.embedder !== undefined;
|
|
28403
|
-
}
|
|
28404
|
-
close() {
|
|
28405
|
-
this.opened?.close();
|
|
28406
|
-
this.opened = undefined;
|
|
28407
|
-
this.attached = undefined;
|
|
28408
|
-
this.lastSemantic = undefined;
|
|
28409
|
-
}
|
|
28410
|
-
whereParams() {
|
|
28411
|
-
const { descriptor, workspaceId, channelIds = [], channelNames = [] } = this.options;
|
|
28412
|
-
return [
|
|
28413
|
-
...descriptor.scopeExpression !== undefined && workspaceId !== undefined ? [workspaceId] : [],
|
|
28414
|
-
...channelIds,
|
|
28415
|
-
...channelNames
|
|
28416
|
-
];
|
|
28417
|
-
}
|
|
28418
|
-
searchLexical(query, topK) {
|
|
28419
|
-
const match = toMatchQuery(query);
|
|
28420
|
-
if (match === undefined || topK <= 0 || !this.attachCrawler())
|
|
28421
|
-
return [];
|
|
28422
|
-
const { descriptor } = this.options;
|
|
28423
|
-
const rows = this.db.prepare(`select ${this.select}, bm25(${descriptor.fts.table}) as rank
|
|
28424
|
-
from ${descriptor.fromClause} join ${descriptor.fts.table} on ${descriptor.fts.joinOn}
|
|
28425
|
-
where ${descriptor.fts.table} match ? and ${this.where}
|
|
28426
|
-
order by rank limit ?`).all(match, ...this.whereParams(), topK);
|
|
28427
|
-
return rows.map((row) => this.toHit(this.toRow(row), -Number(row.rank)));
|
|
28428
|
-
}
|
|
28429
|
-
async searchSemantic(query, topK) {
|
|
28430
|
-
const trimmed = query.trim();
|
|
28431
|
-
if (this.options.embedder === undefined || trimmed.length === 0 || topK <= 0 || !this.attachCrawler())
|
|
28432
|
-
return [];
|
|
28433
|
-
const ranked = await this.rankSemantic(trimmed, topK * POOL_FACTOR);
|
|
28434
|
-
if (ranked.length === 0)
|
|
28435
|
-
return [];
|
|
28436
|
-
const live = this.hydrate(ranked.map((entry) => entry.key));
|
|
28437
|
-
const hits = [];
|
|
28438
|
-
for (const entry of ranked) {
|
|
28439
|
-
const row = live.get(entry.key);
|
|
28440
|
-
if (row !== undefined)
|
|
28441
|
-
hits.push(this.toHit(row, entry.score));
|
|
28442
|
-
if (hits.length >= topK)
|
|
28443
|
-
break;
|
|
28444
|
-
}
|
|
28445
|
-
return hits;
|
|
28446
|
-
}
|
|
28447
|
-
async rankSemantic(query, pool) {
|
|
28448
|
-
const now = Date.now();
|
|
28449
|
-
let cached = this.lastSemantic;
|
|
28450
|
-
if (cached === undefined || cached.query !== query || now - cached.at > SEMANTIC_REUSE_MS) {
|
|
28451
|
-
const embedder = this.options.embedder;
|
|
28452
|
-
if (embedder === undefined)
|
|
28453
|
-
return [];
|
|
28454
|
-
cached = {
|
|
28455
|
-
query,
|
|
28456
|
-
at: now,
|
|
28457
|
-
vector: embedder.embed([query]).then((vectors) => vectors[0], () => {
|
|
28458
|
-
return;
|
|
28459
|
-
}),
|
|
28460
|
-
ranked: undefined
|
|
28461
|
-
};
|
|
28462
|
-
this.lastSemantic = cached;
|
|
28463
|
-
}
|
|
28464
|
-
const vector = await cached.vector;
|
|
28465
|
-
if (vector === undefined) {
|
|
28466
|
-
if (this.lastSemantic === cached)
|
|
28467
|
-
this.lastSemantic = undefined;
|
|
28468
|
-
return [];
|
|
28469
|
-
}
|
|
28470
|
-
if (cached.ranked === undefined || cached.ranked.pool < pool) {
|
|
28471
|
-
const wide = pool * POOL_FACTOR;
|
|
28472
|
-
cached.ranked = { pool: wide, keys: topKByCosine(vector, this.storedVectors(), wide) };
|
|
28473
|
-
}
|
|
28474
|
-
return cached.ranked.keys.slice(0, pool);
|
|
28445
|
+
if (options.instanceId !== undefined)
|
|
28446
|
+
validateMailcrawlInstanceId(options.instanceId);
|
|
28475
28447
|
}
|
|
28476
|
-
async
|
|
28477
|
-
|
|
28478
|
-
|
|
28479
|
-
|
|
28480
|
-
|
|
28481
|
-
|
|
28482
|
-
|
|
28483
|
-
|
|
28484
|
-
|
|
28485
|
-
|
|
28486
|
-
|
|
28487
|
-
const
|
|
28488
|
-
|
|
28489
|
-
|
|
28490
|
-
|
|
28491
|
-
|
|
28492
|
-
|
|
28493
|
-
break;
|
|
28448
|
+
async sync(signal) {
|
|
28449
|
+
const args = [
|
|
28450
|
+
"sync",
|
|
28451
|
+
"--json",
|
|
28452
|
+
...this.options.account === undefined ? [] : ["--account", this.options.account],
|
|
28453
|
+
...this.options.mailbox === undefined ? [] : ["--mailbox", this.options.mailbox],
|
|
28454
|
+
...this.options.backend === undefined ? [] : ["--backend", this.options.backend],
|
|
28455
|
+
...this.options.source === undefined ? [] : ["--source", this.options.source],
|
|
28456
|
+
...this.options.fixture === undefined ? [] : ["--fixture", this.options.fixture],
|
|
28457
|
+
...this.options.himalayaConfig === undefined ? [] : ["--himalaya-config", this.options.himalayaConfig]
|
|
28458
|
+
];
|
|
28459
|
+
const result = await this.run(args, signal, this.indexTimeoutMs());
|
|
28460
|
+
if (!result.ok)
|
|
28461
|
+
return failure3(result);
|
|
28462
|
+
const parsed = parseObject2(result.stdout);
|
|
28463
|
+
if (parsed === undefined || !hasAnyNumber(parsed, ["added", "updated", "deleted", "unchanged", "chunksAdded"])) {
|
|
28464
|
+
return failure3({ ...result, ok: false, reason: "invalid-output" });
|
|
28494
28465
|
}
|
|
28495
|
-
|
|
28466
|
+
const messages = number(parsed, "added") ?? number(parsed, "updated") ?? number(parsed, "unchanged") ?? 0;
|
|
28467
|
+
return ok4({
|
|
28468
|
+
messages,
|
|
28469
|
+
added: number(parsed, "added"),
|
|
28470
|
+
updated: number(parsed, "updated"),
|
|
28471
|
+
deleted: number(parsed, "deleted"),
|
|
28472
|
+
unchanged: number(parsed, "unchanged"),
|
|
28473
|
+
chunksAdded: number(parsed, "chunksAdded"),
|
|
28474
|
+
archiveRevision: string(parsed, "archiveRevision")
|
|
28475
|
+
}, result);
|
|
28496
28476
|
}
|
|
28497
|
-
async
|
|
28498
|
-
|
|
28499
|
-
|
|
28500
|
-
|
|
28501
|
-
|
|
28502
|
-
|
|
28503
|
-
|
|
28504
|
-
|
|
28505
|
-
|
|
28506
|
-
|
|
28507
|
-
|
|
28477
|
+
async search(mode, query, options) {
|
|
28478
|
+
if (query.trim().length === 0)
|
|
28479
|
+
return { ok: true, hits: [], stdout: "", stderr: "", code: 0 };
|
|
28480
|
+
const args = [
|
|
28481
|
+
"search",
|
|
28482
|
+
"--mode",
|
|
28483
|
+
mode === "keyword" ? "bm25" : mode,
|
|
28484
|
+
...this.options.account === undefined ? [] : ["--account", this.options.account],
|
|
28485
|
+
...this.options.mailbox === undefined ? [] : ["--mailbox", this.options.mailbox],
|
|
28486
|
+
"--limit",
|
|
28487
|
+
String(options?.topK ?? 20),
|
|
28488
|
+
"--json",
|
|
28489
|
+
query.trim()
|
|
28490
|
+
];
|
|
28491
|
+
const result = await this.run(args, options?.signal, this.options.timeoutMs ?? DEFAULT_SEARCH_TIMEOUT_MS2);
|
|
28492
|
+
if (!result.ok)
|
|
28493
|
+
return failure3(result);
|
|
28494
|
+
const parsed = parseJson3(result.stdout);
|
|
28495
|
+
if (parsed === undefined)
|
|
28496
|
+
return failure3({ ...result, ok: false, reason: "invalid-output" });
|
|
28497
|
+
const rows = Array.isArray(parsed) ? parsed : objectArray(parsed, ["hits", "results", "items"]);
|
|
28498
|
+
if (rows === undefined)
|
|
28499
|
+
return failure3({ ...result, ok: false, reason: "invalid-output" });
|
|
28500
|
+
const hits = rows.map((row, index) => normalizeHit2(row, mode, index));
|
|
28501
|
+
if (hits.some((hit) => hit === undefined)) {
|
|
28502
|
+
return failure3({ ...result, ok: false, reason: "invalid-output" });
|
|
28508
28503
|
}
|
|
28509
|
-
|
|
28510
|
-
|
|
28511
|
-
|
|
28512
|
-
|
|
28513
|
-
|
|
28514
|
-
|
|
28515
|
-
const embed = async (rows) => {
|
|
28516
|
-
const vectors = await embedder.embed(rows.map((row) => row.input));
|
|
28517
|
-
if (vectors.length !== rows.length) {
|
|
28518
|
-
throw new Error(`embedder returned ${vectors.length} vectors for ${rows.length} messages`);
|
|
28519
|
-
}
|
|
28520
|
-
const wrong = vectors.find((vector) => vector.length !== identity.dimension);
|
|
28521
|
-
if (wrong !== undefined) {
|
|
28522
|
-
throw new Error(`embedder returned a ${wrong.length}-dimension vector, expected ${identity.dimension}`);
|
|
28523
|
-
}
|
|
28524
|
-
return vectors;
|
|
28504
|
+
return {
|
|
28505
|
+
ok: true,
|
|
28506
|
+
hits,
|
|
28507
|
+
stdout: result.stdout,
|
|
28508
|
+
stderr: result.stderr,
|
|
28509
|
+
code: result.code ?? 0
|
|
28525
28510
|
};
|
|
28526
|
-
let embedded = 0;
|
|
28527
|
-
let skipped = 0;
|
|
28528
|
-
for (let offset = 0;offset < target; offset += batchSize) {
|
|
28529
|
-
const rows = [...this.hydrate(pending.slice(offset, Math.min(offset + batchSize, target))).values()];
|
|
28530
|
-
if (rows.length === 0)
|
|
28531
|
-
continue;
|
|
28532
|
-
try {
|
|
28533
|
-
this.store(rows, await embed(rows));
|
|
28534
|
-
embedded += rows.length;
|
|
28535
|
-
} catch (batchError) {
|
|
28536
|
-
const rejected = [];
|
|
28537
|
-
let lastError = batchError;
|
|
28538
|
-
for (const row of rows) {
|
|
28539
|
-
try {
|
|
28540
|
-
this.store([row], await embed([row]));
|
|
28541
|
-
embedded += 1;
|
|
28542
|
-
} catch (error) {
|
|
28543
|
-
rejected.push(row);
|
|
28544
|
-
lastError = error;
|
|
28545
|
-
}
|
|
28546
|
-
}
|
|
28547
|
-
if (rejected.length === 0)
|
|
28548
|
-
continue;
|
|
28549
|
-
if (embedded === 0)
|
|
28550
|
-
return { ok: false, message: errorMessage2(lastError) };
|
|
28551
|
-
this.store(rejected, undefined);
|
|
28552
|
-
skipped += rejected.length;
|
|
28553
|
-
}
|
|
28554
|
-
}
|
|
28555
|
-
return { ok: true, embedded, skipped, pruned, remaining: pending.length - embedded - skipped };
|
|
28556
|
-
}
|
|
28557
|
-
adoptIdentity(identity) {
|
|
28558
|
-
const serialized = JSON.stringify(identity);
|
|
28559
|
-
const current = this.db.prepare("select v from sidecar_meta where k = ?").get(IDENTITY_KEY);
|
|
28560
|
-
if (current?.v === serialized)
|
|
28561
|
-
return;
|
|
28562
|
-
this.transaction(() => {
|
|
28563
|
-
this.db.exec("delete from sidecar_vec");
|
|
28564
|
-
this.db.prepare("insert or replace into sidecar_meta (k, v) values (?, ?)").run(IDENTITY_KEY, serialized);
|
|
28565
|
-
});
|
|
28566
|
-
this.lastSemantic = undefined;
|
|
28567
28511
|
}
|
|
28568
|
-
|
|
28569
|
-
|
|
28570
|
-
const result = this.db.prepare(`delete from sidecar_vec where message_key not in (
|
|
28571
|
-
select ${descriptor.keyExpression} from ${descriptor.fromClause} where ${this.where}
|
|
28572
|
-
)`).run(...this.whereParams());
|
|
28573
|
-
if (Number(result.changes) > 0)
|
|
28574
|
-
this.lastSemantic = undefined;
|
|
28575
|
-
return Number(result.changes);
|
|
28512
|
+
indexTimeoutMs() {
|
|
28513
|
+
return this.options.indexTimeoutMs ?? this.options.timeoutMs ?? DEFAULT_INDEX_TIMEOUT_MS3;
|
|
28576
28514
|
}
|
|
28577
|
-
|
|
28578
|
-
const
|
|
28579
|
-
|
|
28580
|
-
|
|
28581
|
-
left join sidecar_vec v on v.message_key = ${descriptor.keyExpression}
|
|
28582
|
-
where ${this.where}`).iterate(...this.whereParams());
|
|
28583
|
-
const pending = [];
|
|
28584
|
-
for (const row of rows) {
|
|
28585
|
-
if (row.stored_hash !== this.toRow(row).hash)
|
|
28586
|
-
pending.push(String(row.k));
|
|
28515
|
+
async run(args, signal, timeoutMs) {
|
|
28516
|
+
const violation = findRemoteEmbeddingViolation({ ...process.env, ...this.options.env ?? {} });
|
|
28517
|
+
if (violation !== undefined) {
|
|
28518
|
+
return { ok: false, reason: "remote-embedding-rejected", stdout: "", stderr: "", code: null };
|
|
28587
28519
|
}
|
|
28588
|
-
|
|
28520
|
+
const dataDir = this.options.dataDir;
|
|
28521
|
+
if (dataDir !== undefined)
|
|
28522
|
+
ensurePrivateMailcrawlDataDir(dataDir);
|
|
28523
|
+
const env = {};
|
|
28524
|
+
for (const [key, value] of Object.entries(process.env)) {
|
|
28525
|
+
if (value !== undefined && (SAFE_ENV_KEYS2.has(key) || SAFE_MAILCRAWL_ENV_KEYS.has(key)))
|
|
28526
|
+
env[key] = value;
|
|
28527
|
+
}
|
|
28528
|
+
for (const [key, value] of Object.entries(this.options.env ?? {})) {
|
|
28529
|
+
if (value === undefined)
|
|
28530
|
+
delete env[key];
|
|
28531
|
+
else if (SAFE_ENV_KEYS2.has(key) || SAFE_MAILCRAWL_ENV_KEYS.has(key))
|
|
28532
|
+
env[key] = value;
|
|
28533
|
+
}
|
|
28534
|
+
if (dataDir !== undefined)
|
|
28535
|
+
env.MAILCRAWL_DATA_DIR = dataDir;
|
|
28536
|
+
return spawnMailcrawl(this.options.binaryPath ?? DEFAULT_BINARY4, args, env, this.options, signal, timeoutMs);
|
|
28589
28537
|
}
|
|
28590
|
-
|
|
28591
|
-
const
|
|
28592
|
-
|
|
28593
|
-
|
|
28594
|
-
|
|
28595
|
-
|
|
28596
|
-
|
|
28597
|
-
const byKey = new Map(rows.map((row) => [String(row.k), this.toRow(row)]));
|
|
28598
|
-
for (const key of keys) {
|
|
28599
|
-
const row = byKey.get(key);
|
|
28600
|
-
if (row !== undefined)
|
|
28601
|
-
out.set(key, row);
|
|
28538
|
+
async index(signal) {
|
|
28539
|
+
const result = await this.run(["index", "--json"], signal, this.indexTimeoutMs());
|
|
28540
|
+
if (!result.ok)
|
|
28541
|
+
return failure3(result);
|
|
28542
|
+
const parsed = parseObject2(result.stdout);
|
|
28543
|
+
if (parsed === undefined || number(parsed, "embedded") === undefined && number(parsed, "reused") === undefined && string(parsed, "generation") === undefined) {
|
|
28544
|
+
return failure3({ ...result, ok: false, reason: "invalid-output" });
|
|
28602
28545
|
}
|
|
28603
|
-
return
|
|
28546
|
+
return ok4({
|
|
28547
|
+
embedded: number(parsed, "embedded"),
|
|
28548
|
+
reused: number(parsed, "reused"),
|
|
28549
|
+
generation: string(parsed, "generation"),
|
|
28550
|
+
archiveRevision: string(parsed, "archiveRevision"),
|
|
28551
|
+
rebuilt: typeof parsed.rebuilt === "boolean" ? parsed.rebuilt : undefined,
|
|
28552
|
+
embedder: string(parsed, "embedder")
|
|
28553
|
+
}, result);
|
|
28604
28554
|
}
|
|
28605
|
-
|
|
28606
|
-
|
|
28607
|
-
|
|
28608
|
-
|
|
28609
|
-
|
|
28555
|
+
}
|
|
28556
|
+
function spawnMailcrawl(binary, args, env, options, signal, timeoutMs) {
|
|
28557
|
+
return new Promise((resolveResult) => {
|
|
28558
|
+
const portable = portableSpawnCommand(binary, args);
|
|
28559
|
+
const child = spawn15(portable.command, portable.args, {
|
|
28560
|
+
env,
|
|
28561
|
+
detached: process.platform !== "win32",
|
|
28562
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
28563
|
+
});
|
|
28564
|
+
let stdout = { text: "", bytes: 0, exceeded: false }, stderr = { text: "", bytes: 0, exceeded: false }, settled = false, reason;
|
|
28565
|
+
const max = options.maxBufferBytes ?? DEFAULT_MAX_BUFFER_BYTES4;
|
|
28566
|
+
const timer = setTimeout(() => {
|
|
28567
|
+
reason = "timeout";
|
|
28568
|
+
terminate4(child);
|
|
28569
|
+
}, timeoutMs);
|
|
28570
|
+
const abort = () => {
|
|
28571
|
+
reason = "aborted";
|
|
28572
|
+
terminate4(child);
|
|
28573
|
+
};
|
|
28574
|
+
if (signal?.aborted)
|
|
28575
|
+
abort();
|
|
28576
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
28577
|
+
child.stdout.setEncoding("utf8");
|
|
28578
|
+
child.stderr.setEncoding("utf8");
|
|
28579
|
+
child.stdout.on("data", (chunk) => {
|
|
28580
|
+
stdout = appendOutput(stdout, chunk, max);
|
|
28581
|
+
if (stdout.exceeded && reason === undefined) {
|
|
28582
|
+
reason = "stdout-too-large";
|
|
28583
|
+
terminate4(child);
|
|
28584
|
+
}
|
|
28585
|
+
});
|
|
28586
|
+
child.stderr.on("data", (chunk) => {
|
|
28587
|
+
stderr = appendOutput(stderr, chunk, max);
|
|
28588
|
+
if (stderr.exceeded && reason === undefined) {
|
|
28589
|
+
reason = "stderr-too-large";
|
|
28590
|
+
terminate4(child);
|
|
28610
28591
|
}
|
|
28611
28592
|
});
|
|
28612
|
-
|
|
28593
|
+
child.on("error", (error) => {
|
|
28594
|
+
if (settled)
|
|
28595
|
+
return;
|
|
28596
|
+
settled = true;
|
|
28597
|
+
clearTimeout(timer);
|
|
28598
|
+
signal?.removeEventListener("abort", abort);
|
|
28599
|
+
resolveResult({
|
|
28600
|
+
ok: false,
|
|
28601
|
+
reason: error.code === "ENOENT" ? "binary-missing" : "spawn-error",
|
|
28602
|
+
stdout: stdout.text,
|
|
28603
|
+
stderr: "",
|
|
28604
|
+
code: null
|
|
28605
|
+
});
|
|
28606
|
+
});
|
|
28607
|
+
child.on("close", (code) => {
|
|
28608
|
+
if (settled)
|
|
28609
|
+
return;
|
|
28610
|
+
settled = true;
|
|
28611
|
+
clearTimeout(timer);
|
|
28612
|
+
signal?.removeEventListener("abort", abort);
|
|
28613
|
+
resolveResult({
|
|
28614
|
+
ok: code === 0 && reason === undefined,
|
|
28615
|
+
...reason === undefined ? code === 0 ? {} : { reason: "nonzero-exit" } : { reason },
|
|
28616
|
+
stdout: stdout.text,
|
|
28617
|
+
stderr: stderr.text,
|
|
28618
|
+
code
|
|
28619
|
+
});
|
|
28620
|
+
});
|
|
28621
|
+
});
|
|
28622
|
+
}
|
|
28623
|
+
function terminate4(child) {
|
|
28624
|
+
if (child.killed)
|
|
28625
|
+
return;
|
|
28626
|
+
if (process.platform !== "win32" && child.pid !== undefined) {
|
|
28627
|
+
try {
|
|
28628
|
+
process.kill(-child.pid, "SIGTERM");
|
|
28629
|
+
return;
|
|
28630
|
+
} catch {}
|
|
28613
28631
|
}
|
|
28614
|
-
|
|
28615
|
-
|
|
28616
|
-
|
|
28617
|
-
|
|
28618
|
-
|
|
28619
|
-
|
|
28632
|
+
child.kill("SIGTERM");
|
|
28633
|
+
}
|
|
28634
|
+
function parseJson3(text) {
|
|
28635
|
+
try {
|
|
28636
|
+
return JSON.parse(text.trim());
|
|
28637
|
+
} catch {
|
|
28638
|
+
return;
|
|
28639
|
+
}
|
|
28640
|
+
}
|
|
28641
|
+
function parseObject2(text) {
|
|
28642
|
+
const value = parseJson3(text);
|
|
28643
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : undefined;
|
|
28644
|
+
}
|
|
28645
|
+
function objectArray(value, keys) {
|
|
28646
|
+
if (value === null || typeof value !== "object" || Array.isArray(value))
|
|
28647
|
+
return;
|
|
28648
|
+
for (const key of keys)
|
|
28649
|
+
if (Array.isArray(value[key]))
|
|
28650
|
+
return value[key];
|
|
28651
|
+
return;
|
|
28652
|
+
}
|
|
28653
|
+
function hasAnyNumber(value, keys) {
|
|
28654
|
+
return keys.some((key) => number(value, key) !== undefined);
|
|
28655
|
+
}
|
|
28656
|
+
function normalizeHit2(value, mode, index) {
|
|
28657
|
+
if (value === null || typeof value !== "object" || Array.isArray(value))
|
|
28658
|
+
return;
|
|
28659
|
+
const row = value;
|
|
28660
|
+
const text = (key) => typeof row[key] === "string" ? row[key] : "";
|
|
28661
|
+
const chunkId = text("chunkId") || text("chunk_id");
|
|
28662
|
+
const messageId = text("messageId") || text("message_id");
|
|
28663
|
+
const accountId = text("accountId") || text("account_id");
|
|
28664
|
+
const mailbox = text("mailbox");
|
|
28665
|
+
const snippet = text("snippet") || text("content") || text("text");
|
|
28666
|
+
if (!isSafeOpaqueId(chunkId) || !isSafeOpaqueId(messageId) || !snippet)
|
|
28667
|
+
return;
|
|
28668
|
+
const to = Array.isArray(row.to) ? row.to.filter((item) => typeof item === "string") : [];
|
|
28669
|
+
return {
|
|
28670
|
+
chunkId,
|
|
28671
|
+
messageId,
|
|
28672
|
+
threadId: text("threadId") || text("thread_id"),
|
|
28673
|
+
accountId,
|
|
28674
|
+
mailbox,
|
|
28675
|
+
subject: text("subject"),
|
|
28676
|
+
from: text("from"),
|
|
28677
|
+
to,
|
|
28678
|
+
date: text("date"),
|
|
28679
|
+
snippet,
|
|
28680
|
+
score: typeof row.score === "number" ? row.score : 1 / (index + 1),
|
|
28681
|
+
mode
|
|
28682
|
+
};
|
|
28683
|
+
}
|
|
28684
|
+
function number(value, key) {
|
|
28685
|
+
return typeof value[key] === "number" && Number.isFinite(value[key]) ? value[key] : undefined;
|
|
28686
|
+
}
|
|
28687
|
+
function string(value, key) {
|
|
28688
|
+
return typeof value[key] === "string" ? value[key] : undefined;
|
|
28689
|
+
}
|
|
28690
|
+
function ok4(data, result) {
|
|
28691
|
+
return { ok: true, data, stdout: result.stdout, stderr: result.stderr, code: result.code ?? 0 };
|
|
28692
|
+
}
|
|
28693
|
+
function failure3(result) {
|
|
28694
|
+
return {
|
|
28695
|
+
ok: false,
|
|
28696
|
+
reason: result.reason ?? "nonzero-exit",
|
|
28697
|
+
stdout: result.stdout,
|
|
28698
|
+
stderr: boundDiagnosticText6(result.stderr),
|
|
28699
|
+
code: result.code
|
|
28700
|
+
};
|
|
28701
|
+
}
|
|
28702
|
+
function boundDiagnosticText6(value) {
|
|
28703
|
+
if (value.length === 0)
|
|
28704
|
+
return "";
|
|
28705
|
+
return value.trim().slice(0, 4000);
|
|
28706
|
+
}
|
|
28707
|
+
function appendOutput(current, chunk, maxBytes) {
|
|
28708
|
+
if (current.exceeded)
|
|
28709
|
+
return current;
|
|
28710
|
+
const bytes = Buffer.from(chunk, "utf8");
|
|
28711
|
+
const remaining = Math.max(0, maxBytes - current.bytes);
|
|
28712
|
+
const prefix = truncateUtf8(chunk, remaining);
|
|
28713
|
+
const total = current.bytes + bytes.byteLength;
|
|
28714
|
+
return {
|
|
28715
|
+
text: current.text + prefix,
|
|
28716
|
+
bytes: Math.min(total, maxBytes),
|
|
28717
|
+
exceeded: total > maxBytes
|
|
28718
|
+
};
|
|
28719
|
+
}
|
|
28720
|
+
function isSafeOpaqueId(value) {
|
|
28721
|
+
return value.length > 0 && value !== "." && value !== ".." && !/[\\/#\u0000-\u001f\u007f]/u.test(value);
|
|
28722
|
+
}
|
|
28723
|
+
function findRemoteEmbeddingViolation(env) {
|
|
28724
|
+
for (const [key, value] of Object.entries(env)) {
|
|
28725
|
+
if (value === undefined)
|
|
28726
|
+
continue;
|
|
28727
|
+
const lower = key.toLowerCase();
|
|
28728
|
+
if (REMOTE_EMBEDDING_ENV_KEYS.has(lower))
|
|
28729
|
+
return key;
|
|
28730
|
+
if (lower === "mailcrawl_embedder" || lower === "mailcrawl_embed_url") {
|
|
28731
|
+
if (isRemoteHttpEndpoint(value))
|
|
28732
|
+
return key;
|
|
28620
28733
|
}
|
|
28621
28734
|
}
|
|
28622
|
-
|
|
28623
|
-
|
|
28735
|
+
return;
|
|
28736
|
+
}
|
|
28737
|
+
function isRemoteHttpEndpoint(value) {
|
|
28738
|
+
const trimmed = value.trim();
|
|
28739
|
+
if (!/^https?:\/\//iu.test(trimmed))
|
|
28740
|
+
return false;
|
|
28741
|
+
try {
|
|
28742
|
+
return !LOOPBACK_HOSTS2.has(new URL(trimmed).hostname);
|
|
28743
|
+
} catch {
|
|
28744
|
+
return true;
|
|
28745
|
+
}
|
|
28746
|
+
}
|
|
28747
|
+
function truncateUtf8(value, maxBytes) {
|
|
28748
|
+
if (Buffer.byteLength(value) <= maxBytes)
|
|
28749
|
+
return value;
|
|
28750
|
+
const decoder = new TextDecoder("utf-8", { fatal: true });
|
|
28751
|
+
const bytes = Buffer.from(value, "utf8");
|
|
28752
|
+
for (let end = Math.min(maxBytes, bytes.length);end >= 0; end -= 1) {
|
|
28624
28753
|
try {
|
|
28625
|
-
|
|
28626
|
-
|
|
28627
|
-
} catch (error) {
|
|
28628
|
-
this.db.exec("rollback");
|
|
28629
|
-
throw error;
|
|
28630
|
-
}
|
|
28754
|
+
return decoder.decode(bytes.subarray(0, end));
|
|
28755
|
+
} catch {}
|
|
28631
28756
|
}
|
|
28632
|
-
|
|
28633
|
-
|
|
28634
|
-
|
|
28635
|
-
|
|
28636
|
-
|
|
28637
|
-
|
|
28638
|
-
|
|
28639
|
-
|
|
28640
|
-
|
|
28641
|
-
|
|
28642
|
-
|
|
28643
|
-
|
|
28644
|
-
|
|
28645
|
-
|
|
28646
|
-
|
|
28647
|
-
|
|
28648
|
-
|
|
28649
|
-
|
|
28650
|
-
|
|
28651
|
-
|
|
28652
|
-
|
|
28757
|
+
return "";
|
|
28758
|
+
}
|
|
28759
|
+
var DEFAULT_BINARY4 = "mailcrawl", DEFAULT_SEARCH_TIMEOUT_MS2 = 60000, DEFAULT_INDEX_TIMEOUT_MS3 = 1800000, DEFAULT_MAX_BUFFER_BYTES4 = 1048576, LOOPBACK_HOSTS2, SAFE_ENV_KEYS2, SAFE_MAILCRAWL_ENV_KEYS, REMOTE_EMBEDDING_ENV_KEYS;
|
|
28760
|
+
var init_client9 = __esm(() => {
|
|
28761
|
+
init_portable_spawn();
|
|
28762
|
+
init_config3();
|
|
28763
|
+
LOOPBACK_HOSTS2 = new Set(["127.0.0.1", "localhost", "[::1]", "::1"]);
|
|
28764
|
+
SAFE_ENV_KEYS2 = new Set(["HOME", "LANG", "LC_ALL", "PATH", "TMPDIR", "TMP", "TEMP", "NO_COLOR"]);
|
|
28765
|
+
SAFE_MAILCRAWL_ENV_KEYS = new Set([
|
|
28766
|
+
"MAILCRAWL_DATA_DIR",
|
|
28767
|
+
"MAILCRAWL_EMBEDDER",
|
|
28768
|
+
"MAILCRAWL_EMBEDDER_PROVIDER",
|
|
28769
|
+
"MAILCRAWL_EMBED_URL",
|
|
28770
|
+
"MAILCRAWL_EMBED_MODEL",
|
|
28771
|
+
"MAILCRAWL_EMBED_DIM",
|
|
28772
|
+
"MAILCRAWL_EMBED_TIMEOUT",
|
|
28773
|
+
"MAILCRAWL_EMBED_BATCH_SIZE",
|
|
28774
|
+
"MAILCRAWL_QUERY_PREFIX",
|
|
28775
|
+
"MAILCRAWL_PASSAGE_PREFIX",
|
|
28776
|
+
"MAILCRAWL_NATIVE_MODEL",
|
|
28777
|
+
"MAILCRAWL_NATIVE_DEVICE",
|
|
28778
|
+
"MAILCRAWL_NATIVE_DTYPE",
|
|
28779
|
+
"MAILCRAWL_KIWI_MODEL",
|
|
28780
|
+
"MAILCRAWL_JA_HELPER",
|
|
28781
|
+
"MAILCRAWL_ZH_HELPER"
|
|
28782
|
+
]);
|
|
28783
|
+
REMOTE_EMBEDDING_ENV_KEYS = new Set([
|
|
28784
|
+
"mailcrawl_embedder_base_url",
|
|
28785
|
+
"mailcrawl_embedding_base_url",
|
|
28786
|
+
"mailcrawl_allow_remote_embeddings"
|
|
28787
|
+
]);
|
|
28788
|
+
});
|
|
28789
|
+
|
|
28790
|
+
// src/datasource/skills/mailcrawl/methods.ts
|
|
28791
|
+
class MailcrawlMethod {
|
|
28792
|
+
client;
|
|
28793
|
+
mode;
|
|
28794
|
+
instanceId;
|
|
28795
|
+
tags;
|
|
28796
|
+
account;
|
|
28797
|
+
mailbox;
|
|
28798
|
+
constructor(client, mode, instanceId, tags, account, mailbox) {
|
|
28799
|
+
this.client = client;
|
|
28800
|
+
this.mode = mode;
|
|
28801
|
+
this.instanceId = instanceId;
|
|
28802
|
+
this.tags = tags;
|
|
28803
|
+
this.account = account;
|
|
28804
|
+
this.mailbox = mailbox;
|
|
28653
28805
|
}
|
|
28654
|
-
|
|
28806
|
+
describe() {
|
|
28655
28807
|
return {
|
|
28656
|
-
|
|
28657
|
-
|
|
28658
|
-
|
|
28659
|
-
|
|
28660
|
-
|
|
28661
|
-
|
|
28808
|
+
name: `mailcrawl-${this.mode}`,
|
|
28809
|
+
type: this.mode === "bm25" ? "bm25" : this.mode === "semantic" ? "vector" : "hybrid",
|
|
28810
|
+
description: `${this.mode} retrieval over archived email via the external mailcrawl CLI`,
|
|
28811
|
+
status: "active",
|
|
28812
|
+
capabilities: ["email", this.mode, "scoped", "external-cli", "path-opaque-sources"],
|
|
28813
|
+
datasourceId: "mailcrawl",
|
|
28814
|
+
tags: this.tags
|
|
28662
28815
|
};
|
|
28663
28816
|
}
|
|
28817
|
+
async retrieve(query, options) {
|
|
28818
|
+
if (!query.trim())
|
|
28819
|
+
return [];
|
|
28820
|
+
const result = await this.client.search(this.mode, query, {
|
|
28821
|
+
topK: options.topK,
|
|
28822
|
+
signal: options.signal
|
|
28823
|
+
});
|
|
28824
|
+
if (!result.ok)
|
|
28825
|
+
throw datasourceCliError("mailcrawl", `search --mode ${this.mode}`, result);
|
|
28826
|
+
return result.hits.filter((hit) => {
|
|
28827
|
+
if (this.account !== undefined && hit.accountId !== this.account)
|
|
28828
|
+
return false;
|
|
28829
|
+
if (this.mailbox !== undefined && hit.mailbox !== this.mailbox)
|
|
28830
|
+
return false;
|
|
28831
|
+
const source = datasourceSourcePath("mailcrawl", this.instanceId, hit.chunkId);
|
|
28832
|
+
return matchesDatasourceScope(source, options.scope);
|
|
28833
|
+
}).map((hit) => {
|
|
28834
|
+
const source = datasourceSourcePath("mailcrawl", this.instanceId, hit.chunkId);
|
|
28835
|
+
return {
|
|
28836
|
+
id: `mailcrawl:${this.instanceId}:${hit.chunkId}`,
|
|
28837
|
+
content: hit.snippet,
|
|
28838
|
+
source,
|
|
28839
|
+
score: hit.score,
|
|
28840
|
+
metadata: {
|
|
28841
|
+
method: `mailcrawl-${this.mode}`,
|
|
28842
|
+
datasourceId: "mailcrawl",
|
|
28843
|
+
instanceId: this.instanceId,
|
|
28844
|
+
mode: this.mode,
|
|
28845
|
+
chunkId: hit.chunkId,
|
|
28846
|
+
messageId: hit.messageId,
|
|
28847
|
+
threadId: hit.threadId,
|
|
28848
|
+
accountId: hit.accountId,
|
|
28849
|
+
mailbox: hit.mailbox,
|
|
28850
|
+
subject: hit.subject,
|
|
28851
|
+
from: hit.from,
|
|
28852
|
+
to: hit.to,
|
|
28853
|
+
date: hit.date
|
|
28854
|
+
}
|
|
28855
|
+
};
|
|
28856
|
+
}).slice(0, options.topK ?? 20);
|
|
28857
|
+
}
|
|
28664
28858
|
}
|
|
28665
|
-
|
|
28666
|
-
|
|
28667
|
-
|
|
28668
|
-
|
|
28669
|
-
|
|
28670
|
-
|
|
28671
|
-
}
|
|
28672
|
-
var DEFAULT_BATCH_SIZE = 256, DEFAULT_MAX_EMBED_PER_SYNC = 2048, MAX_EMBED_INPUT_CHARS = 8192, BUSY_TIMEOUT_MS = 5000, POOL_FACTOR = 3, SEMANTIC_REUSE_MS = 1e4, IDENTITY_KEY = "identity";
|
|
28673
|
-
var init_sidecar = () => {};
|
|
28674
|
-
|
|
28675
|
-
// src/datasource/sidecar/methods.ts
|
|
28676
|
-
function sidecarMethodName(datasourceId, mode) {
|
|
28677
|
-
return mode === "bm25" ? `${datasourceId}-sidecar-bm25` : `${datasourceId}-${mode}`;
|
|
28678
|
-
}
|
|
28859
|
+
var modes, MAILCRAWL_MODES;
|
|
28860
|
+
var init_methods5 = __esm(() => {
|
|
28861
|
+
init_errors4();
|
|
28862
|
+
init_scope2();
|
|
28863
|
+
modes = ["bm25", "semantic", "hybrid"];
|
|
28864
|
+
MAILCRAWL_MODES = modes;
|
|
28865
|
+
});
|
|
28679
28866
|
|
|
28680
|
-
|
|
28681
|
-
|
|
28682
|
-
|
|
28683
|
-
|
|
28867
|
+
// src/datasource/skills/mailcrawl/skill.ts
|
|
28868
|
+
class MailcrawlSkill {
|
|
28869
|
+
client;
|
|
28870
|
+
instanceId;
|
|
28871
|
+
instances;
|
|
28872
|
+
interval;
|
|
28873
|
+
tags;
|
|
28874
|
+
account;
|
|
28875
|
+
mailbox;
|
|
28876
|
+
lastIndexedAt;
|
|
28877
|
+
constructor(options = {}) {
|
|
28878
|
+
const connectorOptions = options.connectorOptions ?? options;
|
|
28879
|
+
this.instanceId = options.instanceId ?? "default";
|
|
28880
|
+
this.instances = options.instances?.length ? options.instances : [this.instanceId];
|
|
28881
|
+
validateMailcrawlInstanceId(this.instanceId);
|
|
28882
|
+
for (const instance of this.instances)
|
|
28883
|
+
validateMailcrawlInstanceId(instance);
|
|
28884
|
+
this.interval = options.pollingIntervalMs ?? 15 * 60 * 1000;
|
|
28885
|
+
this.tags = options.tags ?? TAGS;
|
|
28886
|
+
this.account = connectorOptions.account;
|
|
28887
|
+
this.mailbox = connectorOptions.mailbox;
|
|
28888
|
+
this.client = options.client ?? new MailcrawlClient({ ...connectorOptions, instanceId: this.instanceId });
|
|
28684
28889
|
}
|
|
28685
28890
|
describe() {
|
|
28686
|
-
const { datasourceId, skillType, mode, tags } = this.options;
|
|
28687
|
-
const { type, text, capabilities } = DESCRIPTIONS[mode];
|
|
28688
28891
|
return {
|
|
28689
|
-
name:
|
|
28690
|
-
|
|
28691
|
-
|
|
28892
|
+
name: "mailcrawl",
|
|
28893
|
+
id: "mailcrawl",
|
|
28894
|
+
type: "mailcrawl-archive",
|
|
28895
|
+
description: "Local email archive datasource via the external mailcrawl CLI",
|
|
28896
|
+
capabilities: ["email", "external-cli", "polling", "incremental", "bm25", "semantic", "hybrid"],
|
|
28897
|
+
tags: this.tags,
|
|
28692
28898
|
status: "active",
|
|
28693
|
-
|
|
28694
|
-
datasourceId,
|
|
28695
|
-
|
|
28899
|
+
requiresExternalCli: true,
|
|
28900
|
+
datasourceId: "mailcrawl",
|
|
28901
|
+
instanceId: this.instanceId,
|
|
28902
|
+
instances: this.instances
|
|
28696
28903
|
};
|
|
28697
28904
|
}
|
|
28698
|
-
|
|
28699
|
-
|
|
28700
|
-
|
|
28701
|
-
|
|
28702
|
-
|
|
28703
|
-
|
|
28704
|
-
|
|
28705
|
-
|
|
28706
|
-
|
|
28707
|
-
const result = this.
|
|
28708
|
-
if (result
|
|
28709
|
-
|
|
28905
|
+
polling() {
|
|
28906
|
+
return {
|
|
28907
|
+
mode: this.interval > 0 ? "poll" : "none",
|
|
28908
|
+
...this.interval > 0 ? { intervalMs: this.interval } : {},
|
|
28909
|
+
lastIndexedAt: this.lastIndexedAt
|
|
28910
|
+
};
|
|
28911
|
+
}
|
|
28912
|
+
async index(options) {
|
|
28913
|
+
try {
|
|
28914
|
+
const result = await this.client.sync(options?.signal);
|
|
28915
|
+
if (!result.ok) {
|
|
28916
|
+
return this.fail(result.reason === "binary-missing" || result.reason === "spawn-error" || result.reason === "timeout" ? "datasource-unavailable" : result.reason === "remote-embedding-rejected" ? "datasource-embedding-egress-rejected" : "datasource-index-failed", result.reason);
|
|
28917
|
+
}
|
|
28918
|
+
const indexed = await this.client.index(options?.signal);
|
|
28919
|
+
const diagnostics = [];
|
|
28920
|
+
if (!indexed.ok) {
|
|
28921
|
+
diagnostics.push({
|
|
28922
|
+
code: indexed.reason === "remote-embedding-rejected" ? "datasource-embedding-egress-rejected" : "datasource-index-failed",
|
|
28923
|
+
severity: "warning",
|
|
28924
|
+
message: `mailcrawl semantic index unavailable (${indexed.reason}); BM25 remains available`,
|
|
28925
|
+
instanceId: this.instanceId,
|
|
28926
|
+
source: "mailcrawl"
|
|
28927
|
+
});
|
|
28928
|
+
}
|
|
28929
|
+
this.lastIndexedAt = Date.now();
|
|
28930
|
+
return {
|
|
28931
|
+
ok: true,
|
|
28932
|
+
instanceId: this.instanceId,
|
|
28933
|
+
skill: "mailcrawl",
|
|
28934
|
+
chunkCount: result.data.chunksAdded ?? result.data.messages,
|
|
28935
|
+
indexedAt: this.lastIndexedAt,
|
|
28936
|
+
diagnostics
|
|
28937
|
+
};
|
|
28938
|
+
} catch {
|
|
28939
|
+
return this.fail("datasource-unavailable", "mailcrawl sync failed unexpectedly");
|
|
28710
28940
|
}
|
|
28711
|
-
return results;
|
|
28712
28941
|
}
|
|
28713
|
-
|
|
28714
|
-
|
|
28715
|
-
|
|
28716
|
-
|
|
28717
|
-
|
|
28718
|
-
|
|
28719
|
-
|
|
28942
|
+
retrievalMethods() {
|
|
28943
|
+
return [
|
|
28944
|
+
new MailcrawlMethod(this.client, "bm25", this.instanceId, this.tags, this.account, this.mailbox),
|
|
28945
|
+
new MailcrawlMethod(this.client, "semantic", this.instanceId, this.tags, this.account, this.mailbox),
|
|
28946
|
+
new MailcrawlMethod(this.client, "hybrid", this.instanceId, this.tags, this.account, this.mailbox)
|
|
28947
|
+
];
|
|
28948
|
+
}
|
|
28949
|
+
describeSources() {
|
|
28950
|
+
return this.instances.map((instanceId) => ({
|
|
28951
|
+
source: datasourceSourcePath("mailcrawl", instanceId),
|
|
28952
|
+
datasourceId: "mailcrawl",
|
|
28953
|
+
skill: "mailcrawl",
|
|
28954
|
+
instanceId,
|
|
28955
|
+
contentType: "email",
|
|
28956
|
+
metadata: { datasourceId: "mailcrawl", instanceId, tags: this.tags }
|
|
28957
|
+
}));
|
|
28958
|
+
}
|
|
28959
|
+
skillManifest() {
|
|
28960
|
+
const scopes = this.instances.map((id) => `- \`${datasourceSourcePath("mailcrawl", id)}\``).join(`
|
|
28961
|
+
`);
|
|
28720
28962
|
return {
|
|
28721
|
-
|
|
28722
|
-
|
|
28723
|
-
|
|
28724
|
-
|
|
28725
|
-
|
|
28726
|
-
|
|
28727
|
-
|
|
28728
|
-
|
|
28729
|
-
|
|
28730
|
-
|
|
28731
|
-
|
|
28732
|
-
|
|
28733
|
-
|
|
28734
|
-
|
|
28963
|
+
name: "datasource-mailcrawl",
|
|
28964
|
+
description: "Search local email synchronized and indexed by mailcrawl.",
|
|
28965
|
+
content: `# mailcrawl email datasource
|
|
28966
|
+
|
|
28967
|
+
Use this skill for authorized local email retrieval. AutoRAG invokes the external \`mailcrawl\` CLI and never opens its SQLite archive directly.
|
|
28968
|
+
|
|
28969
|
+
## Indexing
|
|
28970
|
+
Indexing is server-managed and incremental; do not trigger sync yourself.
|
|
28971
|
+
|
|
28972
|
+
## Search
|
|
28973
|
+
Call the dedicated \`${datasourceSearchToolName("mailcrawl")}\` tool with a natural-language query. Available scopes:
|
|
28974
|
+
${scopes}
|
|
28975
|
+
|
|
28976
|
+
The selectable retrieval modes are BM25, semantic, and hybrid. Scope arguments can only narrow trusted access. Source identifiers remain opaque and hierarchical.
|
|
28977
|
+
|
|
28978
|
+
## Native CLI
|
|
28979
|
+
The external \`mailcrawl\` CLI owns the archive and index. When the dedicated tool cannot express what you need, call mailcrawl directly through \`bash\`:
|
|
28980
|
+
- \`mailcrawl search --mode hybrid --limit 20 --json "<query>"\` (\`bm25\` and \`semantic\` modes also available; \`--account\` / \`--mailbox\` narrow the archive)
|
|
28981
|
+
Never pass datasource virtual paths (\`/mailcrawl/...\`) to bash; they are not OS paths.`
|
|
28982
|
+
};
|
|
28983
|
+
}
|
|
28984
|
+
fail(code, message) {
|
|
28985
|
+
return {
|
|
28986
|
+
ok: false,
|
|
28987
|
+
instanceId: this.instanceId,
|
|
28988
|
+
skill: "mailcrawl",
|
|
28989
|
+
indexedAt: Date.now(),
|
|
28990
|
+
diagnostics: [
|
|
28991
|
+
{
|
|
28992
|
+
code,
|
|
28993
|
+
severity: code === "datasource-unavailable" ? "warning" : "error",
|
|
28994
|
+
message: `mailcrawl ${message}`,
|
|
28995
|
+
instanceId: this.instanceId,
|
|
28996
|
+
source: "mailcrawl"
|
|
28997
|
+
}
|
|
28998
|
+
],
|
|
28999
|
+
error: message,
|
|
29000
|
+
code,
|
|
29001
|
+
message
|
|
28735
29002
|
};
|
|
28736
29003
|
}
|
|
28737
29004
|
}
|
|
28738
|
-
var
|
|
28739
|
-
var
|
|
29005
|
+
var TAGS;
|
|
29006
|
+
var init_skill9 = __esm(() => {
|
|
28740
29007
|
init_scope2();
|
|
28741
|
-
|
|
28742
|
-
|
|
28743
|
-
|
|
28744
|
-
|
|
28745
|
-
|
|
28746
|
-
|
|
28747
|
-
|
|
28748
|
-
|
|
28749
|
-
|
|
29008
|
+
init_client9();
|
|
29009
|
+
init_config3();
|
|
29010
|
+
init_methods5();
|
|
29011
|
+
TAGS = ["mailcrawl", "email", "pii"];
|
|
29012
|
+
});
|
|
29013
|
+
|
|
29014
|
+
// src/datasource/skills/mailcrawl/index.ts
|
|
29015
|
+
var init_mailcrawl = __esm(() => {
|
|
29016
|
+
init_client9();
|
|
29017
|
+
init_methods5();
|
|
29018
|
+
init_skill9();
|
|
28750
29019
|
});
|
|
28751
29020
|
|
|
28752
29021
|
// src/datasource/crawler-skill.ts
|
|
@@ -29029,7 +29298,7 @@ var init_crawler_skill = __esm(() => {
|
|
|
29029
29298
|
init_errors4();
|
|
29030
29299
|
init_scope2();
|
|
29031
29300
|
init_sidecar();
|
|
29032
|
-
|
|
29301
|
+
init_methods4();
|
|
29033
29302
|
DEFAULT_POLLING_INTERVAL_MS4 = 15 * 60 * 1000;
|
|
29034
29303
|
});
|
|
29035
29304
|
|
|
@@ -30818,14 +31087,27 @@ var init_factory = __esm(() => {
|
|
|
30818
31087
|
...config.channels?.ids !== undefined ? { chatIds: config.channels.ids } : {},
|
|
30819
31088
|
...larkConnectorOptions(config.connector)
|
|
30820
31089
|
}),
|
|
30821
|
-
kakao: (config,
|
|
30822
|
-
|
|
30823
|
-
|
|
30824
|
-
|
|
30825
|
-
|
|
30826
|
-
|
|
30827
|
-
|
|
30828
|
-
|
|
31090
|
+
kakao: (config, workspaceRoot, registrationName) => {
|
|
31091
|
+
const connector = config.connector ?? {};
|
|
31092
|
+
const dataDir = typeof connector.workspacePath === "string" && connector.workspacePath.length > 0 ? connector.workspacePath : join39(homedir4(), "Library", "Application Support", "katok");
|
|
31093
|
+
const sidecar = resolveSidecarOptions({
|
|
31094
|
+
descriptor: CRAWLER_DESCRIPTORS.kakao,
|
|
31095
|
+
connector: config.connector,
|
|
31096
|
+
workspaceRoot,
|
|
31097
|
+
registrationName,
|
|
31098
|
+
instanceId: config.instanceId ?? "default",
|
|
31099
|
+
defaultCrawlerDb: join39(dataDir, "archive.sqlite3")
|
|
31100
|
+
});
|
|
31101
|
+
return new LazykatokSkill({
|
|
31102
|
+
client: new LazykatokClient(connector),
|
|
31103
|
+
...config.instanceId !== undefined ? { instanceId: config.instanceId } : {},
|
|
31104
|
+
...config.pollingIntervalMs !== undefined ? { pollingIntervalMs: config.pollingIntervalMs } : {},
|
|
31105
|
+
...config.tags !== undefined ? { tags: config.tags } : {},
|
|
31106
|
+
channelIds: config.channels?.ids,
|
|
31107
|
+
channelNames: config.channels?.names,
|
|
31108
|
+
...sidecar === undefined ? {} : { sidecar }
|
|
31109
|
+
});
|
|
31110
|
+
},
|
|
30829
31111
|
github: (config, workspaceRoot, registrationName) => new GitHubSkill({
|
|
30830
31112
|
...common(config, workspaceRoot),
|
|
30831
31113
|
skillName: registrationName,
|
|
@@ -30971,7 +31253,7 @@ function migrateLegacyConfig(configPath, legacyPath) {
|
|
|
30971
31253
|
const legacy = readConfigFile(legacyPath, true);
|
|
30972
31254
|
const legacyBytes = readFileSync25(legacyPath);
|
|
30973
31255
|
const migrated = normalizeLegacyConfigPaths(legacy ?? {}, dirname26(legacyPath));
|
|
30974
|
-
const migratedPathsUnchanged = legacy?.workspacePath === migrated.workspacePath && legacy?.memoryPath === migrated.memoryPath && JSON.stringify(legacy?.searchPaths) === JSON.stringify(migrated.searchPaths);
|
|
31256
|
+
const migratedPathsUnchanged = legacy?.workspacePath === migrated.workspacePath && legacy?.memoryPath === migrated.memoryPath && JSON.stringify(legacy?.searchPaths) === JSON.stringify(migrated.searchPaths) && JSON.stringify(legacy?.excludePaths) === JSON.stringify(migrated.excludePaths);
|
|
30975
31257
|
const migratedBytes = migratedPathsUnchanged ? legacyBytes : `${JSON.stringify(migrated, null, 2)}
|
|
30976
31258
|
`;
|
|
30977
31259
|
mkdirSync25(dirname26(configPath), { recursive: true });
|
|
@@ -31009,15 +31291,25 @@ function resolveConfigFileReadOnly(configPath, explicit, legacyPath) {
|
|
|
31009
31291
|
function resolveSearchPaths(searchPaths, origin) {
|
|
31010
31292
|
return searchPaths.map((searchPath) => resolvePersistedPath(searchPath, origin));
|
|
31011
31293
|
}
|
|
31294
|
+
function resolveExcludePaths(value, origin) {
|
|
31295
|
+
if (value === undefined)
|
|
31296
|
+
return;
|
|
31297
|
+
if (!Array.isArray(value) || value.some((path) => typeof path !== "string" || path.length === 0)) {
|
|
31298
|
+
throw new ConfigError("Config field 'excludePaths' must be a non-empty string array");
|
|
31299
|
+
}
|
|
31300
|
+
return resolveSearchPaths(value, origin);
|
|
31301
|
+
}
|
|
31012
31302
|
function resolvePersistedPath(path, origin) {
|
|
31013
31303
|
return isAbsolute10(path) ? path : resolve16(origin, path);
|
|
31014
31304
|
}
|
|
31015
31305
|
function normalizeLegacyConfigPaths(partial, origin) {
|
|
31016
31306
|
const workspacePath = resolvePersistedPath(partial.workspacePath ?? ".", origin);
|
|
31307
|
+
const excludePaths = resolveExcludePaths(partial.excludePaths, workspacePath);
|
|
31017
31308
|
return {
|
|
31018
31309
|
...partial,
|
|
31019
31310
|
workspacePath,
|
|
31020
31311
|
...partial.searchPaths === undefined ? {} : { searchPaths: resolveSearchPaths(partial.searchPaths, workspacePath) },
|
|
31312
|
+
...excludePaths === undefined ? {} : { excludePaths },
|
|
31021
31313
|
...partial.memoryPath === undefined ? {} : { memoryPath: resolvePersistedPath(partial.memoryPath, workspacePath) }
|
|
31022
31314
|
};
|
|
31023
31315
|
}
|
|
@@ -31472,10 +31764,7 @@ function resolveConfig(input) {
|
|
|
31472
31764
|
const configOrigin = dirname26(resolve16(configPath));
|
|
31473
31765
|
const fileWorkspacePath = typeof file.workspacePath === "string" ? resolvePersistedPath(file.workspacePath, configOrigin) : undefined;
|
|
31474
31766
|
const fileSearchPaths = file.searchPaths ? resolveSearchPaths(file.searchPaths, fileWorkspacePath ?? configOrigin) : undefined;
|
|
31475
|
-
|
|
31476
|
-
throw new ConfigError("Config field 'excludePaths' must be a non-empty string array");
|
|
31477
|
-
}
|
|
31478
|
-
const fileExcludePaths = file.excludePaths ? resolveSearchPaths(file.excludePaths, fileWorkspacePath ?? configOrigin) : undefined;
|
|
31767
|
+
const fileExcludePaths = resolveExcludePaths(file.excludePaths, fileWorkspacePath ?? configOrigin);
|
|
31479
31768
|
const searchPaths = flagSearchPaths ?? envSearchPaths ?? fileSearchPaths ?? defaultSearchPaths;
|
|
31480
31769
|
const flagWorkspacePath = flagString(flags, "workspace");
|
|
31481
31770
|
const envWorkspacePath = envString(env, "AUTORAG_WORKSPACE");
|
|
@@ -32211,7 +32500,7 @@ function writeDefaultConfig(path, partial, opts = {}) {
|
|
|
32211
32500
|
full.dupey = partial.dupey ?? { enabled: true };
|
|
32212
32501
|
full.excludeExactDuplicates = partial.excludeExactDuplicates ?? true;
|
|
32213
32502
|
if (partial.excludePaths !== undefined)
|
|
32214
|
-
full.excludePaths =
|
|
32503
|
+
full.excludePaths = resolveExcludePaths(partial.excludePaths, workspacePath);
|
|
32215
32504
|
if (partial.limits !== undefined)
|
|
32216
32505
|
full.limits = normalizeLimitsConfig(partial.limits);
|
|
32217
32506
|
if (partial.refresh !== undefined)
|