@autorag/librarian 2.10.2 → 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/agent/search-documents.d.ts +1 -1
- package/dist/cli/commands/types.d.ts +2 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +925 -592
- package/dist/core.js +1641 -1327
- 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 +1642 -1328
- package/dist/mcp/index.js +1642 -1328
- package/dist/minsync/index.d.ts +1 -1
- package/dist/minsync/installer.d.ts +1 -1
- package/dist/minsync/types.d.ts +7 -0
- package/package.json +1 -1
- package/skills/autorag-doctor/SKILL.md +1 -0
- package/skills/autorag-lite-setup/SKILL.md +1 -1
- package/skills/autorag-setup/SKILL.md +1 -1
package/dist/mcp/index.js
CHANGED
|
@@ -2110,6 +2110,25 @@ function errorMessage(error) {
|
|
|
2110
2110
|
var MANIFEST_FILENAME = "manifest.json";
|
|
2111
2111
|
var init_bundle = () => {};
|
|
2112
2112
|
|
|
2113
|
+
// src/filesystem/path-exclusions.ts
|
|
2114
|
+
import { posix as posix2, win32 as win322 } from "node:path";
|
|
2115
|
+
function createPathExclusionMatcher(excludedPaths, platform = process.platform) {
|
|
2116
|
+
const paths = platform === "win32" ? win322 : posix2;
|
|
2117
|
+
const normalize = (path) => {
|
|
2118
|
+
const absolute = paths.resolve(path);
|
|
2119
|
+
return platform === "win32" ? absolute.toLowerCase() : absolute;
|
|
2120
|
+
};
|
|
2121
|
+
const excluded = excludedPaths.map((path) => {
|
|
2122
|
+
const absolute = normalize(path);
|
|
2123
|
+
return { absolute, prefix: absolute.endsWith(paths.sep) ? absolute : `${absolute}${paths.sep}` };
|
|
2124
|
+
});
|
|
2125
|
+
return (path) => {
|
|
2126
|
+
const absolute = normalize(path);
|
|
2127
|
+
return excluded.some((entry) => absolute === entry.absolute || absolute.startsWith(entry.prefix));
|
|
2128
|
+
};
|
|
2129
|
+
}
|
|
2130
|
+
var init_path_exclusions = () => {};
|
|
2131
|
+
|
|
2113
2132
|
// src/everything/client.ts
|
|
2114
2133
|
import { spawn as spawn2 } from "node:child_process";
|
|
2115
2134
|
import { createHash as createHash4 } from "node:crypto";
|
|
@@ -2227,6 +2246,7 @@ class EverythingClient {
|
|
|
2227
2246
|
run;
|
|
2228
2247
|
launch;
|
|
2229
2248
|
stateDir;
|
|
2249
|
+
isExcludedPath;
|
|
2230
2250
|
binaries;
|
|
2231
2251
|
operation = Promise.resolve();
|
|
2232
2252
|
constructor(options) {
|
|
@@ -2236,6 +2256,7 @@ class EverythingClient {
|
|
|
2236
2256
|
this.run = options.run ?? runProcess;
|
|
2237
2257
|
this.launch = options.launch ?? launchDetached;
|
|
2238
2258
|
this.stateDir = join6(options.root, ".autorag", "everything");
|
|
2259
|
+
this.isExcludedPath = createPathExclusionMatcher(options.excludeFolders ?? [], "win32");
|
|
2239
2260
|
}
|
|
2240
2261
|
isSupported() {
|
|
2241
2262
|
return this.platform === "win32";
|
|
@@ -2255,6 +2276,9 @@ class EverythingClient {
|
|
|
2255
2276
|
}
|
|
2256
2277
|
const es = { ok: true, esPath: binaries.esPath };
|
|
2257
2278
|
const timeoutMs = this.options.timeoutMs ?? DEFAULT_TIMEOUT_MS2;
|
|
2279
|
+
if ((this.options.excludeFolders?.length ?? 0) > 0) {
|
|
2280
|
+
return this.searchWithExclusions(es.esPath, request, timeoutMs);
|
|
2281
|
+
}
|
|
2258
2282
|
const args = buildEverythingSearchArgs(this.instanceName, request, timeoutMs);
|
|
2259
2283
|
const result = await this.run(es.esPath, args, timeoutMs + 5000);
|
|
2260
2284
|
if (result.code !== 0)
|
|
@@ -2266,6 +2290,50 @@ class EverythingClient {
|
|
|
2266
2290
|
}
|
|
2267
2291
|
});
|
|
2268
2292
|
}
|
|
2293
|
+
async searchWithExclusions(esPath, request, timeoutMs) {
|
|
2294
|
+
const deadline = Date.now() + timeoutMs;
|
|
2295
|
+
const offset = request.offset ?? 0;
|
|
2296
|
+
const maxResults = request.maxResults ?? DEFAULT_MAX_RESULTS;
|
|
2297
|
+
const results = [];
|
|
2298
|
+
let skipped = 0;
|
|
2299
|
+
let nativeOffset = 0;
|
|
2300
|
+
const timedOut = () => ({
|
|
2301
|
+
ok: false,
|
|
2302
|
+
reason: "search-failed",
|
|
2303
|
+
message: `Everything search exceeded ${timeoutMs}ms while filtering excluded paths; narrow the query or increase the Everything timeout.`
|
|
2304
|
+
});
|
|
2305
|
+
for (;; ) {
|
|
2306
|
+
const remainingMs = deadline - Date.now();
|
|
2307
|
+
if (remainingMs <= 0)
|
|
2308
|
+
return timedOut();
|
|
2309
|
+
const args = buildEverythingSearchArgs(this.instanceName, { ...request, offset: nativeOffset, maxResults: MAX_FILTER_CANDIDATES }, remainingMs);
|
|
2310
|
+
const response = await this.run(esPath, args, remainingMs + 5000);
|
|
2311
|
+
if (response.code !== 0)
|
|
2312
|
+
return this.failure("search-failed", esPath, args, response);
|
|
2313
|
+
if (Date.now() >= deadline)
|
|
2314
|
+
return timedOut();
|
|
2315
|
+
let entries;
|
|
2316
|
+
try {
|
|
2317
|
+
entries = parseEverythingJson(response.stdout);
|
|
2318
|
+
} catch (error) {
|
|
2319
|
+
return { ok: false, reason: "search-failed", message: error.message };
|
|
2320
|
+
}
|
|
2321
|
+
for (const entry of entries) {
|
|
2322
|
+
if (this.isExcludedPath(entry.path))
|
|
2323
|
+
continue;
|
|
2324
|
+
if (skipped < offset) {
|
|
2325
|
+
skipped += 1;
|
|
2326
|
+
continue;
|
|
2327
|
+
}
|
|
2328
|
+
results.push(entry);
|
|
2329
|
+
if (results.length >= maxResults)
|
|
2330
|
+
return { ok: true, results };
|
|
2331
|
+
}
|
|
2332
|
+
if (entries.length < MAX_FILTER_CANDIDATES)
|
|
2333
|
+
return { ok: true, results };
|
|
2334
|
+
nativeOffset += entries.length;
|
|
2335
|
+
}
|
|
2336
|
+
}
|
|
2269
2337
|
index() {
|
|
2270
2338
|
return this.serialize(async () => {
|
|
2271
2339
|
const es = await this.ensureRunning(true);
|
|
@@ -2339,7 +2407,8 @@ class EverythingClient {
|
|
|
2339
2407
|
excludeFolders: [
|
|
2340
2408
|
...new Set([
|
|
2341
2409
|
join6(this.options.root, ".autorag"),
|
|
2342
|
-
...this.options.folders.map((folder) => join6(folder, ".autorag"))
|
|
2410
|
+
...this.options.folders.map((folder) => join6(folder, ".autorag")),
|
|
2411
|
+
...this.options.excludeFolders ?? []
|
|
2343
2412
|
])
|
|
2344
2413
|
]
|
|
2345
2414
|
}));
|
|
@@ -2433,8 +2502,9 @@ function launchDetached(command, args) {
|
|
|
2433
2502
|
child.on("error", () => {});
|
|
2434
2503
|
child.unref();
|
|
2435
2504
|
}
|
|
2436
|
-
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;
|
|
2505
|
+
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;
|
|
2437
2506
|
var init_client = __esm(() => {
|
|
2507
|
+
init_path_exclusions();
|
|
2438
2508
|
init_bundle();
|
|
2439
2509
|
DEFAULT_INDEX_TIMEOUT_MS = 10 * 60000;
|
|
2440
2510
|
MAX_OUTPUT_BYTES = 8 * 1024 * 1024;
|
|
@@ -2454,6 +2524,7 @@ function compare(a, b) {
|
|
|
2454
2524
|
return a < b ? -1 : a > b ? 1 : 0;
|
|
2455
2525
|
}
|
|
2456
2526
|
async function walkFileSearch(folders, request, options = {}) {
|
|
2527
|
+
const isExcluded = createPathExclusionMatcher(options.excludeFolders ?? []);
|
|
2457
2528
|
const maxVisited = options.maxVisited ?? DEFAULT_MAX_VISITED;
|
|
2458
2529
|
const maxMatches = options.maxMatches ?? DEFAULT_MAX_MATCHES;
|
|
2459
2530
|
const deadline = Date.now() + (options.deadlineMs ?? DEFAULT_DEADLINE_MS);
|
|
@@ -2476,6 +2547,8 @@ async function walkFileSearch(folders, request, options = {}) {
|
|
|
2476
2547
|
outer:
|
|
2477
2548
|
while (pending.length > 0) {
|
|
2478
2549
|
const dir = pending.pop();
|
|
2550
|
+
if (isExcluded(dir))
|
|
2551
|
+
continue;
|
|
2479
2552
|
if (Date.now() >= deadline) {
|
|
2480
2553
|
truncated = true;
|
|
2481
2554
|
break;
|
|
@@ -2493,6 +2566,8 @@ async function walkFileSearch(folders, request, options = {}) {
|
|
|
2493
2566
|
}
|
|
2494
2567
|
visited += 1;
|
|
2495
2568
|
const path = join7(dir, dirent.name);
|
|
2569
|
+
if (isExcluded(path))
|
|
2570
|
+
continue;
|
|
2496
2571
|
if (dirent.isDirectory()) {
|
|
2497
2572
|
if (dirent.name === ".autorag")
|
|
2498
2573
|
continue;
|
|
@@ -2527,6 +2602,8 @@ async function walkFileSearch(folders, request, options = {}) {
|
|
|
2527
2602
|
}
|
|
2528
2603
|
}
|
|
2529
2604
|
for (const folder of folders) {
|
|
2605
|
+
if (isExcluded(folder))
|
|
2606
|
+
continue;
|
|
2530
2607
|
if (!underPath(folder) || !matches(folder))
|
|
2531
2608
|
continue;
|
|
2532
2609
|
if (request.kind === "files")
|
|
@@ -2553,6 +2630,7 @@ async function walkFileSearch(folders, request, options = {}) {
|
|
|
2553
2630
|
}
|
|
2554
2631
|
var DEFAULT_MAX_VISITED = 500000, DEFAULT_DEADLINE_MS = 20000, DEFAULT_MAX_MATCHES = 1e4, SORT_ORDERS;
|
|
2555
2632
|
var init_walk = __esm(() => {
|
|
2633
|
+
init_path_exclusions();
|
|
2556
2634
|
SORT_ORDERS = {
|
|
2557
2635
|
"name-ascending": (a, b) => compare(a.name, b.name),
|
|
2558
2636
|
"name-descending": (a, b) => compare(b.name, a.name),
|
|
@@ -2662,6 +2740,7 @@ class FSearchClient {
|
|
|
2662
2740
|
launch;
|
|
2663
2741
|
isAlive;
|
|
2664
2742
|
kill;
|
|
2743
|
+
isExcludedPath;
|
|
2665
2744
|
stateDir;
|
|
2666
2745
|
dbPath;
|
|
2667
2746
|
pidPath;
|
|
@@ -2674,6 +2753,7 @@ class FSearchClient {
|
|
|
2674
2753
|
this.launch = options.launch ?? launchDetached2;
|
|
2675
2754
|
this.isAlive = options.isProcessAlive ?? defaultIsProcessAlive;
|
|
2676
2755
|
this.kill = options.killProcess ?? ((pid) => process.kill(pid, "SIGTERM"));
|
|
2756
|
+
this.isExcludedPath = createPathExclusionMatcher(options.excludeFolders ?? []);
|
|
2677
2757
|
this.stateDir = join8(options.root, ".autorag", "fsearch");
|
|
2678
2758
|
this.dbPath = join8(this.stateDir, "fsearch.db");
|
|
2679
2759
|
this.pidPath = join8(this.stateDir, "watch.pid");
|
|
@@ -2691,6 +2771,7 @@ class FSearchClient {
|
|
|
2691
2771
|
if (unavailable !== undefined || !binary.ok) {
|
|
2692
2772
|
try {
|
|
2693
2773
|
const walked = await walkFileSearch(this.options.folders, request, {
|
|
2774
|
+
excludeFolders: this.options.excludeFolders,
|
|
2694
2775
|
maxVisited: this.options.walkMaxVisited,
|
|
2695
2776
|
deadlineMs: this.options.walkDeadlineMs
|
|
2696
2777
|
});
|
|
@@ -2705,7 +2786,9 @@ class FSearchClient {
|
|
|
2705
2786
|
return { ok: false, reason: "search-failed", message: error.message };
|
|
2706
2787
|
}
|
|
2707
2788
|
}
|
|
2708
|
-
const
|
|
2789
|
+
const hasExclusions = (this.options.excludeFolders?.length ?? 0) > 0;
|
|
2790
|
+
const nativeRequest = hasExclusions ? { ...request, offset: 0, maxResults: MAX_RESULTS_CAP } : request;
|
|
2791
|
+
const args = buildFSearchSearchArgs(this.dbPath, this.socketPath(), nativeRequest);
|
|
2709
2792
|
const result = await this.run(binary.path, args, this.options.timeoutMs ?? DEFAULT_TIMEOUT_MS3);
|
|
2710
2793
|
if (result.code !== 0)
|
|
2711
2794
|
return this.failure("search-failed", binary.path, args, result);
|
|
@@ -2716,6 +2799,10 @@ class FSearchClient {
|
|
|
2716
2799
|
return { ok: false, reason: "search-failed", message: error.message };
|
|
2717
2800
|
}
|
|
2718
2801
|
let entries = parsed.entries;
|
|
2802
|
+
if (hasExclusions) {
|
|
2803
|
+
const isExcluded = this.isExcludedPath;
|
|
2804
|
+
entries = entries.filter((entry) => !isExcluded(entry.path));
|
|
2805
|
+
}
|
|
2719
2806
|
if (request.path !== undefined) {
|
|
2720
2807
|
const prefix = request.path.endsWith(sep3) ? request.path : `${request.path}${sep3}`;
|
|
2721
2808
|
entries = entries.filter((entry) => entry.path === request.path || entry.path.startsWith(prefix));
|
|
@@ -2725,7 +2812,16 @@ class FSearchClient {
|
|
|
2725
2812
|
const maxResults = Math.min(request.maxResults ?? DEFAULT_MAX_RESULTS2, MAX_RESULTS_CAP);
|
|
2726
2813
|
if (entries.length > maxResults)
|
|
2727
2814
|
entries = entries.slice(0, maxResults);
|
|
2728
|
-
return {
|
|
2815
|
+
return {
|
|
2816
|
+
ok: true,
|
|
2817
|
+
backend: "fsearch-cli",
|
|
2818
|
+
results: entries,
|
|
2819
|
+
total: parsed.total,
|
|
2820
|
+
...hasExclusions && parsed.total !== undefined && parsed.total > parsed.entries.length && entries.length < maxResults ? {
|
|
2821
|
+
truncated: true,
|
|
2822
|
+
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.`
|
|
2823
|
+
} : {}
|
|
2824
|
+
};
|
|
2729
2825
|
});
|
|
2730
2826
|
}
|
|
2731
2827
|
index() {
|
|
@@ -2919,6 +3015,7 @@ function defaultIsProcessAlive(pid) {
|
|
|
2919
3015
|
}
|
|
2920
3016
|
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;
|
|
2921
3017
|
var init_client2 = __esm(() => {
|
|
3018
|
+
init_path_exclusions();
|
|
2922
3019
|
init_walk();
|
|
2923
3020
|
DEFAULT_INDEX_TIMEOUT_MS2 = 10 * 60000;
|
|
2924
3021
|
MAX_OUTPUT_BYTES2 = 8 * 1024 * 1024;
|
|
@@ -5514,7 +5611,8 @@ class MinSyncClient {
|
|
|
5514
5611
|
restoreConfig();
|
|
5515
5612
|
return this.lockHeldResult(result.stderr);
|
|
5516
5613
|
}
|
|
5517
|
-
|
|
5614
|
+
const partial = !result.ok && result.code === MINSYNC_PARTIAL_SYNC_EXIT_CODE ? readPartialSync(result.stdout) : undefined;
|
|
5615
|
+
if (!result.ok && partial === undefined) {
|
|
5518
5616
|
restoreConfig();
|
|
5519
5617
|
return {
|
|
5520
5618
|
ok: false,
|
|
@@ -5534,10 +5632,12 @@ class MinSyncClient {
|
|
|
5534
5632
|
}
|
|
5535
5633
|
if (effective.identity)
|
|
5536
5634
|
this.writeIdentity(effective.identity);
|
|
5635
|
+
const filesFailed = partial?.filesFailed ?? [];
|
|
5537
5636
|
return {
|
|
5538
5637
|
ok: true,
|
|
5539
5638
|
synced: readSyncedCount(result.stdout),
|
|
5540
5639
|
workspacePath: this.workspacePath,
|
|
5640
|
+
...filesFailed.length > 0 ? { filesFailed } : {},
|
|
5541
5641
|
...identityChanged ? {
|
|
5542
5642
|
diagnostic: {
|
|
5543
5643
|
code: "embedding-identity-mismatch",
|
|
@@ -5569,6 +5669,19 @@ function readConfigSnapshot(configPath) {
|
|
|
5569
5669
|
throw error;
|
|
5570
5670
|
}
|
|
5571
5671
|
}
|
|
5672
|
+
function readPartialSync(stdout) {
|
|
5673
|
+
const parsed = parseJson(stdout);
|
|
5674
|
+
if (!isRecord4(parsed) || parsed.partial !== true)
|
|
5675
|
+
return;
|
|
5676
|
+
const failures = Array.isArray(parsed.files_failed) ? parsed.files_failed : [];
|
|
5677
|
+
return {
|
|
5678
|
+
filesFailed: failures.filter(isRecord4).map((failure) => ({
|
|
5679
|
+
path: typeof failure.path === "string" ? failure.path : "(unknown)",
|
|
5680
|
+
stage: typeof failure.stage === "string" ? failure.stage : "unknown",
|
|
5681
|
+
error: typeof failure.error === "string" ? failure.error : "unknown error"
|
|
5682
|
+
}))
|
|
5683
|
+
};
|
|
5684
|
+
}
|
|
5572
5685
|
function readSyncedCount(stdout) {
|
|
5573
5686
|
const parsed = parseJson(stdout);
|
|
5574
5687
|
if (!isRecord4(parsed))
|
|
@@ -5624,7 +5737,7 @@ function isIdentity(value) {
|
|
|
5624
5737
|
function isRecord4(value) {
|
|
5625
5738
|
return typeof value === "object" && value !== null;
|
|
5626
5739
|
}
|
|
5627
|
-
var MINSYNC_OLLAMA_MIGRATION_MESSAGE = "This workspace uses the legacy 768-dimensional Ollama/TEI embedding path. Reindex explicitly, or pin an explicit Ollama/TEI embedder config before using the new default runtime.", API_KEY_ENV_PATTERN, MINSYNC_LOCK_HELD_EXIT_CODE = 3, MinSyncQueryError;
|
|
5740
|
+
var MINSYNC_OLLAMA_MIGRATION_MESSAGE = "This workspace uses the legacy 768-dimensional Ollama/TEI embedding path. Reindex explicitly, or pin an explicit Ollama/TEI embedder config before using the new default runtime.", API_KEY_ENV_PATTERN, MINSYNC_LOCK_HELD_EXIT_CODE = 3, MINSYNC_PARTIAL_SYNC_EXIT_CODE = 6, MinSyncQueryError;
|
|
5628
5741
|
var init_client4 = __esm(() => {
|
|
5629
5742
|
init_embedder_config();
|
|
5630
5743
|
init_errors();
|
|
@@ -5868,7 +5981,7 @@ function isGitHubAsset(value) {
|
|
|
5868
5981
|
function isRecord5(value) {
|
|
5869
5982
|
return typeof value === "object" && value !== null;
|
|
5870
5983
|
}
|
|
5871
|
-
var LATEST_RELEASE_URL = "https://api.github.com/repos/NomaDamas/MinSync/releases/latest", SHA256_HEX_PATTERN, MINSYNC_VERSION = "0.4.
|
|
5984
|
+
var LATEST_RELEASE_URL = "https://api.github.com/repos/NomaDamas/MinSync/releases/latest", SHA256_HEX_PATTERN, MINSYNC_VERSION = "0.4.8", MINSYNC_CARGO_INSTALL_TIMEOUT_MS, CARGO_PROBE_TIMEOUT_MS = 5000, MinSyncReleaseError;
|
|
5872
5985
|
var init_installer2 = __esm(() => {
|
|
5873
5986
|
init_process();
|
|
5874
5987
|
SHA256_HEX_PATTERN = /^[a-f0-9]{64}$/;
|
|
@@ -9171,12 +9284,43 @@ function digitsEndAt(text, from) {
|
|
|
9171
9284
|
end += 1;
|
|
9172
9285
|
return end;
|
|
9173
9286
|
}
|
|
9174
|
-
function continuesAsProse(answer, from) {
|
|
9175
|
-
|
|
9287
|
+
function continuesAsProse(answer, openIndex, from) {
|
|
9288
|
+
let cursor = from;
|
|
9289
|
+
let leadingEmphasis = 0;
|
|
9290
|
+
while (openIndex - 1 - leadingEmphasis >= 0 && (answer[openIndex - 1 - leadingEmphasis] === "*" || answer[openIndex - 1 - leadingEmphasis] === "_")) {
|
|
9291
|
+
leadingEmphasis += 1;
|
|
9292
|
+
}
|
|
9293
|
+
if (leadingEmphasis > 0) {
|
|
9294
|
+
let trailingEmphasis = 0;
|
|
9295
|
+
while (cursor + trailingEmphasis < answer.length && trailingEmphasis < leadingEmphasis && answer[cursor + trailingEmphasis] === answer[openIndex - 1 - trailingEmphasis]) {
|
|
9296
|
+
trailingEmphasis += 1;
|
|
9297
|
+
}
|
|
9298
|
+
if (trailingEmphasis === leadingEmphasis)
|
|
9299
|
+
cursor += trailingEmphasis;
|
|
9300
|
+
}
|
|
9301
|
+
const tokenStart = openIndex - leadingEmphasis;
|
|
9302
|
+
const linePrefix = answer.slice(answer.lastIndexOf(`
|
|
9303
|
+
`, tokenStart - 1) + 1, tokenStart);
|
|
9304
|
+
const atHeadingStart = /^[ \t>*-]*$/u.test(linePrefix);
|
|
9305
|
+
if (atHeadingStart && answer[cursor] === ":")
|
|
9306
|
+
cursor += 1;
|
|
9307
|
+
if (answer[cursor] !== " " && answer[cursor] !== " ")
|
|
9176
9308
|
return false;
|
|
9177
|
-
|
|
9309
|
+
cursor += 1;
|
|
9310
|
+
while (cursor < answer.length && (answer[cursor] === "*" || answer[cursor] === "_" || answer[cursor] === '"' || answer[cursor] === "'" || answer[cursor] === "“" || answer[cursor] === "‘")) {
|
|
9311
|
+
cursor += 1;
|
|
9312
|
+
}
|
|
9313
|
+
const next = answer.codePointAt(cursor);
|
|
9178
9314
|
return next !== undefined && /[\p{L}\p{N}]/u.test(String.fromCodePoint(next));
|
|
9179
9315
|
}
|
|
9316
|
+
function isLegalPinpoint(answer, openIndex, afterCloseIndex) {
|
|
9317
|
+
if (PINPOINT_RANGE_AHEAD.test(answer.slice(afterCloseIndex, afterCloseIndex + 16)))
|
|
9318
|
+
return true;
|
|
9319
|
+
if (PINPOINT_RANGE_BEHIND.test(answer.slice(Math.max(0, openIndex - 16), openIndex)))
|
|
9320
|
+
return true;
|
|
9321
|
+
const prefixWindow = answer.slice(Math.max(0, openIndex - 120), openIndex);
|
|
9322
|
+
return PARA_PINPOINT_PREFIX.test(prefixWindow) || CLAUSE_START_AT_PREFIX.test(prefixWindow) || CASE_CITATION_AT_PREFIX.test(prefixWindow);
|
|
9323
|
+
}
|
|
9180
9324
|
function evidenceMarkerAt(answer, index) {
|
|
9181
9325
|
const ids = [];
|
|
9182
9326
|
let cursor = index + 1;
|
|
@@ -9242,7 +9386,7 @@ function answerMarkers(answer, isFile = () => false) {
|
|
|
9242
9386
|
}
|
|
9243
9387
|
if (char === "[") {
|
|
9244
9388
|
const digitsEnd = digitsEndAt(answer, index + 1);
|
|
9245
|
-
if (digitsEnd > index + 1 && answer[digitsEnd] === "]" && answer[digitsEnd + 1] !== "(" && !continuesAsProse(answer, digitsEnd + 1)) {
|
|
9389
|
+
if (digitsEnd > index + 1 && answer[digitsEnd] === "]" && answer[digitsEnd + 1] !== "(" && !continuesAsProse(answer, index, digitsEnd + 1) && !isLegalPinpoint(answer, index, digitsEnd + 1)) {
|
|
9246
9390
|
markers.push({
|
|
9247
9391
|
kind: "number",
|
|
9248
9392
|
start: leadingStart(index),
|
|
@@ -9314,7 +9458,14 @@ function assertResultsMappingOneToOne(label, results, mapping) {
|
|
|
9314
9458
|
].filter((note) => note !== undefined);
|
|
9315
9459
|
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.");
|
|
9316
9460
|
}
|
|
9317
|
-
var MAX_FILE_MARKER_CLOSERS = 8, FILE_MARKER_PREFIX = "[file:";
|
|
9461
|
+
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;
|
|
9462
|
+
var init_citations = __esm(() => {
|
|
9463
|
+
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;
|
|
9464
|
+
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;
|
|
9465
|
+
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;
|
|
9466
|
+
PINPOINT_RANGE_AHEAD = /^[ \t\u00a0]*[-–—][ \t\u00a0]*\[\d+\]/u;
|
|
9467
|
+
PINPOINT_RANGE_BEHIND = /\[\d+\][ \t\u00a0]*[-–—][ \t\u00a0]*$/u;
|
|
9468
|
+
});
|
|
9318
9469
|
|
|
9319
9470
|
// src/agent/evidence-ledger.ts
|
|
9320
9471
|
import { createHash as createHash10 } from "node:crypto";
|
|
@@ -9598,6 +9749,7 @@ function deriveResultsFromAnswer(text, ledger, options) {
|
|
|
9598
9749
|
}
|
|
9599
9750
|
var MAX_EXCERPT_CHARS = 400, MAX_CLAIM_CHARS = 280, CLAIM_LOOKBACK_CHARS = 600, SENTENCE_END, LEADING_LIST_MARK, EVIDENCE_MARKER_TEXT, LEADING_BLANK;
|
|
9600
9751
|
var init_answer_citations = __esm(() => {
|
|
9752
|
+
init_citations();
|
|
9601
9753
|
init_evidence_ledger();
|
|
9602
9754
|
SENTENCE_END = /[.!?。!?]/u;
|
|
9603
9755
|
LEADING_LIST_MARK = /^[\s\-*•>]+/u;
|
|
@@ -9606,7 +9758,7 @@ var init_answer_citations = __esm(() => {
|
|
|
9606
9758
|
});
|
|
9607
9759
|
|
|
9608
9760
|
// src/agent/answer-guidelines.ts
|
|
9609
|
-
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.";
|
|
9761
|
+
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.";
|
|
9610
9762
|
|
|
9611
9763
|
// src/agent/datasource-skill.ts
|
|
9612
9764
|
import { Type as Type3 } from "typebox";
|
|
@@ -13942,6 +14094,7 @@ function recordStructuredResultsSession(sessionId, query, details, memory, compo
|
|
|
13942
14094
|
}
|
|
13943
14095
|
var init_search_documents = __esm(() => {
|
|
13944
14096
|
init_memory();
|
|
14097
|
+
init_citations();
|
|
13945
14098
|
});
|
|
13946
14099
|
|
|
13947
14100
|
// src/agent/search-all-tool.ts
|
|
@@ -14258,6 +14411,10 @@ function buildSystemPrompt(config) {
|
|
|
14258
14411
|
const modelId = config.modelId ?? "the configured model";
|
|
14259
14412
|
const toolLines = [
|
|
14260
14413
|
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)"),
|
|
14414
|
+
...config.remoteSession ? [] : [
|
|
14415
|
+
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"),
|
|
14416
|
+
toolLine(config, "edit", "surgically update an existing deliverable or config file when the user asks (local sessions only)")
|
|
14417
|
+
],
|
|
14261
14418
|
toolLine(config, "jikji_find", "local discovery through Jikji answer packs"),
|
|
14262
14419
|
toolLine(config, "everything_search", "instant Windows file/folder name search (voidtools Everything) across the configured search folders"),
|
|
14263
14420
|
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"),
|
|
@@ -14274,6 +14431,12 @@ function buildSystemPrompt(config) {
|
|
|
14274
14431
|
...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`),
|
|
14275
14432
|
...config.toolNames.filter((name) => ![
|
|
14276
14433
|
"bash",
|
|
14434
|
+
"read",
|
|
14435
|
+
"write",
|
|
14436
|
+
"edit",
|
|
14437
|
+
"grep",
|
|
14438
|
+
"find",
|
|
14439
|
+
"ls",
|
|
14277
14440
|
"jikji_find",
|
|
14278
14441
|
"everything_search",
|
|
14279
14442
|
"fsearch_search",
|
|
@@ -14406,8 +14569,9 @@ Your answer is your final reply, written as plain text, never as a tool call. Ci
|
|
|
14406
14569
|
## Answer Guidelines
|
|
14407
14570
|
|
|
14408
14571
|
- **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.
|
|
14409
|
-
- **Bullet-point core answer**:
|
|
14410
|
-
- **Direct answer only**:
|
|
14572
|
+
- **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.
|
|
14573
|
+
- **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.
|
|
14574
|
+
- ${ANSWER_DELIVERABLE_RULE} ${ANSWER_DELIVERABLE_DELTA_RULE}
|
|
14411
14575
|
- ${ANSWER_CITATION_RULE}
|
|
14412
14576
|
- ${ANSWER_IMAGE_EMBED_RULE} ${ANSWER_IMAGE_DELTA_RULE}
|
|
14413
14577
|
- **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.
|
|
@@ -20364,7 +20528,8 @@ class AutoRAGAgent {
|
|
|
20364
20528
|
const everythingClient = new EverythingClient({
|
|
20365
20529
|
...options.everything ?? {},
|
|
20366
20530
|
root: this.workspaceProjectRoot,
|
|
20367
|
-
folders: this.searchPaths
|
|
20531
|
+
folders: this.searchPaths,
|
|
20532
|
+
excludeFolders: this.excludePaths
|
|
20368
20533
|
});
|
|
20369
20534
|
if (everythingClient.isSupported())
|
|
20370
20535
|
this.everythingClient = everythingClient;
|
|
@@ -20470,8 +20635,9 @@ class AutoRAGAgent {
|
|
|
20470
20635
|
return true;
|
|
20471
20636
|
});
|
|
20472
20637
|
this.tools = tools;
|
|
20638
|
+
const builtinToolNames = this.remoteSession ? PI_BUILTIN_TOOL_NAMES.filter((name) => name !== "write" && name !== "edit") : PI_BUILTIN_TOOL_NAMES;
|
|
20473
20639
|
const toolNames = [
|
|
20474
|
-
...
|
|
20640
|
+
...builtinToolNames,
|
|
20475
20641
|
...tools.map((tool) => tool.name),
|
|
20476
20642
|
...this.jevExtension !== undefined ? [JEV_TOOL_NAME] : []
|
|
20477
20643
|
];
|
|
@@ -20487,7 +20653,7 @@ class AutoRAGAgent {
|
|
|
20487
20653
|
this.innerAgent = {
|
|
20488
20654
|
state: {
|
|
20489
20655
|
systemPrompt,
|
|
20490
|
-
tools: [...
|
|
20656
|
+
tools: [...builtinToolNames.map((name) => ({ name })), ...tools],
|
|
20491
20657
|
messages: []
|
|
20492
20658
|
},
|
|
20493
20659
|
transformContext: (messages) => this.withMemoryContext(messages)
|
|
@@ -20804,6 +20970,7 @@ ${context.text}
|
|
|
20804
20970
|
...this.jevExtension !== undefined ? [this.jevExtension] : []
|
|
20805
20971
|
],
|
|
20806
20972
|
...this.jevExtension !== undefined ? { extensionToolNames: [JEV_TOOL_NAME] } : {},
|
|
20973
|
+
remoteSession: this.remoteSession,
|
|
20807
20974
|
...this.updateNotice === undefined ? {} : { updateNotice: this.updateNotice }
|
|
20808
20975
|
});
|
|
20809
20976
|
this.boundPiRuntime = runtime.runtime;
|
|
@@ -21730,8 +21897,9 @@ ${baseline}
|
|
|
21730
21897
|
` + `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.
|
|
21731
21898
|
|
|
21732
21899
|
` + `Formatting and content rules for the answer:
|
|
21733
|
-
` + `-
|
|
21734
|
-
` + `-
|
|
21900
|
+
` + `- 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.
|
|
21901
|
+
` + `- 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.
|
|
21902
|
+
` + `- ${ANSWER_DELIVERABLE_RULE}
|
|
21735
21903
|
` + `- ${ANSWER_CITATION_RULE}
|
|
21736
21904
|
` + `- ${ANSWER_IMAGE_EMBED_RULE}
|
|
21737
21905
|
` + `- Do not report per-source negative findings (e.g. "no information found in Slack" or "checked Drive but found nothing").
|
|
@@ -21770,12 +21938,14 @@ ${baseline}
|
|
|
21770
21938
|
` + `- NEVER restate or re-list first-answer facts that remain correct, and never repeat its bullet list.
|
|
21771
21939
|
` + `- Mark each item clearly as a correction or as a new finding.
|
|
21772
21940
|
` + `- 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.
|
|
21941
|
+
` + `- ${ANSWER_DELIVERABLE_RULE} ${ANSWER_DELIVERABLE_DELTA_RULE}
|
|
21773
21942
|
` + `- ${ANSWER_CITATION_RULE} A correction or new finding that relies on evidence the first answer cited must cite that evidence's id again.
|
|
21774
21943
|
` + `- ${ANSWER_IMAGE_EMBED_RULE} ${ANSWER_IMAGE_DELTA_RULE}
|
|
21775
21944
|
` + `- Do not report per-source negative findings (e.g. "no information found in Slack").
|
|
21776
21945
|
` + `- 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):
|
|
21777
|
-
` + `-
|
|
21778
|
-
` + `-
|
|
21946
|
+
` + `- 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.
|
|
21947
|
+
` + `- 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.
|
|
21948
|
+
` + `- ${ANSWER_DELIVERABLE_RULE}${fastAnswer !== undefined ? ` ${ANSWER_DELIVERABLE_DELTA_RULE}` : ""}
|
|
21779
21949
|
` + `- ${ANSWER_CITATION_RULE}
|
|
21780
21950
|
` + `- ${ANSWER_IMAGE_EMBED_RULE}
|
|
21781
21951
|
` + `- Do not report per-source negative findings (e.g. "no information found in Slack").
|
|
@@ -21803,8 +21973,9 @@ ${initialRetrievalContext ?? "Pending; continue only with a brief progress state
|
|
|
21803
21973
|
` + `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.
|
|
21804
21974
|
|
|
21805
21975
|
` + `Formatting and content rules for the answer:
|
|
21806
|
-
` + `-
|
|
21807
|
-
` + `-
|
|
21976
|
+
` + `- 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.
|
|
21977
|
+
` + `- 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.
|
|
21978
|
+
` + `- ${ANSWER_DELIVERABLE_RULE}
|
|
21808
21979
|
` + `- ${ANSWER_CITATION_RULE}
|
|
21809
21980
|
` + `- ${ANSWER_IMAGE_EMBED_RULE}
|
|
21810
21981
|
` + `- Do not report per-source negative findings (e.g. "no information found in Slack").
|
|
@@ -21959,6 +22130,7 @@ ${initialRetrievalContext ?? "Pending; continue only with a brief progress state
|
|
|
21959
22130
|
...summary.diagnostics,
|
|
21960
22131
|
...minsyncDiagnostics,
|
|
21961
22132
|
...stagingExcludedDiagnostics(minsync?.stagingExcluded),
|
|
22133
|
+
...fileSkippedDiagnostics(minsync?.filesFailed),
|
|
21962
22134
|
...everythingDiagnostics,
|
|
21963
22135
|
...fsearchDiagnostics
|
|
21964
22136
|
],
|
|
@@ -22595,6 +22767,14 @@ function stagingExcludedDiagnostics(excluded) {
|
|
|
22595
22767
|
source
|
|
22596
22768
|
}));
|
|
22597
22769
|
}
|
|
22770
|
+
function fileSkippedDiagnostics(failed) {
|
|
22771
|
+
return (failed ?? []).map((failure) => ({
|
|
22772
|
+
code: "minsync-file-skipped",
|
|
22773
|
+
severity: "warning",
|
|
22774
|
+
message: `MinSync skipped this file at the ${failure.stage} stage and will retry it on the next sync: ${sanitizeDiagnosticMessage(failure.error)}`,
|
|
22775
|
+
source: failure.path
|
|
22776
|
+
}));
|
|
22777
|
+
}
|
|
22598
22778
|
function toSearchDiagnostic(diagnostic) {
|
|
22599
22779
|
return {
|
|
22600
22780
|
code: diagnostic.code,
|
|
@@ -23057,7 +23237,7 @@ function chatCrawler(id) {
|
|
|
23057
23237
|
hierarchy: (metadata) => ["chats", str(metadata.chatName) ?? str(metadata.chatId) ?? "unknown"]
|
|
23058
23238
|
};
|
|
23059
23239
|
}
|
|
23060
|
-
var str = (value) => typeof value === "string" && value.length > 0 ? value : undefined, slack, CRAWLER_DESCRIPTORS;
|
|
23240
|
+
var str = (value) => typeof value === "string" && value.length > 0 ? value : undefined, slack, kakao, CRAWLER_DESCRIPTORS;
|
|
23061
23241
|
var init_descriptors = __esm(() => {
|
|
23062
23242
|
slack = {
|
|
23063
23243
|
id: "slack",
|
|
@@ -23088,10 +23268,32 @@ var init_descriptors = __esm(() => {
|
|
|
23088
23268
|
str(metadata.channelName) ?? str(metadata.channelId) ?? "unknown"
|
|
23089
23269
|
]
|
|
23090
23270
|
};
|
|
23271
|
+
kakao = {
|
|
23272
|
+
id: "kakao",
|
|
23273
|
+
keyExpression: "m.chunk_id",
|
|
23274
|
+
chunkId: (key) => key,
|
|
23275
|
+
fromClause: "chunks m",
|
|
23276
|
+
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))",
|
|
23277
|
+
textExpression: "coalesce(m.text, '')",
|
|
23278
|
+
titleExpression: "m.chat_name",
|
|
23279
|
+
epochSecondsExpression: "unixepoch(m.started_at)",
|
|
23280
|
+
fts: { table: "chunks_fts", joinOn: "chunks_fts.rowid = m.rowid" },
|
|
23281
|
+
metadata: [
|
|
23282
|
+
{ sql: "m.chat_id", key: "chatId" },
|
|
23283
|
+
{ sql: "m.chat_name", key: "chatName" },
|
|
23284
|
+
{ sql: "m.sender_nickname", key: "senderNickname" },
|
|
23285
|
+
{ sql: "m.started_at", key: "startedAt" },
|
|
23286
|
+
{ sql: "m.ended_at", key: "endedAt" }
|
|
23287
|
+
],
|
|
23288
|
+
channelIdExpression: "m.chat_id",
|
|
23289
|
+
channelNameExpression: "m.chat_name",
|
|
23290
|
+
hierarchy: (metadata) => ["chats", str(metadata.chatName) ?? str(metadata.chatId) ?? "unknown"]
|
|
23291
|
+
};
|
|
23091
23292
|
CRAWLER_DESCRIPTORS = {
|
|
23092
23293
|
slack,
|
|
23093
23294
|
telegram: chatCrawler("telegram"),
|
|
23094
|
-
whatsapp: chatCrawler("whatsapp")
|
|
23295
|
+
whatsapp: chatCrawler("whatsapp"),
|
|
23296
|
+
kakao
|
|
23095
23297
|
};
|
|
23096
23298
|
});
|
|
23097
23299
|
|
|
@@ -27239,1436 +27441,1528 @@ var init_methods3 = __esm(() => {
|
|
|
27239
27441
|
DEFAULT_KAKAO_TAGS = ["kakaotalk", "personal", "pii"];
|
|
27240
27442
|
});
|
|
27241
27443
|
|
|
27242
|
-
// src/datasource/
|
|
27243
|
-
|
|
27244
|
-
|
|
27245
|
-
|
|
27246
|
-
|
|
27247
|
-
|
|
27248
|
-
|
|
27249
|
-
|
|
27250
|
-
|
|
27251
|
-
|
|
27252
|
-
|
|
27253
|
-
|
|
27254
|
-
|
|
27255
|
-
|
|
27256
|
-
|
|
27257
|
-
|
|
27258
|
-
|
|
27259
|
-
|
|
27260
|
-
|
|
27261
|
-
|
|
27262
|
-
|
|
27263
|
-
|
|
27264
|
-
capabilities: ["chat", "external-cli", "polling", "bm25", "semantic"],
|
|
27265
|
-
tags: this.tags,
|
|
27266
|
-
status: "active",
|
|
27267
|
-
requiresExternalCli: true,
|
|
27268
|
-
datasourceId: KAKAO_DATASOURCE_ID2,
|
|
27269
|
-
instanceId: this.instanceId,
|
|
27270
|
-
instances: this.instances
|
|
27271
|
-
};
|
|
27444
|
+
// src/datasource/sidecar/fusion.ts
|
|
27445
|
+
function encodeVector(vector) {
|
|
27446
|
+
const floats = Float32Array.from(vector);
|
|
27447
|
+
return new Uint8Array(floats.buffer, floats.byteOffset, floats.byteLength);
|
|
27448
|
+
}
|
|
27449
|
+
function decodeVector(blob) {
|
|
27450
|
+
if (blob.byteLength % Float32Array.BYTES_PER_ELEMENT !== 0)
|
|
27451
|
+
return;
|
|
27452
|
+
return new Float32Array(new Uint8Array(blob).buffer);
|
|
27453
|
+
}
|
|
27454
|
+
function cosine3(a, b) {
|
|
27455
|
+
if (a.length === 0 || a.length !== b.length)
|
|
27456
|
+
return 0;
|
|
27457
|
+
let dot = 0;
|
|
27458
|
+
let normA = 0;
|
|
27459
|
+
let normB = 0;
|
|
27460
|
+
for (let index = 0;index < a.length; index += 1) {
|
|
27461
|
+
const x = a[index] ?? 0;
|
|
27462
|
+
const y = b[index] ?? 0;
|
|
27463
|
+
dot += x * y;
|
|
27464
|
+
normA += x * x;
|
|
27465
|
+
normB += y * y;
|
|
27272
27466
|
}
|
|
27273
|
-
|
|
27274
|
-
return
|
|
27275
|
-
|
|
27276
|
-
|
|
27277
|
-
|
|
27278
|
-
|
|
27467
|
+
if (normA === 0 || normB === 0)
|
|
27468
|
+
return 0;
|
|
27469
|
+
return dot / Math.sqrt(normA * normB);
|
|
27470
|
+
}
|
|
27471
|
+
function topKByCosine(query, rows, k) {
|
|
27472
|
+
if (k <= 0)
|
|
27473
|
+
return [];
|
|
27474
|
+
const scored = [];
|
|
27475
|
+
for (const row of rows) {
|
|
27476
|
+
const score = cosine3(query, row.vector);
|
|
27477
|
+
if (score > 0)
|
|
27478
|
+
scored.push({ key: row.key, score });
|
|
27279
27479
|
}
|
|
27280
|
-
|
|
27281
|
-
|
|
27282
|
-
|
|
27283
|
-
|
|
27284
|
-
|
|
27285
|
-
|
|
27286
|
-
|
|
27287
|
-
|
|
27288
|
-
|
|
27289
|
-
|
|
27290
|
-
|
|
27291
|
-
|
|
27292
|
-
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;
|
|
27293
|
-
return {
|
|
27294
|
-
ok: true,
|
|
27295
|
-
instanceId: this.instanceId,
|
|
27296
|
-
skill: KAKAO_DATASOURCE_ID2,
|
|
27297
|
-
chunkCount,
|
|
27298
|
-
indexedAt: this.lastIndexedAt,
|
|
27299
|
-
diagnostics: []
|
|
27300
|
-
};
|
|
27301
|
-
} catch (error) {
|
|
27302
|
-
return this.fail("datasource-unavailable", {
|
|
27303
|
-
ok: false,
|
|
27304
|
-
reason: "spawn-error",
|
|
27305
|
-
stdout: "",
|
|
27306
|
-
stderr: describeRetrievalError(error),
|
|
27307
|
-
code: null
|
|
27308
|
-
});
|
|
27480
|
+
scored.sort((a, b) => b.score - a.score || a.key.localeCompare(b.key));
|
|
27481
|
+
return scored.slice(0, k);
|
|
27482
|
+
}
|
|
27483
|
+
function reciprocalRankFusion(lists) {
|
|
27484
|
+
const scores = new Map;
|
|
27485
|
+
for (const list of lists) {
|
|
27486
|
+
const seen = new Set;
|
|
27487
|
+
for (const [index, key] of list.entries()) {
|
|
27488
|
+
if (seen.has(key))
|
|
27489
|
+
continue;
|
|
27490
|
+
seen.add(key);
|
|
27491
|
+
scores.set(key, (scores.get(key) ?? 0) + 1 / (RRF_K2 + index + 1));
|
|
27309
27492
|
}
|
|
27310
27493
|
}
|
|
27311
|
-
|
|
27312
|
-
|
|
27313
|
-
|
|
27314
|
-
|
|
27494
|
+
return [...scores.entries()].map(([key, score]) => ({ key, score })).sort((a, b) => b.score - a.score);
|
|
27495
|
+
}
|
|
27496
|
+
var RRF_K2 = 60;
|
|
27497
|
+
|
|
27498
|
+
// src/datasource/sidecar/index.ts
|
|
27499
|
+
import { createHash as createHash14 } from "node:crypto";
|
|
27500
|
+
import { mkdirSync as mkdirSync22, statSync as statSync3 } from "node:fs";
|
|
27501
|
+
import { dirname as dirname24 } from "node:path";
|
|
27502
|
+
import { DatabaseSync as DatabaseSync2 } from "node:sqlite";
|
|
27503
|
+
import { pathToFileURL as pathToFileURL2 } from "node:url";
|
|
27504
|
+
|
|
27505
|
+
class CrawlerSidecar {
|
|
27506
|
+
descriptor;
|
|
27507
|
+
options;
|
|
27508
|
+
select;
|
|
27509
|
+
where;
|
|
27510
|
+
opened;
|
|
27511
|
+
attached;
|
|
27512
|
+
lastSemantic;
|
|
27513
|
+
constructor(options) {
|
|
27514
|
+
this.options = options;
|
|
27515
|
+
const { descriptor } = options;
|
|
27516
|
+
this.descriptor = descriptor;
|
|
27517
|
+
const metadataColumns = descriptor.metadata.map((column, index) => `${column.sql} as m${index}`);
|
|
27518
|
+
this.select = [
|
|
27519
|
+
`${descriptor.keyExpression} as k`,
|
|
27520
|
+
`${descriptor.textExpression} as content`,
|
|
27521
|
+
`${descriptor.titleExpression ?? "null"} as title`,
|
|
27522
|
+
`${descriptor.epochSecondsExpression ?? "null"} as epoch`,
|
|
27523
|
+
...metadataColumns
|
|
27524
|
+
].join(", ");
|
|
27525
|
+
const scope = descriptor.scopeExpression !== undefined && options.workspaceId !== undefined;
|
|
27526
|
+
const allowlist = [
|
|
27527
|
+
...(options.channelIds?.length ?? 0) > 0 ? [`${descriptor.channelIdExpression} in (${options.channelIds?.map(() => "?").join(", ")})`] : [],
|
|
27528
|
+
...(options.channelNames?.length ?? 0) > 0 ? [`${descriptor.channelNameExpression} in (${options.channelNames?.map(() => "?").join(", ")})`] : []
|
|
27315
27529
|
];
|
|
27530
|
+
this.where = [
|
|
27531
|
+
descriptor.liveWhere,
|
|
27532
|
+
...scope ? [`${descriptor.scopeExpression} = ?`] : [],
|
|
27533
|
+
...allowlist.length > 0 ? [`(${allowlist.join(" or ")})`] : []
|
|
27534
|
+
].join(" and ");
|
|
27316
27535
|
}
|
|
27317
|
-
|
|
27318
|
-
|
|
27319
|
-
|
|
27320
|
-
|
|
27321
|
-
|
|
27322
|
-
|
|
27323
|
-
|
|
27324
|
-
|
|
27325
|
-
|
|
27326
|
-
|
|
27327
|
-
|
|
27328
|
-
|
|
27329
|
-
|
|
27536
|
+
get db() {
|
|
27537
|
+
if (this.opened !== undefined)
|
|
27538
|
+
return this.opened;
|
|
27539
|
+
mkdirSync22(dirname24(this.options.sidecarDbPath), { recursive: true });
|
|
27540
|
+
const db = new DatabaseSync2(this.options.sidecarDbPath);
|
|
27541
|
+
db.exec(`pragma busy_timeout = ${BUSY_TIMEOUT_MS}`);
|
|
27542
|
+
db.exec("pragma journal_mode = wal");
|
|
27543
|
+
db.exec(`
|
|
27544
|
+
create table if not exists sidecar_meta (k text primary key, v text not null);
|
|
27545
|
+
create table if not exists sidecar_vec (message_key text primary key, hash text not null, vec blob not null);
|
|
27546
|
+
`);
|
|
27547
|
+
this.opened = db;
|
|
27548
|
+
return db;
|
|
27330
27549
|
}
|
|
27331
|
-
|
|
27332
|
-
|
|
27333
|
-
|
|
27334
|
-
|
|
27335
|
-
|
|
27336
|
-
|
|
27337
|
-
|
|
27338
|
-
|
|
27339
|
-
`# KakaoTalk datasource (${KAKAO_SKILL_TYPE})`,
|
|
27340
|
-
"",
|
|
27341
|
-
"This skill searches KakaoTalk chats that are indexed through the external `lazykatok` CLI. AutoRAG never reads KakaoTalk databases directly.",
|
|
27342
|
-
"",
|
|
27343
|
-
"## When to use",
|
|
27344
|
-
"Use this skill when the question is about KakaoTalk conversations, chat participants, or content shared inside chats.",
|
|
27345
|
-
"",
|
|
27346
|
-
"## Indexing",
|
|
27347
|
-
`Indexing is server-managed and refreshed ${cadence}. You do not trigger indexing; just search.`,
|
|
27348
|
-
"",
|
|
27349
|
-
"## How to search",
|
|
27350
|
-
`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:`,
|
|
27351
|
-
instanceSources.length > 0 ? instanceSources : "- (no configured instances)",
|
|
27352
|
-
"",
|
|
27353
|
-
"Chat/channel filtering is owned by lazykatok.",
|
|
27354
|
-
"",
|
|
27355
|
-
"## Native CLI",
|
|
27356
|
-
"The external `lazykatok` CLI owns the archive, index, and credentials. When the dedicated tool cannot express what you need, call lazykatok directly through `bash`:",
|
|
27357
|
-
'- `lazykatok search bm25 "<query>" --json --limit 20` — lexical search (`semantic` mode also available)',
|
|
27358
|
-
"- `lazykatok chunk get <chunkId> --json` — fetch one chunk by id (chunk ids appear in result metadata)",
|
|
27359
|
-
"- `lazykatok chunk context <chunkId> --json` — surrounding messages of a chunk",
|
|
27360
|
-
"Never pass datasource virtual paths (`/kakao/...`) to bash; they are not OS paths.",
|
|
27361
|
-
"",
|
|
27362
|
-
"## Output rules",
|
|
27363
|
-
"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."
|
|
27364
|
-
].join(`
|
|
27365
|
-
`)
|
|
27366
|
-
};
|
|
27367
|
-
}
|
|
27368
|
-
fail(code, result) {
|
|
27369
|
-
const base = result.stderr.length > 0 ? `${result.reason}: ${result.stderr.trim().slice(0, 4000)}` : result.reason;
|
|
27370
|
-
const message = code === "datasource-index-failed" && result.reason === "timeout" ? `${base} — ${connectorSyncTimeoutHint()}` : base;
|
|
27371
|
-
const diagnostic = {
|
|
27372
|
-
code,
|
|
27373
|
-
severity: code === "datasource-unavailable" ? "warning" : "error",
|
|
27374
|
-
message,
|
|
27375
|
-
instanceId: this.instanceId,
|
|
27376
|
-
source: KAKAO_DATASOURCE_ID2
|
|
27377
|
-
};
|
|
27378
|
-
return {
|
|
27379
|
-
ok: false,
|
|
27380
|
-
instanceId: this.instanceId,
|
|
27381
|
-
skill: KAKAO_DATASOURCE_ID2,
|
|
27382
|
-
indexedAt: Date.now(),
|
|
27383
|
-
diagnostics: [diagnostic],
|
|
27384
|
-
error: result.reason,
|
|
27385
|
-
code,
|
|
27386
|
-
message
|
|
27387
|
-
};
|
|
27388
|
-
}
|
|
27389
|
-
}
|
|
27390
|
-
var KAKAO_DATASOURCE_ID2 = "kakao", KAKAO_SKILL_TYPE = "kakaotalk", DEFAULT_INSTANCE_ID4 = "default", DEFAULT_POLLING_INTERVAL_MS3, DEFAULT_KAKAO_TAGS2;
|
|
27391
|
-
var init_skill7 = __esm(() => {
|
|
27392
|
-
init_skip();
|
|
27393
|
-
init_scope2();
|
|
27394
|
-
init_methods3();
|
|
27395
|
-
DEFAULT_POLLING_INTERVAL_MS3 = 15 * 60 * 1000;
|
|
27396
|
-
DEFAULT_KAKAO_TAGS2 = ["kakaotalk", "personal", "pii"];
|
|
27397
|
-
});
|
|
27398
|
-
|
|
27399
|
-
// src/datasource/skills/lazykatok/index.ts
|
|
27400
|
-
var init_lazykatok = __esm(() => {
|
|
27401
|
-
init_client8();
|
|
27402
|
-
init_methods3();
|
|
27403
|
-
init_skill7();
|
|
27404
|
-
init_types5();
|
|
27405
|
-
});
|
|
27406
|
-
|
|
27407
|
-
// src/datasource/skills/mail-export/connector.ts
|
|
27408
|
-
import { readdir as readdir4, readFile as readFile5, stat as stat6 } from "node:fs/promises";
|
|
27409
|
-
import { basename as basename12, dirname as dirname24, extname as extname6, join as join37 } from "node:path";
|
|
27410
|
-
import { simpleParser as simpleParser2 } from "mailparser";
|
|
27411
|
-
|
|
27412
|
-
class MailExportConnector {
|
|
27413
|
-
options;
|
|
27414
|
-
constructor(options = {}) {
|
|
27415
|
-
this.options = options;
|
|
27416
|
-
}
|
|
27417
|
-
async fetch() {
|
|
27418
|
-
const paths = this.options.paths ?? [];
|
|
27419
|
-
if (paths.length === 0)
|
|
27420
|
-
return { ok: false, reason: "not-configured", message: "no mail export paths configured" };
|
|
27421
|
-
const maxDocuments = this.options.maxDocuments ?? DEFAULT_MAX_DOCUMENTS3;
|
|
27422
|
-
const maxBytes = this.options.maxBytesPerFile ?? DEFAULT_MAX_BYTES_PER_FILE2;
|
|
27423
|
-
const files = [];
|
|
27424
|
-
let missingPaths = 0;
|
|
27425
|
-
for (const path of paths) {
|
|
27426
|
-
try {
|
|
27427
|
-
const info = await stat6(path);
|
|
27428
|
-
if (info.isDirectory()) {
|
|
27429
|
-
const entries = await readdir4(path, { recursive: true, withFileTypes: true });
|
|
27430
|
-
for (const entry of entries) {
|
|
27431
|
-
if (!entry.isFile())
|
|
27432
|
-
continue;
|
|
27433
|
-
const extension = extname6(entry.name).toLowerCase();
|
|
27434
|
-
if (extension === ".eml" || extension === ".mbox") {
|
|
27435
|
-
files.push(join37(entry.parentPath, entry.name));
|
|
27436
|
-
}
|
|
27437
|
-
}
|
|
27438
|
-
} else if (info.isFile()) {
|
|
27439
|
-
files.push(path);
|
|
27440
|
-
}
|
|
27441
|
-
} catch {
|
|
27442
|
-
missingPaths += 1;
|
|
27443
|
-
}
|
|
27550
|
+
attachCrawler() {
|
|
27551
|
+
let file;
|
|
27552
|
+
try {
|
|
27553
|
+
const { dev, ino } = statSync3(this.options.crawlerDbPath);
|
|
27554
|
+
file = { dev, ino };
|
|
27555
|
+
} catch {
|
|
27556
|
+
this.detachCrawler();
|
|
27557
|
+
return false;
|
|
27444
27558
|
}
|
|
27445
|
-
if (
|
|
27446
|
-
|
|
27559
|
+
if (this.attached !== undefined) {
|
|
27560
|
+
if (this.attached.dev === file.dev && this.attached.ino === file.ino)
|
|
27561
|
+
return true;
|
|
27562
|
+
this.detachCrawler();
|
|
27447
27563
|
}
|
|
27448
|
-
const
|
|
27449
|
-
|
|
27450
|
-
|
|
27451
|
-
|
|
27452
|
-
|
|
27453
|
-
let skippedLarge = 0;
|
|
27454
|
-
for (const file of files.sort()) {
|
|
27455
|
-
if (documents.length >= maxDocuments)
|
|
27456
|
-
break;
|
|
27457
|
-
let bytes;
|
|
27458
|
-
try {
|
|
27459
|
-
const info = await stat6(file);
|
|
27460
|
-
if (info.size > maxBytes) {
|
|
27461
|
-
skippedLarge += 1;
|
|
27462
|
-
continue;
|
|
27463
|
-
}
|
|
27464
|
-
bytes = await readFile5(file);
|
|
27465
|
-
} catch {
|
|
27466
|
-
parseFailures += 1;
|
|
27467
|
-
continue;
|
|
27468
|
-
}
|
|
27469
|
-
const stem = basename12(file, extname6(file));
|
|
27470
|
-
const mailboxDir = basename12(dirname24(file));
|
|
27471
|
-
const hierarchy = ["mailboxes", mailboxDir, stem];
|
|
27472
|
-
const extension = extname6(file).toLowerCase();
|
|
27473
|
-
const rawMessages = extension === ".mbox" ? splitMbox(bytes.toString("utf8")) : [bytes.toString("utf8")];
|
|
27474
|
-
for (const [index, raw] of rawMessages.entries()) {
|
|
27475
|
-
if (documents.length >= maxDocuments)
|
|
27476
|
-
break;
|
|
27477
|
-
try {
|
|
27478
|
-
const parsed = await simpleParser2(raw);
|
|
27479
|
-
const subject = parsed.subject ?? "(no subject)";
|
|
27480
|
-
const from = Array.isArray(parsed.from) ? parsed.from[0]?.text : parsed.from?.text;
|
|
27481
|
-
const to = Array.isArray(parsed.to) ? parsed.to[0]?.text : parsed.to?.text;
|
|
27482
|
-
const body = (parsed.text ?? "").trim().slice(0, MAX_BODY_CHARS);
|
|
27483
|
-
const headerBlock = [
|
|
27484
|
-
`Subject: ${subject}`,
|
|
27485
|
-
...from !== undefined ? [`From: ${from}`] : [],
|
|
27486
|
-
...to !== undefined ? [`To: ${to}`] : [],
|
|
27487
|
-
...parsed.date !== undefined ? [`Date: ${parsed.date.toISOString()}`] : []
|
|
27488
|
-
].join(`
|
|
27489
|
-
`);
|
|
27490
|
-
const content = `${headerBlock}
|
|
27491
|
-
|
|
27492
|
-
${body}`.trim();
|
|
27493
|
-
if (content.length === 0)
|
|
27494
|
-
continue;
|
|
27495
|
-
documents.push({
|
|
27496
|
-
docId: extension === ".mbox" ? `${stem}-${index + 1}` : stem,
|
|
27497
|
-
hierarchy,
|
|
27498
|
-
title: subject,
|
|
27499
|
-
content,
|
|
27500
|
-
...parsed.date !== undefined ? { publishedAt: parsed.date.getTime() } : {},
|
|
27501
|
-
metadata: {
|
|
27502
|
-
path: file,
|
|
27503
|
-
subject,
|
|
27504
|
-
...from !== undefined ? { from } : {},
|
|
27505
|
-
messageIndex: index
|
|
27506
|
-
}
|
|
27507
|
-
});
|
|
27508
|
-
} catch {
|
|
27509
|
-
parseFailures += 1;
|
|
27510
|
-
}
|
|
27511
|
-
}
|
|
27564
|
+
const crawlerUri = `${pathToFileURL2(this.options.crawlerDbPath).href}?mode=ro`.replaceAll("'", "''");
|
|
27565
|
+
try {
|
|
27566
|
+
this.db.exec(`attach database '${crawlerUri}' as crawler`);
|
|
27567
|
+
} catch {
|
|
27568
|
+
return false;
|
|
27512
27569
|
}
|
|
27513
|
-
|
|
27514
|
-
|
|
27515
|
-
|
|
27516
|
-
|
|
27517
|
-
|
|
27518
|
-
|
|
27519
|
-
|
|
27520
|
-
function splitMbox(text) {
|
|
27521
|
-
const lines = text.split(/\r?\n/u);
|
|
27522
|
-
const messages = [];
|
|
27523
|
-
let current = [];
|
|
27524
|
-
for (const line of lines) {
|
|
27525
|
-
if (MBOX_SEPARATOR.test(line) && line.startsWith("From ")) {
|
|
27526
|
-
if (current.length > 0)
|
|
27527
|
-
messages.push(current.join(`
|
|
27528
|
-
`));
|
|
27529
|
-
current = [];
|
|
27530
|
-
continue;
|
|
27570
|
+
const { descriptor } = this.options;
|
|
27571
|
+
try {
|
|
27572
|
+
this.db.prepare(`select ${this.select} from ${descriptor.fromClause} where ${this.where} limit 0`).all(...this.whereParams());
|
|
27573
|
+
this.db.prepare(`select 1 from ${descriptor.fts.table} limit 0`).all();
|
|
27574
|
+
} catch {
|
|
27575
|
+
this.db.exec("detach database crawler");
|
|
27576
|
+
return false;
|
|
27531
27577
|
}
|
|
27532
|
-
|
|
27578
|
+
this.attached = file;
|
|
27579
|
+
this.lastSemantic = undefined;
|
|
27580
|
+
return true;
|
|
27533
27581
|
}
|
|
27534
|
-
|
|
27535
|
-
|
|
27536
|
-
|
|
27537
|
-
|
|
27538
|
-
|
|
27539
|
-
|
|
27540
|
-
|
|
27541
|
-
|
|
27542
|
-
MBOX_SEPARATOR = /^From .*\d{4}$/mu;
|
|
27543
|
-
});
|
|
27544
|
-
|
|
27545
|
-
// src/datasource/skills/mail-export/skill.ts
|
|
27546
|
-
var MAIL_EXPORT_SKILL_DEFINITION, MailExportSkill;
|
|
27547
|
-
var init_skill8 = __esm(() => {
|
|
27548
|
-
init_connector_skill();
|
|
27549
|
-
init_connector4();
|
|
27550
|
-
MAIL_EXPORT_SKILL_DEFINITION = {
|
|
27551
|
-
skillName: "mail-export",
|
|
27552
|
-
skillType: "mail-export",
|
|
27553
|
-
description: "Local mail export datasource",
|
|
27554
|
-
capabilities: ["email", "filesystem"],
|
|
27555
|
-
defaultTags: ["mail-export", "email", "pii"],
|
|
27556
|
-
contentType: "email",
|
|
27557
|
-
manifestDescription: "Search indexed local mail exports (mbox/eml archives). Use for questions about archived email conversations.",
|
|
27558
|
-
manifestNotes: [
|
|
27559
|
-
"Only the server-configured export paths are indexed; archives are read locally and never uploaded."
|
|
27560
|
-
]
|
|
27561
|
-
};
|
|
27562
|
-
MailExportSkill = class MailExportSkill extends ConnectorDatasourceSkill {
|
|
27563
|
-
constructor(options = {}) {
|
|
27564
|
-
const { connector, connectorOptions, ...rest } = options;
|
|
27565
|
-
super(MAIL_EXPORT_SKILL_DEFINITION, {
|
|
27566
|
-
...rest,
|
|
27567
|
-
connector: connector ?? new MailExportConnector(connectorOptions ?? {})
|
|
27568
|
-
});
|
|
27569
|
-
}
|
|
27570
|
-
};
|
|
27571
|
-
});
|
|
27572
|
-
|
|
27573
|
-
// src/datasource/skills/mail-export/index.ts
|
|
27574
|
-
var init_mail_export = __esm(() => {
|
|
27575
|
-
init_connector4();
|
|
27576
|
-
init_skill8();
|
|
27577
|
-
});
|
|
27578
|
-
|
|
27579
|
-
// src/datasource/skills/mailcrawl/config.ts
|
|
27580
|
-
import { chmodSync, mkdirSync as mkdirSync22 } from "node:fs";
|
|
27581
|
-
function validateMailcrawlInstanceId(instanceId) {
|
|
27582
|
-
if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/u.test(instanceId)) {
|
|
27583
|
-
throw new Error("mailcrawl instanceId must be a safe single path segment");
|
|
27582
|
+
detachCrawler() {
|
|
27583
|
+
if (this.attached === undefined)
|
|
27584
|
+
return;
|
|
27585
|
+
this.attached = undefined;
|
|
27586
|
+
this.lastSemantic = undefined;
|
|
27587
|
+
try {
|
|
27588
|
+
this.opened?.exec("detach database crawler");
|
|
27589
|
+
} catch {}
|
|
27584
27590
|
}
|
|
27585
|
-
|
|
27586
|
-
|
|
27587
|
-
mkdirSync22(path, { recursive: true, mode: 448 });
|
|
27588
|
-
if (process.platform !== "win32")
|
|
27589
|
-
chmodSync(path, 448);
|
|
27590
|
-
}
|
|
27591
|
-
var init_config3 = () => {};
|
|
27592
|
-
|
|
27593
|
-
// src/datasource/skills/mailcrawl/client.ts
|
|
27594
|
-
import { spawn as spawn15 } from "node:child_process";
|
|
27595
|
-
|
|
27596
|
-
class MailcrawlClient {
|
|
27597
|
-
options;
|
|
27598
|
-
constructor(options = {}) {
|
|
27599
|
-
this.options = options;
|
|
27600
|
-
if (options.instanceId !== undefined)
|
|
27601
|
-
validateMailcrawlInstanceId(options.instanceId);
|
|
27591
|
+
get semanticEnabled() {
|
|
27592
|
+
return this.options.embedder !== undefined;
|
|
27602
27593
|
}
|
|
27603
|
-
|
|
27604
|
-
|
|
27605
|
-
|
|
27606
|
-
|
|
27607
|
-
|
|
27608
|
-
...this.options.mailbox === undefined ? [] : ["--mailbox", this.options.mailbox],
|
|
27609
|
-
...this.options.backend === undefined ? [] : ["--backend", this.options.backend],
|
|
27610
|
-
...this.options.source === undefined ? [] : ["--source", this.options.source],
|
|
27611
|
-
...this.options.fixture === undefined ? [] : ["--fixture", this.options.fixture],
|
|
27612
|
-
...this.options.himalayaConfig === undefined ? [] : ["--himalaya-config", this.options.himalayaConfig]
|
|
27613
|
-
];
|
|
27614
|
-
const result = await this.run(args, signal, this.indexTimeoutMs());
|
|
27615
|
-
if (!result.ok)
|
|
27616
|
-
return failure3(result);
|
|
27617
|
-
const parsed = parseObject2(result.stdout);
|
|
27618
|
-
if (parsed === undefined || !hasAnyNumber(parsed, ["added", "updated", "deleted", "unchanged", "chunksAdded"])) {
|
|
27619
|
-
return failure3({ ...result, ok: false, reason: "invalid-output" });
|
|
27620
|
-
}
|
|
27621
|
-
const messages = number(parsed, "added") ?? number(parsed, "updated") ?? number(parsed, "unchanged") ?? 0;
|
|
27622
|
-
return ok4({
|
|
27623
|
-
messages,
|
|
27624
|
-
added: number(parsed, "added"),
|
|
27625
|
-
updated: number(parsed, "updated"),
|
|
27626
|
-
deleted: number(parsed, "deleted"),
|
|
27627
|
-
unchanged: number(parsed, "unchanged"),
|
|
27628
|
-
chunksAdded: number(parsed, "chunksAdded"),
|
|
27629
|
-
archiveRevision: string(parsed, "archiveRevision")
|
|
27630
|
-
}, result);
|
|
27594
|
+
close() {
|
|
27595
|
+
this.opened?.close();
|
|
27596
|
+
this.opened = undefined;
|
|
27597
|
+
this.attached = undefined;
|
|
27598
|
+
this.lastSemantic = undefined;
|
|
27631
27599
|
}
|
|
27632
|
-
|
|
27633
|
-
|
|
27634
|
-
|
|
27635
|
-
|
|
27636
|
-
|
|
27637
|
-
|
|
27638
|
-
mode === "keyword" ? "bm25" : mode,
|
|
27639
|
-
...this.options.account === undefined ? [] : ["--account", this.options.account],
|
|
27640
|
-
...this.options.mailbox === undefined ? [] : ["--mailbox", this.options.mailbox],
|
|
27641
|
-
"--limit",
|
|
27642
|
-
String(options?.topK ?? 20),
|
|
27643
|
-
"--json",
|
|
27644
|
-
query.trim()
|
|
27600
|
+
whereParams() {
|
|
27601
|
+
const { descriptor, workspaceId, channelIds = [], channelNames = [] } = this.options;
|
|
27602
|
+
return [
|
|
27603
|
+
...descriptor.scopeExpression !== undefined && workspaceId !== undefined ? [workspaceId] : [],
|
|
27604
|
+
...channelIds,
|
|
27605
|
+
...channelNames
|
|
27645
27606
|
];
|
|
27646
|
-
const result = await this.run(args, options?.signal, this.options.timeoutMs ?? DEFAULT_SEARCH_TIMEOUT_MS2);
|
|
27647
|
-
if (!result.ok)
|
|
27648
|
-
return failure3(result);
|
|
27649
|
-
const parsed = parseJson3(result.stdout);
|
|
27650
|
-
if (parsed === undefined)
|
|
27651
|
-
return failure3({ ...result, ok: false, reason: "invalid-output" });
|
|
27652
|
-
const rows = Array.isArray(parsed) ? parsed : objectArray(parsed, ["hits", "results", "items"]);
|
|
27653
|
-
if (rows === undefined)
|
|
27654
|
-
return failure3({ ...result, ok: false, reason: "invalid-output" });
|
|
27655
|
-
const hits = rows.map((row, index) => normalizeHit2(row, mode, index));
|
|
27656
|
-
if (hits.some((hit) => hit === undefined)) {
|
|
27657
|
-
return failure3({ ...result, ok: false, reason: "invalid-output" });
|
|
27658
|
-
}
|
|
27659
|
-
return {
|
|
27660
|
-
ok: true,
|
|
27661
|
-
hits,
|
|
27662
|
-
stdout: result.stdout,
|
|
27663
|
-
stderr: result.stderr,
|
|
27664
|
-
code: result.code ?? 0
|
|
27665
|
-
};
|
|
27666
27607
|
}
|
|
27667
|
-
|
|
27668
|
-
|
|
27608
|
+
searchLexical(query, topK) {
|
|
27609
|
+
const match = toMatchQuery(query);
|
|
27610
|
+
if (match === undefined || topK <= 0 || !this.attachCrawler())
|
|
27611
|
+
return [];
|
|
27612
|
+
const { descriptor } = this.options;
|
|
27613
|
+
const rows = this.db.prepare(`select ${this.select}, bm25(${descriptor.fts.table}) as rank
|
|
27614
|
+
from ${descriptor.fromClause} join ${descriptor.fts.table} on ${descriptor.fts.joinOn}
|
|
27615
|
+
where ${descriptor.fts.table} match ? and ${this.where}
|
|
27616
|
+
order by rank limit ?`).all(match, ...this.whereParams(), topK);
|
|
27617
|
+
return rows.map((row) => this.toHit(this.toRow(row), -Number(row.rank)));
|
|
27669
27618
|
}
|
|
27670
|
-
async
|
|
27671
|
-
const
|
|
27672
|
-
if (
|
|
27673
|
-
return
|
|
27619
|
+
async searchSemantic(query, topK) {
|
|
27620
|
+
const trimmed = query.trim();
|
|
27621
|
+
if (this.options.embedder === undefined || trimmed.length === 0 || topK <= 0 || !this.attachCrawler())
|
|
27622
|
+
return [];
|
|
27623
|
+
const ranked = await this.rankSemantic(trimmed, topK * POOL_FACTOR);
|
|
27624
|
+
if (ranked.length === 0)
|
|
27625
|
+
return [];
|
|
27626
|
+
const live = this.hydrate(ranked.map((entry) => entry.key));
|
|
27627
|
+
const hits = [];
|
|
27628
|
+
for (const entry of ranked) {
|
|
27629
|
+
const row = live.get(entry.key);
|
|
27630
|
+
if (row !== undefined)
|
|
27631
|
+
hits.push(this.toHit(row, entry.score));
|
|
27632
|
+
if (hits.length >= topK)
|
|
27633
|
+
break;
|
|
27674
27634
|
}
|
|
27675
|
-
|
|
27676
|
-
|
|
27677
|
-
|
|
27678
|
-
const
|
|
27679
|
-
|
|
27680
|
-
|
|
27681
|
-
|
|
27635
|
+
return hits;
|
|
27636
|
+
}
|
|
27637
|
+
async rankSemantic(query, pool) {
|
|
27638
|
+
const now = Date.now();
|
|
27639
|
+
let cached = this.lastSemantic;
|
|
27640
|
+
if (cached === undefined || cached.query !== query || now - cached.at > SEMANTIC_REUSE_MS) {
|
|
27641
|
+
const embedder = this.options.embedder;
|
|
27642
|
+
if (embedder === undefined)
|
|
27643
|
+
return [];
|
|
27644
|
+
cached = {
|
|
27645
|
+
query,
|
|
27646
|
+
at: now,
|
|
27647
|
+
vector: embedder.embed([query]).then((vectors) => vectors[0], () => {
|
|
27648
|
+
return;
|
|
27649
|
+
}),
|
|
27650
|
+
ranked: undefined
|
|
27651
|
+
};
|
|
27652
|
+
this.lastSemantic = cached;
|
|
27682
27653
|
}
|
|
27683
|
-
|
|
27684
|
-
|
|
27685
|
-
|
|
27686
|
-
|
|
27687
|
-
|
|
27654
|
+
const vector = await cached.vector;
|
|
27655
|
+
if (vector === undefined) {
|
|
27656
|
+
if (this.lastSemantic === cached)
|
|
27657
|
+
this.lastSemantic = undefined;
|
|
27658
|
+
return [];
|
|
27688
27659
|
}
|
|
27689
|
-
if (
|
|
27690
|
-
|
|
27691
|
-
|
|
27660
|
+
if (cached.ranked === undefined || cached.ranked.pool < pool) {
|
|
27661
|
+
const wide = pool * POOL_FACTOR;
|
|
27662
|
+
cached.ranked = { pool: wide, keys: topKByCosine(vector, this.storedVectors(), wide) };
|
|
27663
|
+
}
|
|
27664
|
+
return cached.ranked.keys.slice(0, pool);
|
|
27692
27665
|
}
|
|
27693
|
-
async
|
|
27694
|
-
|
|
27695
|
-
|
|
27696
|
-
|
|
27697
|
-
const
|
|
27698
|
-
|
|
27699
|
-
|
|
27666
|
+
async searchHybrid(query, topK) {
|
|
27667
|
+
if (topK <= 0)
|
|
27668
|
+
return [];
|
|
27669
|
+
const pool = topK * POOL_FACTOR;
|
|
27670
|
+
const lexical = this.searchLexical(query, pool);
|
|
27671
|
+
const semantic = await this.searchSemantic(query, pool);
|
|
27672
|
+
if (semantic.length === 0)
|
|
27673
|
+
return lexical.slice(0, topK);
|
|
27674
|
+
const byKey = new Map;
|
|
27675
|
+
for (const hit of [...semantic, ...lexical])
|
|
27676
|
+
byKey.set(hit.key, hit);
|
|
27677
|
+
const fused = [];
|
|
27678
|
+
for (const entry of reciprocalRankFusion([lexical.map((hit) => hit.key), semantic.map((hit) => hit.key)])) {
|
|
27679
|
+
const hit = byKey.get(entry.key);
|
|
27680
|
+
if (hit !== undefined)
|
|
27681
|
+
fused.push({ ...hit, score: entry.score });
|
|
27682
|
+
if (fused.length >= topK)
|
|
27683
|
+
break;
|
|
27700
27684
|
}
|
|
27701
|
-
return
|
|
27702
|
-
embedded: number(parsed, "embedded"),
|
|
27703
|
-
reused: number(parsed, "reused"),
|
|
27704
|
-
generation: string(parsed, "generation"),
|
|
27705
|
-
archiveRevision: string(parsed, "archiveRevision"),
|
|
27706
|
-
rebuilt: typeof parsed.rebuilt === "boolean" ? parsed.rebuilt : undefined,
|
|
27707
|
-
embedder: string(parsed, "embedder")
|
|
27708
|
-
}, result);
|
|
27685
|
+
return fused;
|
|
27709
27686
|
}
|
|
27710
|
-
|
|
27711
|
-
|
|
27712
|
-
|
|
27713
|
-
|
|
27714
|
-
|
|
27715
|
-
|
|
27716
|
-
|
|
27717
|
-
|
|
27718
|
-
|
|
27719
|
-
|
|
27720
|
-
|
|
27721
|
-
|
|
27722
|
-
|
|
27723
|
-
|
|
27724
|
-
|
|
27725
|
-
const
|
|
27726
|
-
|
|
27727
|
-
|
|
27728
|
-
|
|
27729
|
-
|
|
27730
|
-
|
|
27731
|
-
|
|
27732
|
-
child.stdout.setEncoding("utf8");
|
|
27733
|
-
child.stderr.setEncoding("utf8");
|
|
27734
|
-
child.stdout.on("data", (chunk) => {
|
|
27735
|
-
stdout = appendOutput(stdout, chunk, max);
|
|
27736
|
-
if (stdout.exceeded && reason === undefined) {
|
|
27737
|
-
reason = "stdout-too-large";
|
|
27738
|
-
terminate4(child);
|
|
27687
|
+
async sync() {
|
|
27688
|
+
const { embedder } = this.options;
|
|
27689
|
+
if (embedder === undefined)
|
|
27690
|
+
return { ok: false, message: "no embedder configured" };
|
|
27691
|
+
if (!this.attachCrawler())
|
|
27692
|
+
return { ok: false, message: "crawler archive not available yet" };
|
|
27693
|
+
let identity;
|
|
27694
|
+
try {
|
|
27695
|
+
identity = await embedder.identity();
|
|
27696
|
+
} catch (error) {
|
|
27697
|
+
return { ok: false, message: errorMessage2(error) };
|
|
27698
|
+
}
|
|
27699
|
+
this.adoptIdentity(identity);
|
|
27700
|
+
const pruned = this.prune();
|
|
27701
|
+
const pending = this.pendingKeys();
|
|
27702
|
+
const batchSize = Math.max(1, this.options.batchSize ?? DEFAULT_BATCH_SIZE);
|
|
27703
|
+
const budget = Math.max(1, this.options.maxEmbedPerSync ?? DEFAULT_MAX_EMBED_PER_SYNC);
|
|
27704
|
+
const target = Math.min(pending.length, budget);
|
|
27705
|
+
const embed = async (rows) => {
|
|
27706
|
+
const vectors = await embedder.embed(rows.map((row) => row.input));
|
|
27707
|
+
if (vectors.length !== rows.length) {
|
|
27708
|
+
throw new Error(`embedder returned ${vectors.length} vectors for ${rows.length} messages`);
|
|
27739
27709
|
}
|
|
27740
|
-
|
|
27741
|
-
|
|
27742
|
-
|
|
27743
|
-
|
|
27744
|
-
|
|
27745
|
-
|
|
27710
|
+
const wrong = vectors.find((vector) => vector.length !== identity.dimension);
|
|
27711
|
+
if (wrong !== undefined) {
|
|
27712
|
+
throw new Error(`embedder returned a ${wrong.length}-dimension vector, expected ${identity.dimension}`);
|
|
27713
|
+
}
|
|
27714
|
+
return vectors;
|
|
27715
|
+
};
|
|
27716
|
+
let embedded = 0;
|
|
27717
|
+
let skipped = 0;
|
|
27718
|
+
for (let offset = 0;offset < target; offset += batchSize) {
|
|
27719
|
+
const rows = [...this.hydrate(pending.slice(offset, Math.min(offset + batchSize, target))).values()];
|
|
27720
|
+
if (rows.length === 0)
|
|
27721
|
+
continue;
|
|
27722
|
+
try {
|
|
27723
|
+
this.store(rows, await embed(rows));
|
|
27724
|
+
embedded += rows.length;
|
|
27725
|
+
} catch (batchError) {
|
|
27726
|
+
const rejected = [];
|
|
27727
|
+
let lastError = batchError;
|
|
27728
|
+
for (const row of rows) {
|
|
27729
|
+
try {
|
|
27730
|
+
this.store([row], await embed([row]));
|
|
27731
|
+
embedded += 1;
|
|
27732
|
+
} catch (error) {
|
|
27733
|
+
rejected.push(row);
|
|
27734
|
+
lastError = error;
|
|
27735
|
+
}
|
|
27736
|
+
}
|
|
27737
|
+
if (rejected.length === 0)
|
|
27738
|
+
continue;
|
|
27739
|
+
if (embedded === 0)
|
|
27740
|
+
return { ok: false, message: errorMessage2(lastError) };
|
|
27741
|
+
this.store(rejected, undefined);
|
|
27742
|
+
skipped += rejected.length;
|
|
27746
27743
|
}
|
|
27747
|
-
});
|
|
27748
|
-
child.on("error", (error) => {
|
|
27749
|
-
if (settled)
|
|
27750
|
-
return;
|
|
27751
|
-
settled = true;
|
|
27752
|
-
clearTimeout(timer);
|
|
27753
|
-
signal?.removeEventListener("abort", abort);
|
|
27754
|
-
resolveResult({
|
|
27755
|
-
ok: false,
|
|
27756
|
-
reason: error.code === "ENOENT" ? "binary-missing" : "spawn-error",
|
|
27757
|
-
stdout: stdout.text,
|
|
27758
|
-
stderr: "",
|
|
27759
|
-
code: null
|
|
27760
|
-
});
|
|
27761
|
-
});
|
|
27762
|
-
child.on("close", (code) => {
|
|
27763
|
-
if (settled)
|
|
27764
|
-
return;
|
|
27765
|
-
settled = true;
|
|
27766
|
-
clearTimeout(timer);
|
|
27767
|
-
signal?.removeEventListener("abort", abort);
|
|
27768
|
-
resolveResult({
|
|
27769
|
-
ok: code === 0 && reason === undefined,
|
|
27770
|
-
...reason === undefined ? code === 0 ? {} : { reason: "nonzero-exit" } : { reason },
|
|
27771
|
-
stdout: stdout.text,
|
|
27772
|
-
stderr: stderr.text,
|
|
27773
|
-
code
|
|
27774
|
-
});
|
|
27775
|
-
});
|
|
27776
|
-
});
|
|
27777
|
-
}
|
|
27778
|
-
function terminate4(child) {
|
|
27779
|
-
if (child.killed)
|
|
27780
|
-
return;
|
|
27781
|
-
if (process.platform !== "win32" && child.pid !== undefined) {
|
|
27782
|
-
try {
|
|
27783
|
-
process.kill(-child.pid, "SIGTERM");
|
|
27784
|
-
return;
|
|
27785
|
-
} catch {}
|
|
27786
|
-
}
|
|
27787
|
-
child.kill("SIGTERM");
|
|
27788
|
-
}
|
|
27789
|
-
function parseJson3(text) {
|
|
27790
|
-
try {
|
|
27791
|
-
return JSON.parse(text.trim());
|
|
27792
|
-
} catch {
|
|
27793
|
-
return;
|
|
27794
|
-
}
|
|
27795
|
-
}
|
|
27796
|
-
function parseObject2(text) {
|
|
27797
|
-
const value = parseJson3(text);
|
|
27798
|
-
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : undefined;
|
|
27799
|
-
}
|
|
27800
|
-
function objectArray(value, keys) {
|
|
27801
|
-
if (value === null || typeof value !== "object" || Array.isArray(value))
|
|
27802
|
-
return;
|
|
27803
|
-
for (const key of keys)
|
|
27804
|
-
if (Array.isArray(value[key]))
|
|
27805
|
-
return value[key];
|
|
27806
|
-
return;
|
|
27807
|
-
}
|
|
27808
|
-
function hasAnyNumber(value, keys) {
|
|
27809
|
-
return keys.some((key) => number(value, key) !== undefined);
|
|
27810
|
-
}
|
|
27811
|
-
function normalizeHit2(value, mode, index) {
|
|
27812
|
-
if (value === null || typeof value !== "object" || Array.isArray(value))
|
|
27813
|
-
return;
|
|
27814
|
-
const row = value;
|
|
27815
|
-
const text = (key) => typeof row[key] === "string" ? row[key] : "";
|
|
27816
|
-
const chunkId = text("chunkId") || text("chunk_id");
|
|
27817
|
-
const messageId = text("messageId") || text("message_id");
|
|
27818
|
-
const accountId = text("accountId") || text("account_id");
|
|
27819
|
-
const mailbox = text("mailbox");
|
|
27820
|
-
const snippet = text("snippet") || text("content") || text("text");
|
|
27821
|
-
if (!isSafeOpaqueId(chunkId) || !isSafeOpaqueId(messageId) || !snippet)
|
|
27822
|
-
return;
|
|
27823
|
-
const to = Array.isArray(row.to) ? row.to.filter((item) => typeof item === "string") : [];
|
|
27824
|
-
return {
|
|
27825
|
-
chunkId,
|
|
27826
|
-
messageId,
|
|
27827
|
-
threadId: text("threadId") || text("thread_id"),
|
|
27828
|
-
accountId,
|
|
27829
|
-
mailbox,
|
|
27830
|
-
subject: text("subject"),
|
|
27831
|
-
from: text("from"),
|
|
27832
|
-
to,
|
|
27833
|
-
date: text("date"),
|
|
27834
|
-
snippet,
|
|
27835
|
-
score: typeof row.score === "number" ? row.score : 1 / (index + 1),
|
|
27836
|
-
mode
|
|
27837
|
-
};
|
|
27838
|
-
}
|
|
27839
|
-
function number(value, key) {
|
|
27840
|
-
return typeof value[key] === "number" && Number.isFinite(value[key]) ? value[key] : undefined;
|
|
27841
|
-
}
|
|
27842
|
-
function string(value, key) {
|
|
27843
|
-
return typeof value[key] === "string" ? value[key] : undefined;
|
|
27844
|
-
}
|
|
27845
|
-
function ok4(data, result) {
|
|
27846
|
-
return { ok: true, data, stdout: result.stdout, stderr: result.stderr, code: result.code ?? 0 };
|
|
27847
|
-
}
|
|
27848
|
-
function failure3(result) {
|
|
27849
|
-
return {
|
|
27850
|
-
ok: false,
|
|
27851
|
-
reason: result.reason ?? "nonzero-exit",
|
|
27852
|
-
stdout: result.stdout,
|
|
27853
|
-
stderr: boundDiagnosticText6(result.stderr),
|
|
27854
|
-
code: result.code
|
|
27855
|
-
};
|
|
27856
|
-
}
|
|
27857
|
-
function boundDiagnosticText6(value) {
|
|
27858
|
-
if (value.length === 0)
|
|
27859
|
-
return "";
|
|
27860
|
-
return value.trim().slice(0, 4000);
|
|
27861
|
-
}
|
|
27862
|
-
function appendOutput(current, chunk, maxBytes) {
|
|
27863
|
-
if (current.exceeded)
|
|
27864
|
-
return current;
|
|
27865
|
-
const bytes = Buffer.from(chunk, "utf8");
|
|
27866
|
-
const remaining = Math.max(0, maxBytes - current.bytes);
|
|
27867
|
-
const prefix = truncateUtf8(chunk, remaining);
|
|
27868
|
-
const total = current.bytes + bytes.byteLength;
|
|
27869
|
-
return {
|
|
27870
|
-
text: current.text + prefix,
|
|
27871
|
-
bytes: Math.min(total, maxBytes),
|
|
27872
|
-
exceeded: total > maxBytes
|
|
27873
|
-
};
|
|
27874
|
-
}
|
|
27875
|
-
function isSafeOpaqueId(value) {
|
|
27876
|
-
return value.length > 0 && value !== "." && value !== ".." && !/[\\/#\u0000-\u001f\u007f]/u.test(value);
|
|
27877
|
-
}
|
|
27878
|
-
function findRemoteEmbeddingViolation(env) {
|
|
27879
|
-
for (const [key, value] of Object.entries(env)) {
|
|
27880
|
-
if (value === undefined)
|
|
27881
|
-
continue;
|
|
27882
|
-
const lower = key.toLowerCase();
|
|
27883
|
-
if (REMOTE_EMBEDDING_ENV_KEYS.has(lower))
|
|
27884
|
-
return key;
|
|
27885
|
-
if (lower === "mailcrawl_embedder" || lower === "mailcrawl_embed_url") {
|
|
27886
|
-
if (isRemoteHttpEndpoint(value))
|
|
27887
|
-
return key;
|
|
27888
27744
|
}
|
|
27745
|
+
return { ok: true, embedded, skipped, pruned, remaining: pending.length - embedded - skipped };
|
|
27889
27746
|
}
|
|
27890
|
-
|
|
27891
|
-
|
|
27892
|
-
|
|
27893
|
-
|
|
27894
|
-
|
|
27895
|
-
|
|
27896
|
-
|
|
27897
|
-
|
|
27898
|
-
|
|
27899
|
-
|
|
27747
|
+
adoptIdentity(identity) {
|
|
27748
|
+
const serialized = JSON.stringify(identity);
|
|
27749
|
+
const current = this.db.prepare("select v from sidecar_meta where k = ?").get(IDENTITY_KEY);
|
|
27750
|
+
if (current?.v === serialized)
|
|
27751
|
+
return;
|
|
27752
|
+
this.transaction(() => {
|
|
27753
|
+
this.db.exec("delete from sidecar_vec");
|
|
27754
|
+
this.db.prepare("insert or replace into sidecar_meta (k, v) values (?, ?)").run(IDENTITY_KEY, serialized);
|
|
27755
|
+
});
|
|
27756
|
+
this.lastSemantic = undefined;
|
|
27900
27757
|
}
|
|
27901
|
-
|
|
27902
|
-
|
|
27903
|
-
|
|
27904
|
-
|
|
27905
|
-
|
|
27906
|
-
|
|
27907
|
-
|
|
27758
|
+
prune() {
|
|
27759
|
+
const { descriptor } = this.options;
|
|
27760
|
+
const result = this.db.prepare(`delete from sidecar_vec where message_key not in (
|
|
27761
|
+
select ${descriptor.keyExpression} from ${descriptor.fromClause} where ${this.where}
|
|
27762
|
+
)`).run(...this.whereParams());
|
|
27763
|
+
if (Number(result.changes) > 0)
|
|
27764
|
+
this.lastSemantic = undefined;
|
|
27765
|
+
return Number(result.changes);
|
|
27766
|
+
}
|
|
27767
|
+
pendingKeys() {
|
|
27768
|
+
const { descriptor } = this.options;
|
|
27769
|
+
const rows = this.db.prepare(`select ${this.select}, v.hash as stored_hash
|
|
27770
|
+
from ${descriptor.fromClause}
|
|
27771
|
+
left join sidecar_vec v on v.message_key = ${descriptor.keyExpression}
|
|
27772
|
+
where ${this.where}`).iterate(...this.whereParams());
|
|
27773
|
+
const pending = [];
|
|
27774
|
+
for (const row of rows) {
|
|
27775
|
+
if (row.stored_hash !== this.toRow(row).hash)
|
|
27776
|
+
pending.push(String(row.k));
|
|
27777
|
+
}
|
|
27778
|
+
return pending;
|
|
27779
|
+
}
|
|
27780
|
+
hydrate(keys) {
|
|
27781
|
+
const { descriptor } = this.options;
|
|
27782
|
+
const out = new Map;
|
|
27783
|
+
if (keys.length === 0)
|
|
27784
|
+
return out;
|
|
27785
|
+
const rows = this.db.prepare(`select ${this.select} from ${descriptor.fromClause}
|
|
27786
|
+
where ${descriptor.keyExpression} in (${keys.map(() => "?").join(", ")}) and ${this.where}`).all(...keys, ...this.whereParams());
|
|
27787
|
+
const byKey = new Map(rows.map((row) => [String(row.k), this.toRow(row)]));
|
|
27788
|
+
for (const key of keys) {
|
|
27789
|
+
const row = byKey.get(key);
|
|
27790
|
+
if (row !== undefined)
|
|
27791
|
+
out.set(key, row);
|
|
27792
|
+
}
|
|
27793
|
+
return out;
|
|
27794
|
+
}
|
|
27795
|
+
store(rows, vectors) {
|
|
27796
|
+
const upsert = this.db.prepare("insert or replace into sidecar_vec (message_key, hash, vec) values (?, ?, ?)");
|
|
27797
|
+
this.transaction(() => {
|
|
27798
|
+
for (const [index, row] of rows.entries()) {
|
|
27799
|
+
upsert.run(row.key, row.hash, encodeVector(vectors?.[index] ?? []));
|
|
27800
|
+
}
|
|
27801
|
+
});
|
|
27802
|
+
this.lastSemantic = undefined;
|
|
27803
|
+
}
|
|
27804
|
+
*storedVectors() {
|
|
27805
|
+
const rows = this.db.prepare("select message_key, vec from sidecar_vec where length(vec) > 0").iterate();
|
|
27806
|
+
for (const row of rows) {
|
|
27807
|
+
const vector = row.vec instanceof Uint8Array ? decodeVector(row.vec) : undefined;
|
|
27808
|
+
if (vector !== undefined)
|
|
27809
|
+
yield { key: String(row.message_key), vector };
|
|
27810
|
+
}
|
|
27811
|
+
}
|
|
27812
|
+
transaction(work) {
|
|
27813
|
+
this.db.exec("begin immediate");
|
|
27908
27814
|
try {
|
|
27909
|
-
|
|
27910
|
-
|
|
27815
|
+
work();
|
|
27816
|
+
this.db.exec("commit");
|
|
27817
|
+
} catch (error) {
|
|
27818
|
+
this.db.exec("rollback");
|
|
27819
|
+
throw error;
|
|
27820
|
+
}
|
|
27821
|
+
}
|
|
27822
|
+
toRow(row) {
|
|
27823
|
+
const metadata = {};
|
|
27824
|
+
for (const [index, column] of this.options.descriptor.metadata.entries()) {
|
|
27825
|
+
const value = row[`m${index}`];
|
|
27826
|
+
if (typeof value === "string" && value.length > 0 || typeof value === "number")
|
|
27827
|
+
metadata[column.key] = String(value);
|
|
27828
|
+
}
|
|
27829
|
+
const title = typeof row.title === "string" && row.title.length > 0 ? row.title : undefined;
|
|
27830
|
+
const epoch = typeof row.epoch === "number" && Number.isFinite(row.epoch) ? row.epoch : undefined;
|
|
27831
|
+
const content = String(row.content ?? "");
|
|
27832
|
+
const input = `${title ?? ""}
|
|
27833
|
+
${content}`.slice(0, MAX_EMBED_INPUT_CHARS);
|
|
27834
|
+
return {
|
|
27835
|
+
key: String(row.k),
|
|
27836
|
+
content,
|
|
27837
|
+
title,
|
|
27838
|
+
epoch,
|
|
27839
|
+
metadata,
|
|
27840
|
+
input,
|
|
27841
|
+
hash: createHash14("sha1").update(input).digest("hex")
|
|
27842
|
+
};
|
|
27843
|
+
}
|
|
27844
|
+
toHit(row, score) {
|
|
27845
|
+
return {
|
|
27846
|
+
key: row.key,
|
|
27847
|
+
content: row.content,
|
|
27848
|
+
score,
|
|
27849
|
+
...row.title !== undefined ? { title: row.title } : {},
|
|
27850
|
+
...row.epoch !== undefined ? { publishedAt: Math.round(row.epoch * 1000) } : {},
|
|
27851
|
+
metadata: row.metadata
|
|
27852
|
+
};
|
|
27911
27853
|
}
|
|
27912
|
-
return "";
|
|
27913
27854
|
}
|
|
27914
|
-
|
|
27915
|
-
|
|
27916
|
-
|
|
27917
|
-
|
|
27918
|
-
|
|
27919
|
-
|
|
27920
|
-
|
|
27921
|
-
|
|
27922
|
-
|
|
27923
|
-
"MAILCRAWL_EMBEDDER_PROVIDER",
|
|
27924
|
-
"MAILCRAWL_EMBED_URL",
|
|
27925
|
-
"MAILCRAWL_EMBED_MODEL",
|
|
27926
|
-
"MAILCRAWL_EMBED_DIM",
|
|
27927
|
-
"MAILCRAWL_EMBED_TIMEOUT",
|
|
27928
|
-
"MAILCRAWL_EMBED_BATCH_SIZE",
|
|
27929
|
-
"MAILCRAWL_QUERY_PREFIX",
|
|
27930
|
-
"MAILCRAWL_PASSAGE_PREFIX",
|
|
27931
|
-
"MAILCRAWL_NATIVE_MODEL",
|
|
27932
|
-
"MAILCRAWL_NATIVE_DEVICE",
|
|
27933
|
-
"MAILCRAWL_NATIVE_DTYPE",
|
|
27934
|
-
"MAILCRAWL_KIWI_MODEL",
|
|
27935
|
-
"MAILCRAWL_JA_HELPER",
|
|
27936
|
-
"MAILCRAWL_ZH_HELPER"
|
|
27937
|
-
]);
|
|
27938
|
-
REMOTE_EMBEDDING_ENV_KEYS = new Set([
|
|
27939
|
-
"mailcrawl_embedder_base_url",
|
|
27940
|
-
"mailcrawl_embedding_base_url",
|
|
27941
|
-
"mailcrawl_allow_remote_embeddings"
|
|
27942
|
-
]);
|
|
27943
|
-
});
|
|
27855
|
+
function toMatchQuery(text) {
|
|
27856
|
+
const terms = [...new Set(text.match(/[\p{L}\p{N}_]+/gu) ?? [])];
|
|
27857
|
+
return terms.length === 0 ? undefined : terms.map((term) => `"${term}"`).join(" OR ");
|
|
27858
|
+
}
|
|
27859
|
+
function errorMessage2(error) {
|
|
27860
|
+
return error instanceof Error ? error.message : String(error);
|
|
27861
|
+
}
|
|
27862
|
+
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";
|
|
27863
|
+
var init_sidecar = () => {};
|
|
27944
27864
|
|
|
27945
|
-
// src/datasource/
|
|
27946
|
-
|
|
27947
|
-
|
|
27948
|
-
|
|
27949
|
-
|
|
27950
|
-
|
|
27951
|
-
|
|
27952
|
-
|
|
27953
|
-
|
|
27954
|
-
this.client = client;
|
|
27955
|
-
this.mode = mode;
|
|
27956
|
-
this.instanceId = instanceId;
|
|
27957
|
-
this.tags = tags;
|
|
27958
|
-
this.account = account;
|
|
27959
|
-
this.mailbox = mailbox;
|
|
27865
|
+
// src/datasource/sidecar/methods.ts
|
|
27866
|
+
function sidecarMethodName(datasourceId, mode) {
|
|
27867
|
+
return mode === "bm25" ? `${datasourceId}-sidecar-bm25` : `${datasourceId}-${mode}`;
|
|
27868
|
+
}
|
|
27869
|
+
|
|
27870
|
+
class SidecarRetrievalMethod {
|
|
27871
|
+
options;
|
|
27872
|
+
constructor(options) {
|
|
27873
|
+
this.options = options;
|
|
27960
27874
|
}
|
|
27961
27875
|
describe() {
|
|
27876
|
+
const { datasourceId, skillType, mode, tags } = this.options;
|
|
27877
|
+
const { type, text, capabilities } = DESCRIPTIONS[mode];
|
|
27962
27878
|
return {
|
|
27963
|
-
name:
|
|
27964
|
-
type
|
|
27965
|
-
description: `${
|
|
27879
|
+
name: sidecarMethodName(datasourceId, mode),
|
|
27880
|
+
type,
|
|
27881
|
+
description: `${text} over ${skillType} via a read-only sidecar index`,
|
|
27966
27882
|
status: "active",
|
|
27967
|
-
capabilities: [
|
|
27968
|
-
datasourceId
|
|
27969
|
-
tags
|
|
27883
|
+
capabilities: [...capabilities, "scoped", "path-opaque-sources"],
|
|
27884
|
+
datasourceId,
|
|
27885
|
+
tags
|
|
27886
|
+
};
|
|
27887
|
+
}
|
|
27888
|
+
async retrieve(query, options) {
|
|
27889
|
+
const trimmed = query.trim();
|
|
27890
|
+
if (trimmed.length === 0)
|
|
27891
|
+
return [];
|
|
27892
|
+
const topK = options.topK ?? DEFAULT_TOP_K5;
|
|
27893
|
+
const { sidecar, mode } = this.options;
|
|
27894
|
+
const hits = mode === "bm25" ? sidecar.searchLexical(trimmed, topK) : mode === "semantic" ? await sidecar.searchSemantic(trimmed, topK) : await sidecar.searchHybrid(trimmed, topK);
|
|
27895
|
+
const results = [];
|
|
27896
|
+
for (const hit of hits) {
|
|
27897
|
+
const result = this.toResult(hit, options);
|
|
27898
|
+
if (result !== undefined)
|
|
27899
|
+
results.push(result);
|
|
27900
|
+
}
|
|
27901
|
+
return results;
|
|
27902
|
+
}
|
|
27903
|
+
toResult(hit, options) {
|
|
27904
|
+
const { descriptor, datasourceId, instanceId, mode } = this.options;
|
|
27905
|
+
const chunkId = descriptor.chunkId(hit.key, hit.metadata);
|
|
27906
|
+
const source = datasourceSourcePath(datasourceId, instanceId, chunkId);
|
|
27907
|
+
if (!matchesDatasourceScope(source, options.scope))
|
|
27908
|
+
return;
|
|
27909
|
+
const method = sidecarMethodName(datasourceId, mode);
|
|
27910
|
+
return {
|
|
27911
|
+
id: `${datasourceId}:${instanceId}:${chunkId}`,
|
|
27912
|
+
content: hit.content,
|
|
27913
|
+
source,
|
|
27914
|
+
score: hit.score,
|
|
27915
|
+
metadata: {
|
|
27916
|
+
...hit.metadata,
|
|
27917
|
+
method,
|
|
27918
|
+
datasourceId,
|
|
27919
|
+
instanceId,
|
|
27920
|
+
backend: "sidecar",
|
|
27921
|
+
...hit.title !== undefined ? { title: hit.title } : {},
|
|
27922
|
+
hierarchy: descriptor.hierarchy(hit.metadata).join("/"),
|
|
27923
|
+
...hit.publishedAt !== undefined ? { publishedAt: hit.publishedAt } : {}
|
|
27924
|
+
}
|
|
27970
27925
|
};
|
|
27971
27926
|
}
|
|
27972
|
-
async retrieve(query, options) {
|
|
27973
|
-
if (!query.trim())
|
|
27974
|
-
return [];
|
|
27975
|
-
const result = await this.client.search(this.mode, query, {
|
|
27976
|
-
topK: options.topK,
|
|
27977
|
-
signal: options.signal
|
|
27978
|
-
});
|
|
27979
|
-
if (!result.ok)
|
|
27980
|
-
throw datasourceCliError("mailcrawl", `search --mode ${this.mode}`, result);
|
|
27981
|
-
return result.hits.filter((hit) => {
|
|
27982
|
-
if (this.account !== undefined && hit.accountId !== this.account)
|
|
27983
|
-
return false;
|
|
27984
|
-
if (this.mailbox !== undefined && hit.mailbox !== this.mailbox)
|
|
27985
|
-
return false;
|
|
27986
|
-
const source = datasourceSourcePath("mailcrawl", this.instanceId, hit.chunkId);
|
|
27987
|
-
return matchesDatasourceScope(source, options.scope);
|
|
27988
|
-
}).map((hit) => {
|
|
27989
|
-
const source = datasourceSourcePath("mailcrawl", this.instanceId, hit.chunkId);
|
|
27990
|
-
return {
|
|
27991
|
-
id: `mailcrawl:${this.instanceId}:${hit.chunkId}`,
|
|
27992
|
-
content: hit.snippet,
|
|
27993
|
-
source,
|
|
27994
|
-
score: hit.score,
|
|
27995
|
-
metadata: {
|
|
27996
|
-
method: `mailcrawl-${this.mode}`,
|
|
27997
|
-
datasourceId: "mailcrawl",
|
|
27998
|
-
instanceId: this.instanceId,
|
|
27999
|
-
mode: this.mode,
|
|
28000
|
-
chunkId: hit.chunkId,
|
|
28001
|
-
messageId: hit.messageId,
|
|
28002
|
-
threadId: hit.threadId,
|
|
28003
|
-
accountId: hit.accountId,
|
|
28004
|
-
mailbox: hit.mailbox,
|
|
28005
|
-
subject: hit.subject,
|
|
28006
|
-
from: hit.from,
|
|
28007
|
-
to: hit.to,
|
|
28008
|
-
date: hit.date
|
|
28009
|
-
}
|
|
28010
|
-
};
|
|
28011
|
-
}).slice(0, options.topK ?? 20);
|
|
28012
|
-
}
|
|
28013
27927
|
}
|
|
28014
|
-
var
|
|
27928
|
+
var DEFAULT_TOP_K5 = 20, DESCRIPTIONS;
|
|
28015
27929
|
var init_methods4 = __esm(() => {
|
|
28016
|
-
init_errors4();
|
|
28017
27930
|
init_scope2();
|
|
28018
|
-
|
|
28019
|
-
|
|
27931
|
+
DESCRIPTIONS = {
|
|
27932
|
+
bm25: { type: "bm25", text: "BM25-ranked FTS5 retrieval", capabilities: ["lexical", "fts5", "bm25"] },
|
|
27933
|
+
semantic: { type: "vector", text: "Semantic (embedding) retrieval", capabilities: ["semantic"] },
|
|
27934
|
+
hybrid: {
|
|
27935
|
+
type: "hybrid",
|
|
27936
|
+
text: "Hybrid BM25 + semantic retrieval (reciprocal rank fusion)",
|
|
27937
|
+
capabilities: ["lexical", "semantic", "hybrid"]
|
|
27938
|
+
}
|
|
27939
|
+
};
|
|
28020
27940
|
});
|
|
28021
27941
|
|
|
28022
|
-
// src/datasource/skills/
|
|
28023
|
-
class
|
|
27942
|
+
// src/datasource/skills/lazykatok/skill.ts
|
|
27943
|
+
class LazykatokSkill {
|
|
28024
27944
|
client;
|
|
28025
27945
|
instanceId;
|
|
28026
27946
|
instances;
|
|
28027
|
-
|
|
27947
|
+
pollingIntervalMs;
|
|
28028
27948
|
tags;
|
|
28029
|
-
|
|
28030
|
-
mailbox;
|
|
27949
|
+
sidecar;
|
|
28031
27950
|
lastIndexedAt;
|
|
28032
|
-
constructor(options
|
|
28033
|
-
|
|
28034
|
-
this.instanceId = options.instanceId ??
|
|
28035
|
-
this.instances = options.instances
|
|
28036
|
-
|
|
28037
|
-
|
|
28038
|
-
|
|
28039
|
-
this.
|
|
28040
|
-
|
|
28041
|
-
|
|
28042
|
-
|
|
28043
|
-
|
|
27951
|
+
constructor(options) {
|
|
27952
|
+
this.client = options.client;
|
|
27953
|
+
this.instanceId = options.instanceId ?? DEFAULT_INSTANCE_ID4;
|
|
27954
|
+
this.instances = options.instances !== undefined && options.instances.length > 0 ? options.instances : [this.instanceId];
|
|
27955
|
+
this.pollingIntervalMs = options.pollingIntervalMs ?? DEFAULT_POLLING_INTERVAL_MS3;
|
|
27956
|
+
this.tags = options.tags ?? DEFAULT_KAKAO_TAGS2;
|
|
27957
|
+
this.lastIndexedAt = options.lastIndexedAt;
|
|
27958
|
+
this.sidecar = options.sidecar === undefined ? undefined : new CrawlerSidecar({
|
|
27959
|
+
...options.sidecar,
|
|
27960
|
+
channelIds: options.channelIds ?? options.sidecar.channelIds ?? [],
|
|
27961
|
+
channelNames: options.channelNames ?? options.sidecar.channelNames ?? []
|
|
27962
|
+
});
|
|
28044
27963
|
}
|
|
28045
27964
|
describe() {
|
|
28046
27965
|
return {
|
|
28047
|
-
name:
|
|
28048
|
-
id:
|
|
28049
|
-
type:
|
|
28050
|
-
description: "
|
|
28051
|
-
capabilities: ["
|
|
27966
|
+
name: KAKAO_DATASOURCE_ID2,
|
|
27967
|
+
id: KAKAO_DATASOURCE_ID2,
|
|
27968
|
+
type: KAKAO_SKILL_TYPE,
|
|
27969
|
+
description: "KakaoTalk datasource via the external lazykatok CLI",
|
|
27970
|
+
capabilities: ["chat", "external-cli", "polling", "bm25", "semantic"],
|
|
28052
27971
|
tags: this.tags,
|
|
28053
27972
|
status: "active",
|
|
28054
27973
|
requiresExternalCli: true,
|
|
28055
|
-
datasourceId:
|
|
27974
|
+
datasourceId: KAKAO_DATASOURCE_ID2,
|
|
28056
27975
|
instanceId: this.instanceId,
|
|
28057
27976
|
instances: this.instances
|
|
28058
27977
|
};
|
|
28059
27978
|
}
|
|
28060
27979
|
polling() {
|
|
28061
27980
|
return {
|
|
28062
|
-
mode:
|
|
28063
|
-
|
|
27981
|
+
mode: "poll",
|
|
27982
|
+
intervalMs: this.pollingIntervalMs,
|
|
28064
27983
|
lastIndexedAt: this.lastIndexedAt
|
|
28065
27984
|
};
|
|
28066
27985
|
}
|
|
28067
27986
|
async index(options) {
|
|
28068
27987
|
try {
|
|
28069
|
-
const
|
|
28070
|
-
if (!
|
|
28071
|
-
return this.fail(
|
|
28072
|
-
|
|
28073
|
-
|
|
28074
|
-
|
|
28075
|
-
if (
|
|
28076
|
-
|
|
28077
|
-
|
|
28078
|
-
|
|
28079
|
-
message: `mailcrawl semantic index unavailable (${indexed.reason}); BM25 remains available`,
|
|
27988
|
+
const doctor = await this.client.doctor(options?.signal);
|
|
27989
|
+
if (!doctor.ok)
|
|
27990
|
+
return this.fail("datasource-unavailable", doctor);
|
|
27991
|
+
const sync = await this.client.sync(options?.signal);
|
|
27992
|
+
if (!sync.ok)
|
|
27993
|
+
return this.fail("datasource-index-failed", sync);
|
|
27994
|
+
if (this.sidecar !== undefined) {
|
|
27995
|
+
this.lastIndexedAt = Date.now();
|
|
27996
|
+
return {
|
|
27997
|
+
ok: true,
|
|
28080
27998
|
instanceId: this.instanceId,
|
|
28081
|
-
|
|
28082
|
-
|
|
27999
|
+
skill: KAKAO_DATASOURCE_ID2,
|
|
28000
|
+
chunkCount: syncChunkCount(sync),
|
|
28001
|
+
indexedAt: this.lastIndexedAt,
|
|
28002
|
+
diagnostics: await this.syncSidecar()
|
|
28003
|
+
};
|
|
28083
28004
|
}
|
|
28005
|
+
const indexResult = await this.client.index(options?.signal);
|
|
28006
|
+
if (!indexResult.ok)
|
|
28007
|
+
return this.fail("datasource-index-failed", indexResult);
|
|
28084
28008
|
this.lastIndexedAt = Date.now();
|
|
28009
|
+
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;
|
|
28085
28010
|
return {
|
|
28086
28011
|
ok: true,
|
|
28087
28012
|
instanceId: this.instanceId,
|
|
28088
|
-
skill:
|
|
28089
|
-
chunkCount
|
|
28013
|
+
skill: KAKAO_DATASOURCE_ID2,
|
|
28014
|
+
chunkCount,
|
|
28090
28015
|
indexedAt: this.lastIndexedAt,
|
|
28091
|
-
diagnostics
|
|
28016
|
+
diagnostics: []
|
|
28092
28017
|
};
|
|
28093
|
-
} catch {
|
|
28094
|
-
return this.fail("datasource-unavailable",
|
|
28018
|
+
} catch (error) {
|
|
28019
|
+
return this.fail("datasource-unavailable", {
|
|
28020
|
+
ok: false,
|
|
28021
|
+
reason: "spawn-error",
|
|
28022
|
+
stdout: "",
|
|
28023
|
+
stderr: describeRetrievalError(error),
|
|
28024
|
+
code: null
|
|
28025
|
+
});
|
|
28095
28026
|
}
|
|
28096
28027
|
}
|
|
28028
|
+
close() {
|
|
28029
|
+
this.sidecar?.close();
|
|
28030
|
+
}
|
|
28097
28031
|
retrievalMethods() {
|
|
28032
|
+
const cliBm25 = new LazykatokBm25Method({ client: this.client, instanceId: this.instanceId, tags: this.tags });
|
|
28033
|
+
const { sidecar } = this;
|
|
28034
|
+
if (sidecar === undefined) {
|
|
28035
|
+
return [
|
|
28036
|
+
cliBm25,
|
|
28037
|
+
new LazykatokSemanticMethod({ client: this.client, instanceId: this.instanceId, tags: this.tags })
|
|
28038
|
+
];
|
|
28039
|
+
}
|
|
28040
|
+
const shared = {
|
|
28041
|
+
sidecar,
|
|
28042
|
+
descriptor: sidecar.descriptor,
|
|
28043
|
+
datasourceId: KAKAO_DATASOURCE_ID2,
|
|
28044
|
+
skillType: KAKAO_SKILL_TYPE,
|
|
28045
|
+
instanceId: this.instanceId,
|
|
28046
|
+
tags: this.tags
|
|
28047
|
+
};
|
|
28048
|
+
const bm25 = new SidecarRetrievalMethod({ ...shared, mode: "bm25" });
|
|
28049
|
+
if (!sidecar.semanticEnabled)
|
|
28050
|
+
return [cliBm25, bm25];
|
|
28098
28051
|
return [
|
|
28099
|
-
|
|
28100
|
-
|
|
28101
|
-
new
|
|
28052
|
+
cliBm25,
|
|
28053
|
+
bm25,
|
|
28054
|
+
new SidecarRetrievalMethod({ ...shared, mode: "semantic" }),
|
|
28055
|
+
new SidecarRetrievalMethod({ ...shared, mode: "hybrid" })
|
|
28102
28056
|
];
|
|
28103
28057
|
}
|
|
28058
|
+
async syncSidecar() {
|
|
28059
|
+
if (this.sidecar === undefined || !this.sidecar.semanticEnabled)
|
|
28060
|
+
return [];
|
|
28061
|
+
const base = { instanceId: this.instanceId, source: KAKAO_DATASOURCE_ID2 };
|
|
28062
|
+
let result;
|
|
28063
|
+
try {
|
|
28064
|
+
result = await this.sidecar.sync();
|
|
28065
|
+
} catch (error) {
|
|
28066
|
+
result = { ok: false, message: error instanceof Error ? error.message : String(error) };
|
|
28067
|
+
}
|
|
28068
|
+
if (!result.ok) {
|
|
28069
|
+
return [
|
|
28070
|
+
{
|
|
28071
|
+
code: "datasource-index-failed",
|
|
28072
|
+
severity: "warning",
|
|
28073
|
+
message: `semantic-unavailable: ${result.message}`.slice(0, 300),
|
|
28074
|
+
...base
|
|
28075
|
+
}
|
|
28076
|
+
];
|
|
28077
|
+
}
|
|
28078
|
+
const diagnostics = [];
|
|
28079
|
+
if (result.skipped > 0) {
|
|
28080
|
+
diagnostics.push({
|
|
28081
|
+
code: "datasource-index-partial",
|
|
28082
|
+
severity: "warning",
|
|
28083
|
+
message: `semantic index skipped ${result.skipped} message(s) the embedder rejected; they stay searchable by BM25 and are retried when edited`,
|
|
28084
|
+
...base
|
|
28085
|
+
});
|
|
28086
|
+
}
|
|
28087
|
+
if (result.remaining > 0) {
|
|
28088
|
+
diagnostics.push({
|
|
28089
|
+
code: "datasource-index-partial",
|
|
28090
|
+
severity: "info",
|
|
28091
|
+
message: `semantic index catching up: ${result.embedded} embedded this refresh, ${result.remaining} remaining; hybrid search uses BM25 for the rest until a later refresh`,
|
|
28092
|
+
...base
|
|
28093
|
+
});
|
|
28094
|
+
}
|
|
28095
|
+
return diagnostics;
|
|
28096
|
+
}
|
|
28104
28097
|
describeSources() {
|
|
28105
28098
|
return this.instances.map((instanceId) => ({
|
|
28106
|
-
source: datasourceSourcePath(
|
|
28107
|
-
datasourceId:
|
|
28108
|
-
skill:
|
|
28099
|
+
source: datasourceSourcePath(KAKAO_DATASOURCE_ID2, instanceId),
|
|
28100
|
+
datasourceId: KAKAO_DATASOURCE_ID2,
|
|
28101
|
+
skill: KAKAO_DATASOURCE_ID2,
|
|
28109
28102
|
instanceId,
|
|
28110
|
-
contentType: "
|
|
28111
|
-
metadata: {
|
|
28112
|
-
|
|
28113
|
-
|
|
28114
|
-
|
|
28115
|
-
|
|
28116
|
-
|
|
28117
|
-
|
|
28118
|
-
|
|
28119
|
-
|
|
28120
|
-
|
|
28121
|
-
|
|
28122
|
-
|
|
28123
|
-
|
|
28124
|
-
|
|
28125
|
-
|
|
28126
|
-
|
|
28127
|
-
|
|
28128
|
-
|
|
28129
|
-
|
|
28130
|
-
|
|
28131
|
-
|
|
28132
|
-
|
|
28133
|
-
##
|
|
28134
|
-
|
|
28135
|
-
|
|
28136
|
-
|
|
28103
|
+
contentType: "chat",
|
|
28104
|
+
metadata: {
|
|
28105
|
+
datasourceId: KAKAO_DATASOURCE_ID2,
|
|
28106
|
+
instanceId,
|
|
28107
|
+
tags: this.tags
|
|
28108
|
+
}
|
|
28109
|
+
}));
|
|
28110
|
+
}
|
|
28111
|
+
skillManifest() {
|
|
28112
|
+
const instanceSources = this.instances.map((instanceId) => `- \`${datasourceSourcePath(KAKAO_DATASOURCE_ID2, instanceId)}\``).join(`
|
|
28113
|
+
`);
|
|
28114
|
+
const cadence = this.pollingIntervalMs > 0 ? `roughly every ${Math.round(this.pollingIntervalMs / 60000)} minute(s) when auto-refresh runs` : "on manual refresh only";
|
|
28115
|
+
return {
|
|
28116
|
+
name: `datasource-${KAKAO_DATASOURCE_ID2}`,
|
|
28117
|
+
description: "Search indexed KakaoTalk chat history (messages, senders, room context). Use for questions about KakaoTalk conversations, decisions made in chats, or who said what.",
|
|
28118
|
+
content: [
|
|
28119
|
+
`# KakaoTalk datasource (${KAKAO_SKILL_TYPE})`,
|
|
28120
|
+
"",
|
|
28121
|
+
"This skill searches KakaoTalk chats that are indexed through the external `lazykatok` CLI. AutoRAG never reads KakaoTalk databases directly.",
|
|
28122
|
+
"",
|
|
28123
|
+
"## When to use",
|
|
28124
|
+
"Use this skill when the question is about KakaoTalk conversations, chat participants, or content shared inside chats.",
|
|
28125
|
+
"",
|
|
28126
|
+
"## Indexing",
|
|
28127
|
+
`Indexing is server-managed and refreshed ${cadence}. You do not trigger indexing; just search.`,
|
|
28128
|
+
"",
|
|
28129
|
+
"## How to search",
|
|
28130
|
+
`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:`,
|
|
28131
|
+
instanceSources.length > 0 ? instanceSources : "- (no configured instances)",
|
|
28132
|
+
"",
|
|
28133
|
+
"Chat/channel filtering is owned by lazykatok.",
|
|
28134
|
+
"",
|
|
28135
|
+
"## Native CLI",
|
|
28136
|
+
"The external `lazykatok` CLI owns the archive, index, and credentials. When the dedicated tool cannot express what you need, call lazykatok directly through `bash`:",
|
|
28137
|
+
'- `lazykatok search bm25 "<query>" --json --limit 20` — lexical search (`semantic` mode also available)',
|
|
28138
|
+
"- `lazykatok chunk get <chunkId> --json` — fetch one chunk by id (chunk ids appear in result metadata)",
|
|
28139
|
+
"- `lazykatok chunk context <chunkId> --json` — surrounding messages of a chunk",
|
|
28140
|
+
"Never pass datasource virtual paths (`/kakao/...`) to bash; they are not OS paths.",
|
|
28141
|
+
"",
|
|
28142
|
+
"## Output rules",
|
|
28143
|
+
"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."
|
|
28144
|
+
].join(`
|
|
28145
|
+
`)
|
|
28137
28146
|
};
|
|
28138
28147
|
}
|
|
28139
|
-
fail(code,
|
|
28148
|
+
fail(code, result) {
|
|
28149
|
+
const base = result.stderr.length > 0 ? `${result.reason}: ${result.stderr.trim().slice(0, 4000)}` : result.reason;
|
|
28150
|
+
const message = code === "datasource-index-failed" && result.reason === "timeout" ? `${base} — ${connectorSyncTimeoutHint()}` : base;
|
|
28151
|
+
const diagnostic = {
|
|
28152
|
+
code,
|
|
28153
|
+
severity: code === "datasource-unavailable" ? "warning" : "error",
|
|
28154
|
+
message,
|
|
28155
|
+
instanceId: this.instanceId,
|
|
28156
|
+
source: KAKAO_DATASOURCE_ID2
|
|
28157
|
+
};
|
|
28140
28158
|
return {
|
|
28141
28159
|
ok: false,
|
|
28142
28160
|
instanceId: this.instanceId,
|
|
28143
|
-
skill:
|
|
28161
|
+
skill: KAKAO_DATASOURCE_ID2,
|
|
28144
28162
|
indexedAt: Date.now(),
|
|
28145
|
-
diagnostics: [
|
|
28146
|
-
|
|
28147
|
-
code,
|
|
28148
|
-
severity: code === "datasource-unavailable" ? "warning" : "error",
|
|
28149
|
-
message: `mailcrawl ${message}`,
|
|
28150
|
-
instanceId: this.instanceId,
|
|
28151
|
-
source: "mailcrawl"
|
|
28152
|
-
}
|
|
28153
|
-
],
|
|
28154
|
-
error: message,
|
|
28163
|
+
diagnostics: [diagnostic],
|
|
28164
|
+
error: result.reason,
|
|
28155
28165
|
code,
|
|
28156
28166
|
message
|
|
28157
28167
|
};
|
|
28158
28168
|
}
|
|
28159
28169
|
}
|
|
28160
|
-
|
|
28161
|
-
|
|
28170
|
+
function syncChunkCount(sync) {
|
|
28171
|
+
if (!("data" in sync) || typeof sync.data !== "object" || sync.data === null)
|
|
28172
|
+
return 0;
|
|
28173
|
+
const meta = "metadata" in sync.data && typeof sync.data.metadata === "object" && sync.data.metadata !== null ? sync.data.metadata : undefined;
|
|
28174
|
+
if (typeof meta?.chunks === "number")
|
|
28175
|
+
return meta.chunks;
|
|
28176
|
+
if ("messageCount" in sync.data && typeof sync.data.messageCount === "number")
|
|
28177
|
+
return sync.data.messageCount;
|
|
28178
|
+
return 0;
|
|
28179
|
+
}
|
|
28180
|
+
var KAKAO_DATASOURCE_ID2 = "kakao", KAKAO_SKILL_TYPE = "kakaotalk", DEFAULT_INSTANCE_ID4 = "default", DEFAULT_POLLING_INTERVAL_MS3, DEFAULT_KAKAO_TAGS2;
|
|
28181
|
+
var init_skill7 = __esm(() => {
|
|
28182
|
+
init_skip();
|
|
28162
28183
|
init_scope2();
|
|
28163
|
-
|
|
28164
|
-
init_config3();
|
|
28184
|
+
init_sidecar();
|
|
28165
28185
|
init_methods4();
|
|
28166
|
-
|
|
28186
|
+
init_methods3();
|
|
28187
|
+
DEFAULT_POLLING_INTERVAL_MS3 = 15 * 60 * 1000;
|
|
28188
|
+
DEFAULT_KAKAO_TAGS2 = ["kakaotalk", "personal", "pii"];
|
|
28167
28189
|
});
|
|
28168
28190
|
|
|
28169
|
-
// src/datasource/skills/
|
|
28170
|
-
var
|
|
28171
|
-
|
|
28172
|
-
|
|
28173
|
-
|
|
28191
|
+
// src/datasource/skills/lazykatok/index.ts
|
|
28192
|
+
var init_lazykatok = __esm(() => {
|
|
28193
|
+
init_client8();
|
|
28194
|
+
init_methods3();
|
|
28195
|
+
init_skill7();
|
|
28196
|
+
init_types5();
|
|
28197
|
+
});
|
|
28198
|
+
|
|
28199
|
+
// src/datasource/skills/mail-export/connector.ts
|
|
28200
|
+
import { readdir as readdir4, readFile as readFile5, stat as stat6 } from "node:fs/promises";
|
|
28201
|
+
import { basename as basename12, dirname as dirname25, extname as extname6, join as join37 } from "node:path";
|
|
28202
|
+
import { simpleParser as simpleParser2 } from "mailparser";
|
|
28203
|
+
|
|
28204
|
+
class MailExportConnector {
|
|
28205
|
+
options;
|
|
28206
|
+
constructor(options = {}) {
|
|
28207
|
+
this.options = options;
|
|
28208
|
+
}
|
|
28209
|
+
async fetch() {
|
|
28210
|
+
const paths = this.options.paths ?? [];
|
|
28211
|
+
if (paths.length === 0)
|
|
28212
|
+
return { ok: false, reason: "not-configured", message: "no mail export paths configured" };
|
|
28213
|
+
const maxDocuments = this.options.maxDocuments ?? DEFAULT_MAX_DOCUMENTS3;
|
|
28214
|
+
const maxBytes = this.options.maxBytesPerFile ?? DEFAULT_MAX_BYTES_PER_FILE2;
|
|
28215
|
+
const files = [];
|
|
28216
|
+
let missingPaths = 0;
|
|
28217
|
+
for (const path of paths) {
|
|
28218
|
+
try {
|
|
28219
|
+
const info = await stat6(path);
|
|
28220
|
+
if (info.isDirectory()) {
|
|
28221
|
+
const entries = await readdir4(path, { recursive: true, withFileTypes: true });
|
|
28222
|
+
for (const entry of entries) {
|
|
28223
|
+
if (!entry.isFile())
|
|
28224
|
+
continue;
|
|
28225
|
+
const extension = extname6(entry.name).toLowerCase();
|
|
28226
|
+
if (extension === ".eml" || extension === ".mbox") {
|
|
28227
|
+
files.push(join37(entry.parentPath, entry.name));
|
|
28228
|
+
}
|
|
28229
|
+
}
|
|
28230
|
+
} else if (info.isFile()) {
|
|
28231
|
+
files.push(path);
|
|
28232
|
+
}
|
|
28233
|
+
} catch {
|
|
28234
|
+
missingPaths += 1;
|
|
28235
|
+
}
|
|
28236
|
+
}
|
|
28237
|
+
if (missingPaths === paths.length) {
|
|
28238
|
+
return { ok: false, reason: "unavailable", message: "no configured mail export paths were readable" };
|
|
28239
|
+
}
|
|
28240
|
+
const documents = [];
|
|
28241
|
+
const warnings = [];
|
|
28242
|
+
if (missingPaths > 0)
|
|
28243
|
+
warnings.push(`${missingPaths} configured path(s) were unreadable`);
|
|
28244
|
+
let parseFailures = 0;
|
|
28245
|
+
let skippedLarge = 0;
|
|
28246
|
+
for (const file of files.sort()) {
|
|
28247
|
+
if (documents.length >= maxDocuments)
|
|
28248
|
+
break;
|
|
28249
|
+
let bytes;
|
|
28250
|
+
try {
|
|
28251
|
+
const info = await stat6(file);
|
|
28252
|
+
if (info.size > maxBytes) {
|
|
28253
|
+
skippedLarge += 1;
|
|
28254
|
+
continue;
|
|
28255
|
+
}
|
|
28256
|
+
bytes = await readFile5(file);
|
|
28257
|
+
} catch {
|
|
28258
|
+
parseFailures += 1;
|
|
28259
|
+
continue;
|
|
28260
|
+
}
|
|
28261
|
+
const stem = basename12(file, extname6(file));
|
|
28262
|
+
const mailboxDir = basename12(dirname25(file));
|
|
28263
|
+
const hierarchy = ["mailboxes", mailboxDir, stem];
|
|
28264
|
+
const extension = extname6(file).toLowerCase();
|
|
28265
|
+
const rawMessages = extension === ".mbox" ? splitMbox(bytes.toString("utf8")) : [bytes.toString("utf8")];
|
|
28266
|
+
for (const [index, raw] of rawMessages.entries()) {
|
|
28267
|
+
if (documents.length >= maxDocuments)
|
|
28268
|
+
break;
|
|
28269
|
+
try {
|
|
28270
|
+
const parsed = await simpleParser2(raw);
|
|
28271
|
+
const subject = parsed.subject ?? "(no subject)";
|
|
28272
|
+
const from = Array.isArray(parsed.from) ? parsed.from[0]?.text : parsed.from?.text;
|
|
28273
|
+
const to = Array.isArray(parsed.to) ? parsed.to[0]?.text : parsed.to?.text;
|
|
28274
|
+
const body = (parsed.text ?? "").trim().slice(0, MAX_BODY_CHARS);
|
|
28275
|
+
const headerBlock = [
|
|
28276
|
+
`Subject: ${subject}`,
|
|
28277
|
+
...from !== undefined ? [`From: ${from}`] : [],
|
|
28278
|
+
...to !== undefined ? [`To: ${to}`] : [],
|
|
28279
|
+
...parsed.date !== undefined ? [`Date: ${parsed.date.toISOString()}`] : []
|
|
28280
|
+
].join(`
|
|
28281
|
+
`);
|
|
28282
|
+
const content = `${headerBlock}
|
|
28283
|
+
|
|
28284
|
+
${body}`.trim();
|
|
28285
|
+
if (content.length === 0)
|
|
28286
|
+
continue;
|
|
28287
|
+
documents.push({
|
|
28288
|
+
docId: extension === ".mbox" ? `${stem}-${index + 1}` : stem,
|
|
28289
|
+
hierarchy,
|
|
28290
|
+
title: subject,
|
|
28291
|
+
content,
|
|
28292
|
+
...parsed.date !== undefined ? { publishedAt: parsed.date.getTime() } : {},
|
|
28293
|
+
metadata: {
|
|
28294
|
+
path: file,
|
|
28295
|
+
subject,
|
|
28296
|
+
...from !== undefined ? { from } : {},
|
|
28297
|
+
messageIndex: index
|
|
28298
|
+
}
|
|
28299
|
+
});
|
|
28300
|
+
} catch {
|
|
28301
|
+
parseFailures += 1;
|
|
28302
|
+
}
|
|
28303
|
+
}
|
|
28304
|
+
}
|
|
28305
|
+
if (skippedLarge > 0)
|
|
28306
|
+
warnings.push(`${skippedLarge} file(s) exceeded the size limit and were skipped`);
|
|
28307
|
+
if (parseFailures > 0)
|
|
28308
|
+
warnings.push(`${parseFailures} message(s) failed to parse`);
|
|
28309
|
+
return { ok: true, documents, ...warnings.length > 0 ? { warnings } : {} };
|
|
28310
|
+
}
|
|
28311
|
+
}
|
|
28312
|
+
function splitMbox(text) {
|
|
28313
|
+
const lines = text.split(/\r?\n/u);
|
|
28314
|
+
const messages = [];
|
|
28315
|
+
let current = [];
|
|
28316
|
+
for (const line of lines) {
|
|
28317
|
+
if (MBOX_SEPARATOR.test(line) && line.startsWith("From ")) {
|
|
28318
|
+
if (current.length > 0)
|
|
28319
|
+
messages.push(current.join(`
|
|
28320
|
+
`));
|
|
28321
|
+
current = [];
|
|
28322
|
+
continue;
|
|
28323
|
+
}
|
|
28324
|
+
current.push(line);
|
|
28325
|
+
}
|
|
28326
|
+
if (current.length > 0)
|
|
28327
|
+
messages.push(current.join(`
|
|
28328
|
+
`));
|
|
28329
|
+
return messages.filter((message) => message.trim().length > 0);
|
|
28330
|
+
}
|
|
28331
|
+
var DEFAULT_MAX_DOCUMENTS3 = 500, DEFAULT_MAX_BYTES_PER_FILE2, MAX_BODY_CHARS = 50000, MBOX_SEPARATOR;
|
|
28332
|
+
var init_connector4 = __esm(() => {
|
|
28333
|
+
DEFAULT_MAX_BYTES_PER_FILE2 = 50 * 1024 * 1024;
|
|
28334
|
+
MBOX_SEPARATOR = /^From .*\d{4}$/mu;
|
|
28335
|
+
});
|
|
28336
|
+
|
|
28337
|
+
// src/datasource/skills/mail-export/skill.ts
|
|
28338
|
+
var MAIL_EXPORT_SKILL_DEFINITION, MailExportSkill;
|
|
28339
|
+
var init_skill8 = __esm(() => {
|
|
28340
|
+
init_connector_skill();
|
|
28341
|
+
init_connector4();
|
|
28342
|
+
MAIL_EXPORT_SKILL_DEFINITION = {
|
|
28343
|
+
skillName: "mail-export",
|
|
28344
|
+
skillType: "mail-export",
|
|
28345
|
+
description: "Local mail export datasource",
|
|
28346
|
+
capabilities: ["email", "filesystem"],
|
|
28347
|
+
defaultTags: ["mail-export", "email", "pii"],
|
|
28348
|
+
contentType: "email",
|
|
28349
|
+
manifestDescription: "Search indexed local mail exports (mbox/eml archives). Use for questions about archived email conversations.",
|
|
28350
|
+
manifestNotes: [
|
|
28351
|
+
"Only the server-configured export paths are indexed; archives are read locally and never uploaded."
|
|
28352
|
+
]
|
|
28353
|
+
};
|
|
28354
|
+
MailExportSkill = class MailExportSkill extends ConnectorDatasourceSkill {
|
|
28355
|
+
constructor(options = {}) {
|
|
28356
|
+
const { connector, connectorOptions, ...rest } = options;
|
|
28357
|
+
super(MAIL_EXPORT_SKILL_DEFINITION, {
|
|
28358
|
+
...rest,
|
|
28359
|
+
connector: connector ?? new MailExportConnector(connectorOptions ?? {})
|
|
28360
|
+
});
|
|
28361
|
+
}
|
|
28362
|
+
};
|
|
28363
|
+
});
|
|
28364
|
+
|
|
28365
|
+
// src/datasource/skills/mail-export/index.ts
|
|
28366
|
+
var init_mail_export = __esm(() => {
|
|
28367
|
+
init_connector4();
|
|
28368
|
+
init_skill8();
|
|
28174
28369
|
});
|
|
28175
28370
|
|
|
28176
|
-
// src/datasource/
|
|
28177
|
-
|
|
28178
|
-
|
|
28179
|
-
|
|
28180
|
-
|
|
28181
|
-
function decodeVector(blob) {
|
|
28182
|
-
if (blob.byteLength % Float32Array.BYTES_PER_ELEMENT !== 0)
|
|
28183
|
-
return;
|
|
28184
|
-
return new Float32Array(new Uint8Array(blob).buffer);
|
|
28185
|
-
}
|
|
28186
|
-
function cosine3(a, b) {
|
|
28187
|
-
if (a.length === 0 || a.length !== b.length)
|
|
28188
|
-
return 0;
|
|
28189
|
-
let dot = 0;
|
|
28190
|
-
let normA = 0;
|
|
28191
|
-
let normB = 0;
|
|
28192
|
-
for (let index = 0;index < a.length; index += 1) {
|
|
28193
|
-
const x = a[index] ?? 0;
|
|
28194
|
-
const y = b[index] ?? 0;
|
|
28195
|
-
dot += x * y;
|
|
28196
|
-
normA += x * x;
|
|
28197
|
-
normB += y * y;
|
|
28198
|
-
}
|
|
28199
|
-
if (normA === 0 || normB === 0)
|
|
28200
|
-
return 0;
|
|
28201
|
-
return dot / Math.sqrt(normA * normB);
|
|
28202
|
-
}
|
|
28203
|
-
function topKByCosine(query, rows, k) {
|
|
28204
|
-
if (k <= 0)
|
|
28205
|
-
return [];
|
|
28206
|
-
const scored = [];
|
|
28207
|
-
for (const row of rows) {
|
|
28208
|
-
const score = cosine3(query, row.vector);
|
|
28209
|
-
if (score > 0)
|
|
28210
|
-
scored.push({ key: row.key, score });
|
|
28371
|
+
// src/datasource/skills/mailcrawl/config.ts
|
|
28372
|
+
import { chmodSync, mkdirSync as mkdirSync23 } from "node:fs";
|
|
28373
|
+
function validateMailcrawlInstanceId(instanceId) {
|
|
28374
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/u.test(instanceId)) {
|
|
28375
|
+
throw new Error("mailcrawl instanceId must be a safe single path segment");
|
|
28211
28376
|
}
|
|
28212
|
-
scored.sort((a, b) => b.score - a.score || a.key.localeCompare(b.key));
|
|
28213
|
-
return scored.slice(0, k);
|
|
28214
28377
|
}
|
|
28215
|
-
function
|
|
28216
|
-
|
|
28217
|
-
|
|
28218
|
-
|
|
28219
|
-
for (const [index, key] of list.entries()) {
|
|
28220
|
-
if (seen.has(key))
|
|
28221
|
-
continue;
|
|
28222
|
-
seen.add(key);
|
|
28223
|
-
scores.set(key, (scores.get(key) ?? 0) + 1 / (RRF_K2 + index + 1));
|
|
28224
|
-
}
|
|
28225
|
-
}
|
|
28226
|
-
return [...scores.entries()].map(([key, score]) => ({ key, score })).sort((a, b) => b.score - a.score);
|
|
28378
|
+
function ensurePrivateMailcrawlDataDir(path) {
|
|
28379
|
+
mkdirSync23(path, { recursive: true, mode: 448 });
|
|
28380
|
+
if (process.platform !== "win32")
|
|
28381
|
+
chmodSync(path, 448);
|
|
28227
28382
|
}
|
|
28228
|
-
var
|
|
28383
|
+
var init_config3 = () => {};
|
|
28229
28384
|
|
|
28230
|
-
// src/datasource/
|
|
28231
|
-
import {
|
|
28232
|
-
import { mkdirSync as mkdirSync23, statSync as statSync3 } from "node:fs";
|
|
28233
|
-
import { dirname as dirname25 } from "node:path";
|
|
28234
|
-
import { DatabaseSync as DatabaseSync2 } from "node:sqlite";
|
|
28235
|
-
import { pathToFileURL as pathToFileURL2 } from "node:url";
|
|
28385
|
+
// src/datasource/skills/mailcrawl/client.ts
|
|
28386
|
+
import { spawn as spawn15 } from "node:child_process";
|
|
28236
28387
|
|
|
28237
|
-
class
|
|
28238
|
-
descriptor;
|
|
28388
|
+
class MailcrawlClient {
|
|
28239
28389
|
options;
|
|
28240
|
-
|
|
28241
|
-
where;
|
|
28242
|
-
opened;
|
|
28243
|
-
attached;
|
|
28244
|
-
lastSemantic;
|
|
28245
|
-
constructor(options) {
|
|
28390
|
+
constructor(options = {}) {
|
|
28246
28391
|
this.options = options;
|
|
28247
|
-
|
|
28248
|
-
|
|
28249
|
-
const metadataColumns = descriptor.metadata.map((column, index) => `${column.sql} as m${index}`);
|
|
28250
|
-
this.select = [
|
|
28251
|
-
`${descriptor.keyExpression} as k`,
|
|
28252
|
-
`${descriptor.textExpression} as content`,
|
|
28253
|
-
`${descriptor.titleExpression ?? "null"} as title`,
|
|
28254
|
-
`${descriptor.epochSecondsExpression ?? "null"} as epoch`,
|
|
28255
|
-
...metadataColumns
|
|
28256
|
-
].join(", ");
|
|
28257
|
-
const scope = descriptor.scopeExpression !== undefined && options.workspaceId !== undefined;
|
|
28258
|
-
const allowlist = [
|
|
28259
|
-
...(options.channelIds?.length ?? 0) > 0 ? [`${descriptor.channelIdExpression} in (${options.channelIds?.map(() => "?").join(", ")})`] : [],
|
|
28260
|
-
...(options.channelNames?.length ?? 0) > 0 ? [`${descriptor.channelNameExpression} in (${options.channelNames?.map(() => "?").join(", ")})`] : []
|
|
28261
|
-
];
|
|
28262
|
-
this.where = [
|
|
28263
|
-
descriptor.liveWhere,
|
|
28264
|
-
...scope ? [`${descriptor.scopeExpression} = ?`] : [],
|
|
28265
|
-
...allowlist.length > 0 ? [`(${allowlist.join(" or ")})`] : []
|
|
28266
|
-
].join(" and ");
|
|
28267
|
-
}
|
|
28268
|
-
get db() {
|
|
28269
|
-
if (this.opened !== undefined)
|
|
28270
|
-
return this.opened;
|
|
28271
|
-
mkdirSync23(dirname25(this.options.sidecarDbPath), { recursive: true });
|
|
28272
|
-
const db = new DatabaseSync2(this.options.sidecarDbPath);
|
|
28273
|
-
db.exec(`pragma busy_timeout = ${BUSY_TIMEOUT_MS}`);
|
|
28274
|
-
db.exec("pragma journal_mode = wal");
|
|
28275
|
-
db.exec(`
|
|
28276
|
-
create table if not exists sidecar_meta (k text primary key, v text not null);
|
|
28277
|
-
create table if not exists sidecar_vec (message_key text primary key, hash text not null, vec blob not null);
|
|
28278
|
-
`);
|
|
28279
|
-
this.opened = db;
|
|
28280
|
-
return db;
|
|
28281
|
-
}
|
|
28282
|
-
attachCrawler() {
|
|
28283
|
-
let file;
|
|
28284
|
-
try {
|
|
28285
|
-
const { dev, ino } = statSync3(this.options.crawlerDbPath);
|
|
28286
|
-
file = { dev, ino };
|
|
28287
|
-
} catch {
|
|
28288
|
-
this.detachCrawler();
|
|
28289
|
-
return false;
|
|
28290
|
-
}
|
|
28291
|
-
if (this.attached !== undefined) {
|
|
28292
|
-
if (this.attached.dev === file.dev && this.attached.ino === file.ino)
|
|
28293
|
-
return true;
|
|
28294
|
-
this.detachCrawler();
|
|
28295
|
-
}
|
|
28296
|
-
const crawlerUri = `${pathToFileURL2(this.options.crawlerDbPath).href}?mode=ro`.replaceAll("'", "''");
|
|
28297
|
-
try {
|
|
28298
|
-
this.db.exec(`attach database '${crawlerUri}' as crawler`);
|
|
28299
|
-
} catch {
|
|
28300
|
-
return false;
|
|
28301
|
-
}
|
|
28302
|
-
const { descriptor } = this.options;
|
|
28303
|
-
try {
|
|
28304
|
-
this.db.prepare(`select ${this.select} from ${descriptor.fromClause} where ${this.where} limit 0`).all(...this.whereParams());
|
|
28305
|
-
this.db.prepare(`select 1 from ${descriptor.fts.table} limit 0`).all();
|
|
28306
|
-
} catch {
|
|
28307
|
-
this.db.exec("detach database crawler");
|
|
28308
|
-
return false;
|
|
28309
|
-
}
|
|
28310
|
-
this.attached = file;
|
|
28311
|
-
this.lastSemantic = undefined;
|
|
28312
|
-
return true;
|
|
28313
|
-
}
|
|
28314
|
-
detachCrawler() {
|
|
28315
|
-
if (this.attached === undefined)
|
|
28316
|
-
return;
|
|
28317
|
-
this.attached = undefined;
|
|
28318
|
-
this.lastSemantic = undefined;
|
|
28319
|
-
try {
|
|
28320
|
-
this.opened?.exec("detach database crawler");
|
|
28321
|
-
} catch {}
|
|
28322
|
-
}
|
|
28323
|
-
get semanticEnabled() {
|
|
28324
|
-
return this.options.embedder !== undefined;
|
|
28325
|
-
}
|
|
28326
|
-
close() {
|
|
28327
|
-
this.opened?.close();
|
|
28328
|
-
this.opened = undefined;
|
|
28329
|
-
this.attached = undefined;
|
|
28330
|
-
this.lastSemantic = undefined;
|
|
28331
|
-
}
|
|
28332
|
-
whereParams() {
|
|
28333
|
-
const { descriptor, workspaceId, channelIds = [], channelNames = [] } = this.options;
|
|
28334
|
-
return [
|
|
28335
|
-
...descriptor.scopeExpression !== undefined && workspaceId !== undefined ? [workspaceId] : [],
|
|
28336
|
-
...channelIds,
|
|
28337
|
-
...channelNames
|
|
28338
|
-
];
|
|
28339
|
-
}
|
|
28340
|
-
searchLexical(query, topK) {
|
|
28341
|
-
const match = toMatchQuery(query);
|
|
28342
|
-
if (match === undefined || topK <= 0 || !this.attachCrawler())
|
|
28343
|
-
return [];
|
|
28344
|
-
const { descriptor } = this.options;
|
|
28345
|
-
const rows = this.db.prepare(`select ${this.select}, bm25(${descriptor.fts.table}) as rank
|
|
28346
|
-
from ${descriptor.fromClause} join ${descriptor.fts.table} on ${descriptor.fts.joinOn}
|
|
28347
|
-
where ${descriptor.fts.table} match ? and ${this.where}
|
|
28348
|
-
order by rank limit ?`).all(match, ...this.whereParams(), topK);
|
|
28349
|
-
return rows.map((row) => this.toHit(this.toRow(row), -Number(row.rank)));
|
|
28350
|
-
}
|
|
28351
|
-
async searchSemantic(query, topK) {
|
|
28352
|
-
const trimmed = query.trim();
|
|
28353
|
-
if (this.options.embedder === undefined || trimmed.length === 0 || topK <= 0 || !this.attachCrawler())
|
|
28354
|
-
return [];
|
|
28355
|
-
const ranked = await this.rankSemantic(trimmed, topK * POOL_FACTOR);
|
|
28356
|
-
if (ranked.length === 0)
|
|
28357
|
-
return [];
|
|
28358
|
-
const live = this.hydrate(ranked.map((entry) => entry.key));
|
|
28359
|
-
const hits = [];
|
|
28360
|
-
for (const entry of ranked) {
|
|
28361
|
-
const row = live.get(entry.key);
|
|
28362
|
-
if (row !== undefined)
|
|
28363
|
-
hits.push(this.toHit(row, entry.score));
|
|
28364
|
-
if (hits.length >= topK)
|
|
28365
|
-
break;
|
|
28366
|
-
}
|
|
28367
|
-
return hits;
|
|
28368
|
-
}
|
|
28369
|
-
async rankSemantic(query, pool) {
|
|
28370
|
-
const now = Date.now();
|
|
28371
|
-
let cached = this.lastSemantic;
|
|
28372
|
-
if (cached === undefined || cached.query !== query || now - cached.at > SEMANTIC_REUSE_MS) {
|
|
28373
|
-
const embedder = this.options.embedder;
|
|
28374
|
-
if (embedder === undefined)
|
|
28375
|
-
return [];
|
|
28376
|
-
cached = {
|
|
28377
|
-
query,
|
|
28378
|
-
at: now,
|
|
28379
|
-
vector: embedder.embed([query]).then((vectors) => vectors[0], () => {
|
|
28380
|
-
return;
|
|
28381
|
-
}),
|
|
28382
|
-
ranked: undefined
|
|
28383
|
-
};
|
|
28384
|
-
this.lastSemantic = cached;
|
|
28385
|
-
}
|
|
28386
|
-
const vector = await cached.vector;
|
|
28387
|
-
if (vector === undefined) {
|
|
28388
|
-
if (this.lastSemantic === cached)
|
|
28389
|
-
this.lastSemantic = undefined;
|
|
28390
|
-
return [];
|
|
28391
|
-
}
|
|
28392
|
-
if (cached.ranked === undefined || cached.ranked.pool < pool) {
|
|
28393
|
-
const wide = pool * POOL_FACTOR;
|
|
28394
|
-
cached.ranked = { pool: wide, keys: topKByCosine(vector, this.storedVectors(), wide) };
|
|
28395
|
-
}
|
|
28396
|
-
return cached.ranked.keys.slice(0, pool);
|
|
28392
|
+
if (options.instanceId !== undefined)
|
|
28393
|
+
validateMailcrawlInstanceId(options.instanceId);
|
|
28397
28394
|
}
|
|
28398
|
-
async
|
|
28399
|
-
|
|
28400
|
-
|
|
28401
|
-
|
|
28402
|
-
|
|
28403
|
-
|
|
28404
|
-
|
|
28405
|
-
|
|
28406
|
-
|
|
28407
|
-
|
|
28408
|
-
|
|
28409
|
-
const
|
|
28410
|
-
|
|
28411
|
-
|
|
28412
|
-
|
|
28413
|
-
|
|
28414
|
-
|
|
28415
|
-
break;
|
|
28395
|
+
async sync(signal) {
|
|
28396
|
+
const args = [
|
|
28397
|
+
"sync",
|
|
28398
|
+
"--json",
|
|
28399
|
+
...this.options.account === undefined ? [] : ["--account", this.options.account],
|
|
28400
|
+
...this.options.mailbox === undefined ? [] : ["--mailbox", this.options.mailbox],
|
|
28401
|
+
...this.options.backend === undefined ? [] : ["--backend", this.options.backend],
|
|
28402
|
+
...this.options.source === undefined ? [] : ["--source", this.options.source],
|
|
28403
|
+
...this.options.fixture === undefined ? [] : ["--fixture", this.options.fixture],
|
|
28404
|
+
...this.options.himalayaConfig === undefined ? [] : ["--himalaya-config", this.options.himalayaConfig]
|
|
28405
|
+
];
|
|
28406
|
+
const result = await this.run(args, signal, this.indexTimeoutMs());
|
|
28407
|
+
if (!result.ok)
|
|
28408
|
+
return failure3(result);
|
|
28409
|
+
const parsed = parseObject2(result.stdout);
|
|
28410
|
+
if (parsed === undefined || !hasAnyNumber(parsed, ["added", "updated", "deleted", "unchanged", "chunksAdded"])) {
|
|
28411
|
+
return failure3({ ...result, ok: false, reason: "invalid-output" });
|
|
28416
28412
|
}
|
|
28417
|
-
|
|
28413
|
+
const messages = number(parsed, "added") ?? number(parsed, "updated") ?? number(parsed, "unchanged") ?? 0;
|
|
28414
|
+
return ok4({
|
|
28415
|
+
messages,
|
|
28416
|
+
added: number(parsed, "added"),
|
|
28417
|
+
updated: number(parsed, "updated"),
|
|
28418
|
+
deleted: number(parsed, "deleted"),
|
|
28419
|
+
unchanged: number(parsed, "unchanged"),
|
|
28420
|
+
chunksAdded: number(parsed, "chunksAdded"),
|
|
28421
|
+
archiveRevision: string(parsed, "archiveRevision")
|
|
28422
|
+
}, result);
|
|
28418
28423
|
}
|
|
28419
|
-
async
|
|
28420
|
-
|
|
28421
|
-
|
|
28422
|
-
|
|
28423
|
-
|
|
28424
|
-
|
|
28425
|
-
|
|
28426
|
-
|
|
28427
|
-
|
|
28428
|
-
|
|
28429
|
-
|
|
28424
|
+
async search(mode, query, options) {
|
|
28425
|
+
if (query.trim().length === 0)
|
|
28426
|
+
return { ok: true, hits: [], stdout: "", stderr: "", code: 0 };
|
|
28427
|
+
const args = [
|
|
28428
|
+
"search",
|
|
28429
|
+
"--mode",
|
|
28430
|
+
mode === "keyword" ? "bm25" : mode,
|
|
28431
|
+
...this.options.account === undefined ? [] : ["--account", this.options.account],
|
|
28432
|
+
...this.options.mailbox === undefined ? [] : ["--mailbox", this.options.mailbox],
|
|
28433
|
+
"--limit",
|
|
28434
|
+
String(options?.topK ?? 20),
|
|
28435
|
+
"--json",
|
|
28436
|
+
query.trim()
|
|
28437
|
+
];
|
|
28438
|
+
const result = await this.run(args, options?.signal, this.options.timeoutMs ?? DEFAULT_SEARCH_TIMEOUT_MS2);
|
|
28439
|
+
if (!result.ok)
|
|
28440
|
+
return failure3(result);
|
|
28441
|
+
const parsed = parseJson3(result.stdout);
|
|
28442
|
+
if (parsed === undefined)
|
|
28443
|
+
return failure3({ ...result, ok: false, reason: "invalid-output" });
|
|
28444
|
+
const rows = Array.isArray(parsed) ? parsed : objectArray(parsed, ["hits", "results", "items"]);
|
|
28445
|
+
if (rows === undefined)
|
|
28446
|
+
return failure3({ ...result, ok: false, reason: "invalid-output" });
|
|
28447
|
+
const hits = rows.map((row, index) => normalizeHit2(row, mode, index));
|
|
28448
|
+
if (hits.some((hit) => hit === undefined)) {
|
|
28449
|
+
return failure3({ ...result, ok: false, reason: "invalid-output" });
|
|
28430
28450
|
}
|
|
28431
|
-
|
|
28432
|
-
|
|
28433
|
-
|
|
28434
|
-
|
|
28435
|
-
|
|
28436
|
-
|
|
28437
|
-
const embed = async (rows) => {
|
|
28438
|
-
const vectors = await embedder.embed(rows.map((row) => row.input));
|
|
28439
|
-
if (vectors.length !== rows.length) {
|
|
28440
|
-
throw new Error(`embedder returned ${vectors.length} vectors for ${rows.length} messages`);
|
|
28441
|
-
}
|
|
28442
|
-
const wrong = vectors.find((vector) => vector.length !== identity.dimension);
|
|
28443
|
-
if (wrong !== undefined) {
|
|
28444
|
-
throw new Error(`embedder returned a ${wrong.length}-dimension vector, expected ${identity.dimension}`);
|
|
28445
|
-
}
|
|
28446
|
-
return vectors;
|
|
28451
|
+
return {
|
|
28452
|
+
ok: true,
|
|
28453
|
+
hits,
|
|
28454
|
+
stdout: result.stdout,
|
|
28455
|
+
stderr: result.stderr,
|
|
28456
|
+
code: result.code ?? 0
|
|
28447
28457
|
};
|
|
28448
|
-
let embedded = 0;
|
|
28449
|
-
let skipped = 0;
|
|
28450
|
-
for (let offset = 0;offset < target; offset += batchSize) {
|
|
28451
|
-
const rows = [...this.hydrate(pending.slice(offset, Math.min(offset + batchSize, target))).values()];
|
|
28452
|
-
if (rows.length === 0)
|
|
28453
|
-
continue;
|
|
28454
|
-
try {
|
|
28455
|
-
this.store(rows, await embed(rows));
|
|
28456
|
-
embedded += rows.length;
|
|
28457
|
-
} catch (batchError) {
|
|
28458
|
-
const rejected = [];
|
|
28459
|
-
let lastError = batchError;
|
|
28460
|
-
for (const row of rows) {
|
|
28461
|
-
try {
|
|
28462
|
-
this.store([row], await embed([row]));
|
|
28463
|
-
embedded += 1;
|
|
28464
|
-
} catch (error) {
|
|
28465
|
-
rejected.push(row);
|
|
28466
|
-
lastError = error;
|
|
28467
|
-
}
|
|
28468
|
-
}
|
|
28469
|
-
if (rejected.length === 0)
|
|
28470
|
-
continue;
|
|
28471
|
-
if (embedded === 0)
|
|
28472
|
-
return { ok: false, message: errorMessage2(lastError) };
|
|
28473
|
-
this.store(rejected, undefined);
|
|
28474
|
-
skipped += rejected.length;
|
|
28475
|
-
}
|
|
28476
|
-
}
|
|
28477
|
-
return { ok: true, embedded, skipped, pruned, remaining: pending.length - embedded - skipped };
|
|
28478
|
-
}
|
|
28479
|
-
adoptIdentity(identity) {
|
|
28480
|
-
const serialized = JSON.stringify(identity);
|
|
28481
|
-
const current = this.db.prepare("select v from sidecar_meta where k = ?").get(IDENTITY_KEY);
|
|
28482
|
-
if (current?.v === serialized)
|
|
28483
|
-
return;
|
|
28484
|
-
this.transaction(() => {
|
|
28485
|
-
this.db.exec("delete from sidecar_vec");
|
|
28486
|
-
this.db.prepare("insert or replace into sidecar_meta (k, v) values (?, ?)").run(IDENTITY_KEY, serialized);
|
|
28487
|
-
});
|
|
28488
|
-
this.lastSemantic = undefined;
|
|
28489
28458
|
}
|
|
28490
|
-
|
|
28491
|
-
|
|
28492
|
-
const result = this.db.prepare(`delete from sidecar_vec where message_key not in (
|
|
28493
|
-
select ${descriptor.keyExpression} from ${descriptor.fromClause} where ${this.where}
|
|
28494
|
-
)`).run(...this.whereParams());
|
|
28495
|
-
if (Number(result.changes) > 0)
|
|
28496
|
-
this.lastSemantic = undefined;
|
|
28497
|
-
return Number(result.changes);
|
|
28459
|
+
indexTimeoutMs() {
|
|
28460
|
+
return this.options.indexTimeoutMs ?? this.options.timeoutMs ?? DEFAULT_INDEX_TIMEOUT_MS3;
|
|
28498
28461
|
}
|
|
28499
|
-
|
|
28500
|
-
const
|
|
28501
|
-
|
|
28502
|
-
|
|
28503
|
-
left join sidecar_vec v on v.message_key = ${descriptor.keyExpression}
|
|
28504
|
-
where ${this.where}`).iterate(...this.whereParams());
|
|
28505
|
-
const pending = [];
|
|
28506
|
-
for (const row of rows) {
|
|
28507
|
-
if (row.stored_hash !== this.toRow(row).hash)
|
|
28508
|
-
pending.push(String(row.k));
|
|
28462
|
+
async run(args, signal, timeoutMs) {
|
|
28463
|
+
const violation = findRemoteEmbeddingViolation({ ...process.env, ...this.options.env ?? {} });
|
|
28464
|
+
if (violation !== undefined) {
|
|
28465
|
+
return { ok: false, reason: "remote-embedding-rejected", stdout: "", stderr: "", code: null };
|
|
28509
28466
|
}
|
|
28510
|
-
|
|
28467
|
+
const dataDir = this.options.dataDir;
|
|
28468
|
+
if (dataDir !== undefined)
|
|
28469
|
+
ensurePrivateMailcrawlDataDir(dataDir);
|
|
28470
|
+
const env = {};
|
|
28471
|
+
for (const [key, value] of Object.entries(process.env)) {
|
|
28472
|
+
if (value !== undefined && (SAFE_ENV_KEYS2.has(key) || SAFE_MAILCRAWL_ENV_KEYS.has(key)))
|
|
28473
|
+
env[key] = value;
|
|
28474
|
+
}
|
|
28475
|
+
for (const [key, value] of Object.entries(this.options.env ?? {})) {
|
|
28476
|
+
if (value === undefined)
|
|
28477
|
+
delete env[key];
|
|
28478
|
+
else if (SAFE_ENV_KEYS2.has(key) || SAFE_MAILCRAWL_ENV_KEYS.has(key))
|
|
28479
|
+
env[key] = value;
|
|
28480
|
+
}
|
|
28481
|
+
if (dataDir !== undefined)
|
|
28482
|
+
env.MAILCRAWL_DATA_DIR = dataDir;
|
|
28483
|
+
return spawnMailcrawl(this.options.binaryPath ?? DEFAULT_BINARY4, args, env, this.options, signal, timeoutMs);
|
|
28511
28484
|
}
|
|
28512
|
-
|
|
28513
|
-
const
|
|
28514
|
-
|
|
28515
|
-
|
|
28516
|
-
|
|
28517
|
-
|
|
28518
|
-
|
|
28519
|
-
const byKey = new Map(rows.map((row) => [String(row.k), this.toRow(row)]));
|
|
28520
|
-
for (const key of keys) {
|
|
28521
|
-
const row = byKey.get(key);
|
|
28522
|
-
if (row !== undefined)
|
|
28523
|
-
out.set(key, row);
|
|
28485
|
+
async index(signal) {
|
|
28486
|
+
const result = await this.run(["index", "--json"], signal, this.indexTimeoutMs());
|
|
28487
|
+
if (!result.ok)
|
|
28488
|
+
return failure3(result);
|
|
28489
|
+
const parsed = parseObject2(result.stdout);
|
|
28490
|
+
if (parsed === undefined || number(parsed, "embedded") === undefined && number(parsed, "reused") === undefined && string(parsed, "generation") === undefined) {
|
|
28491
|
+
return failure3({ ...result, ok: false, reason: "invalid-output" });
|
|
28524
28492
|
}
|
|
28525
|
-
return
|
|
28493
|
+
return ok4({
|
|
28494
|
+
embedded: number(parsed, "embedded"),
|
|
28495
|
+
reused: number(parsed, "reused"),
|
|
28496
|
+
generation: string(parsed, "generation"),
|
|
28497
|
+
archiveRevision: string(parsed, "archiveRevision"),
|
|
28498
|
+
rebuilt: typeof parsed.rebuilt === "boolean" ? parsed.rebuilt : undefined,
|
|
28499
|
+
embedder: string(parsed, "embedder")
|
|
28500
|
+
}, result);
|
|
28526
28501
|
}
|
|
28527
|
-
|
|
28528
|
-
|
|
28529
|
-
|
|
28530
|
-
|
|
28531
|
-
|
|
28502
|
+
}
|
|
28503
|
+
function spawnMailcrawl(binary, args, env, options, signal, timeoutMs) {
|
|
28504
|
+
return new Promise((resolveResult) => {
|
|
28505
|
+
const portable = portableSpawnCommand(binary, args);
|
|
28506
|
+
const child = spawn15(portable.command, portable.args, {
|
|
28507
|
+
env,
|
|
28508
|
+
detached: process.platform !== "win32",
|
|
28509
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
28510
|
+
});
|
|
28511
|
+
let stdout = { text: "", bytes: 0, exceeded: false }, stderr = { text: "", bytes: 0, exceeded: false }, settled = false, reason;
|
|
28512
|
+
const max = options.maxBufferBytes ?? DEFAULT_MAX_BUFFER_BYTES4;
|
|
28513
|
+
const timer = setTimeout(() => {
|
|
28514
|
+
reason = "timeout";
|
|
28515
|
+
terminate4(child);
|
|
28516
|
+
}, timeoutMs);
|
|
28517
|
+
const abort = () => {
|
|
28518
|
+
reason = "aborted";
|
|
28519
|
+
terminate4(child);
|
|
28520
|
+
};
|
|
28521
|
+
if (signal?.aborted)
|
|
28522
|
+
abort();
|
|
28523
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
28524
|
+
child.stdout.setEncoding("utf8");
|
|
28525
|
+
child.stderr.setEncoding("utf8");
|
|
28526
|
+
child.stdout.on("data", (chunk) => {
|
|
28527
|
+
stdout = appendOutput(stdout, chunk, max);
|
|
28528
|
+
if (stdout.exceeded && reason === undefined) {
|
|
28529
|
+
reason = "stdout-too-large";
|
|
28530
|
+
terminate4(child);
|
|
28531
|
+
}
|
|
28532
|
+
});
|
|
28533
|
+
child.stderr.on("data", (chunk) => {
|
|
28534
|
+
stderr = appendOutput(stderr, chunk, max);
|
|
28535
|
+
if (stderr.exceeded && reason === undefined) {
|
|
28536
|
+
reason = "stderr-too-large";
|
|
28537
|
+
terminate4(child);
|
|
28532
28538
|
}
|
|
28533
28539
|
});
|
|
28534
|
-
|
|
28540
|
+
child.on("error", (error) => {
|
|
28541
|
+
if (settled)
|
|
28542
|
+
return;
|
|
28543
|
+
settled = true;
|
|
28544
|
+
clearTimeout(timer);
|
|
28545
|
+
signal?.removeEventListener("abort", abort);
|
|
28546
|
+
resolveResult({
|
|
28547
|
+
ok: false,
|
|
28548
|
+
reason: error.code === "ENOENT" ? "binary-missing" : "spawn-error",
|
|
28549
|
+
stdout: stdout.text,
|
|
28550
|
+
stderr: "",
|
|
28551
|
+
code: null
|
|
28552
|
+
});
|
|
28553
|
+
});
|
|
28554
|
+
child.on("close", (code) => {
|
|
28555
|
+
if (settled)
|
|
28556
|
+
return;
|
|
28557
|
+
settled = true;
|
|
28558
|
+
clearTimeout(timer);
|
|
28559
|
+
signal?.removeEventListener("abort", abort);
|
|
28560
|
+
resolveResult({
|
|
28561
|
+
ok: code === 0 && reason === undefined,
|
|
28562
|
+
...reason === undefined ? code === 0 ? {} : { reason: "nonzero-exit" } : { reason },
|
|
28563
|
+
stdout: stdout.text,
|
|
28564
|
+
stderr: stderr.text,
|
|
28565
|
+
code
|
|
28566
|
+
});
|
|
28567
|
+
});
|
|
28568
|
+
});
|
|
28569
|
+
}
|
|
28570
|
+
function terminate4(child) {
|
|
28571
|
+
if (child.killed)
|
|
28572
|
+
return;
|
|
28573
|
+
if (process.platform !== "win32" && child.pid !== undefined) {
|
|
28574
|
+
try {
|
|
28575
|
+
process.kill(-child.pid, "SIGTERM");
|
|
28576
|
+
return;
|
|
28577
|
+
} catch {}
|
|
28535
28578
|
}
|
|
28536
|
-
|
|
28537
|
-
|
|
28538
|
-
|
|
28539
|
-
|
|
28540
|
-
|
|
28541
|
-
|
|
28579
|
+
child.kill("SIGTERM");
|
|
28580
|
+
}
|
|
28581
|
+
function parseJson3(text) {
|
|
28582
|
+
try {
|
|
28583
|
+
return JSON.parse(text.trim());
|
|
28584
|
+
} catch {
|
|
28585
|
+
return;
|
|
28586
|
+
}
|
|
28587
|
+
}
|
|
28588
|
+
function parseObject2(text) {
|
|
28589
|
+
const value = parseJson3(text);
|
|
28590
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : undefined;
|
|
28591
|
+
}
|
|
28592
|
+
function objectArray(value, keys) {
|
|
28593
|
+
if (value === null || typeof value !== "object" || Array.isArray(value))
|
|
28594
|
+
return;
|
|
28595
|
+
for (const key of keys)
|
|
28596
|
+
if (Array.isArray(value[key]))
|
|
28597
|
+
return value[key];
|
|
28598
|
+
return;
|
|
28599
|
+
}
|
|
28600
|
+
function hasAnyNumber(value, keys) {
|
|
28601
|
+
return keys.some((key) => number(value, key) !== undefined);
|
|
28602
|
+
}
|
|
28603
|
+
function normalizeHit2(value, mode, index) {
|
|
28604
|
+
if (value === null || typeof value !== "object" || Array.isArray(value))
|
|
28605
|
+
return;
|
|
28606
|
+
const row = value;
|
|
28607
|
+
const text = (key) => typeof row[key] === "string" ? row[key] : "";
|
|
28608
|
+
const chunkId = text("chunkId") || text("chunk_id");
|
|
28609
|
+
const messageId = text("messageId") || text("message_id");
|
|
28610
|
+
const accountId = text("accountId") || text("account_id");
|
|
28611
|
+
const mailbox = text("mailbox");
|
|
28612
|
+
const snippet = text("snippet") || text("content") || text("text");
|
|
28613
|
+
if (!isSafeOpaqueId(chunkId) || !isSafeOpaqueId(messageId) || !snippet)
|
|
28614
|
+
return;
|
|
28615
|
+
const to = Array.isArray(row.to) ? row.to.filter((item) => typeof item === "string") : [];
|
|
28616
|
+
return {
|
|
28617
|
+
chunkId,
|
|
28618
|
+
messageId,
|
|
28619
|
+
threadId: text("threadId") || text("thread_id"),
|
|
28620
|
+
accountId,
|
|
28621
|
+
mailbox,
|
|
28622
|
+
subject: text("subject"),
|
|
28623
|
+
from: text("from"),
|
|
28624
|
+
to,
|
|
28625
|
+
date: text("date"),
|
|
28626
|
+
snippet,
|
|
28627
|
+
score: typeof row.score === "number" ? row.score : 1 / (index + 1),
|
|
28628
|
+
mode
|
|
28629
|
+
};
|
|
28630
|
+
}
|
|
28631
|
+
function number(value, key) {
|
|
28632
|
+
return typeof value[key] === "number" && Number.isFinite(value[key]) ? value[key] : undefined;
|
|
28633
|
+
}
|
|
28634
|
+
function string(value, key) {
|
|
28635
|
+
return typeof value[key] === "string" ? value[key] : undefined;
|
|
28636
|
+
}
|
|
28637
|
+
function ok4(data, result) {
|
|
28638
|
+
return { ok: true, data, stdout: result.stdout, stderr: result.stderr, code: result.code ?? 0 };
|
|
28639
|
+
}
|
|
28640
|
+
function failure3(result) {
|
|
28641
|
+
return {
|
|
28642
|
+
ok: false,
|
|
28643
|
+
reason: result.reason ?? "nonzero-exit",
|
|
28644
|
+
stdout: result.stdout,
|
|
28645
|
+
stderr: boundDiagnosticText6(result.stderr),
|
|
28646
|
+
code: result.code
|
|
28647
|
+
};
|
|
28648
|
+
}
|
|
28649
|
+
function boundDiagnosticText6(value) {
|
|
28650
|
+
if (value.length === 0)
|
|
28651
|
+
return "";
|
|
28652
|
+
return value.trim().slice(0, 4000);
|
|
28653
|
+
}
|
|
28654
|
+
function appendOutput(current, chunk, maxBytes) {
|
|
28655
|
+
if (current.exceeded)
|
|
28656
|
+
return current;
|
|
28657
|
+
const bytes = Buffer.from(chunk, "utf8");
|
|
28658
|
+
const remaining = Math.max(0, maxBytes - current.bytes);
|
|
28659
|
+
const prefix = truncateUtf8(chunk, remaining);
|
|
28660
|
+
const total = current.bytes + bytes.byteLength;
|
|
28661
|
+
return {
|
|
28662
|
+
text: current.text + prefix,
|
|
28663
|
+
bytes: Math.min(total, maxBytes),
|
|
28664
|
+
exceeded: total > maxBytes
|
|
28665
|
+
};
|
|
28666
|
+
}
|
|
28667
|
+
function isSafeOpaqueId(value) {
|
|
28668
|
+
return value.length > 0 && value !== "." && value !== ".." && !/[\\/#\u0000-\u001f\u007f]/u.test(value);
|
|
28669
|
+
}
|
|
28670
|
+
function findRemoteEmbeddingViolation(env) {
|
|
28671
|
+
for (const [key, value] of Object.entries(env)) {
|
|
28672
|
+
if (value === undefined)
|
|
28673
|
+
continue;
|
|
28674
|
+
const lower = key.toLowerCase();
|
|
28675
|
+
if (REMOTE_EMBEDDING_ENV_KEYS.has(lower))
|
|
28676
|
+
return key;
|
|
28677
|
+
if (lower === "mailcrawl_embedder" || lower === "mailcrawl_embed_url") {
|
|
28678
|
+
if (isRemoteHttpEndpoint(value))
|
|
28679
|
+
return key;
|
|
28542
28680
|
}
|
|
28543
28681
|
}
|
|
28544
|
-
|
|
28545
|
-
|
|
28682
|
+
return;
|
|
28683
|
+
}
|
|
28684
|
+
function isRemoteHttpEndpoint(value) {
|
|
28685
|
+
const trimmed = value.trim();
|
|
28686
|
+
if (!/^https?:\/\//iu.test(trimmed))
|
|
28687
|
+
return false;
|
|
28688
|
+
try {
|
|
28689
|
+
return !LOOPBACK_HOSTS2.has(new URL(trimmed).hostname);
|
|
28690
|
+
} catch {
|
|
28691
|
+
return true;
|
|
28692
|
+
}
|
|
28693
|
+
}
|
|
28694
|
+
function truncateUtf8(value, maxBytes) {
|
|
28695
|
+
if (Buffer.byteLength(value) <= maxBytes)
|
|
28696
|
+
return value;
|
|
28697
|
+
const decoder = new TextDecoder("utf-8", { fatal: true });
|
|
28698
|
+
const bytes = Buffer.from(value, "utf8");
|
|
28699
|
+
for (let end = Math.min(maxBytes, bytes.length);end >= 0; end -= 1) {
|
|
28546
28700
|
try {
|
|
28547
|
-
|
|
28548
|
-
|
|
28549
|
-
} catch (error) {
|
|
28550
|
-
this.db.exec("rollback");
|
|
28551
|
-
throw error;
|
|
28552
|
-
}
|
|
28701
|
+
return decoder.decode(bytes.subarray(0, end));
|
|
28702
|
+
} catch {}
|
|
28553
28703
|
}
|
|
28554
|
-
|
|
28555
|
-
|
|
28556
|
-
|
|
28557
|
-
|
|
28558
|
-
|
|
28559
|
-
|
|
28560
|
-
|
|
28561
|
-
|
|
28562
|
-
|
|
28563
|
-
|
|
28564
|
-
|
|
28565
|
-
|
|
28566
|
-
|
|
28567
|
-
|
|
28568
|
-
|
|
28569
|
-
|
|
28570
|
-
|
|
28571
|
-
|
|
28572
|
-
|
|
28573
|
-
|
|
28574
|
-
|
|
28704
|
+
return "";
|
|
28705
|
+
}
|
|
28706
|
+
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;
|
|
28707
|
+
var init_client9 = __esm(() => {
|
|
28708
|
+
init_portable_spawn();
|
|
28709
|
+
init_config3();
|
|
28710
|
+
LOOPBACK_HOSTS2 = new Set(["127.0.0.1", "localhost", "[::1]", "::1"]);
|
|
28711
|
+
SAFE_ENV_KEYS2 = new Set(["HOME", "LANG", "LC_ALL", "PATH", "TMPDIR", "TMP", "TEMP", "NO_COLOR"]);
|
|
28712
|
+
SAFE_MAILCRAWL_ENV_KEYS = new Set([
|
|
28713
|
+
"MAILCRAWL_DATA_DIR",
|
|
28714
|
+
"MAILCRAWL_EMBEDDER",
|
|
28715
|
+
"MAILCRAWL_EMBEDDER_PROVIDER",
|
|
28716
|
+
"MAILCRAWL_EMBED_URL",
|
|
28717
|
+
"MAILCRAWL_EMBED_MODEL",
|
|
28718
|
+
"MAILCRAWL_EMBED_DIM",
|
|
28719
|
+
"MAILCRAWL_EMBED_TIMEOUT",
|
|
28720
|
+
"MAILCRAWL_EMBED_BATCH_SIZE",
|
|
28721
|
+
"MAILCRAWL_QUERY_PREFIX",
|
|
28722
|
+
"MAILCRAWL_PASSAGE_PREFIX",
|
|
28723
|
+
"MAILCRAWL_NATIVE_MODEL",
|
|
28724
|
+
"MAILCRAWL_NATIVE_DEVICE",
|
|
28725
|
+
"MAILCRAWL_NATIVE_DTYPE",
|
|
28726
|
+
"MAILCRAWL_KIWI_MODEL",
|
|
28727
|
+
"MAILCRAWL_JA_HELPER",
|
|
28728
|
+
"MAILCRAWL_ZH_HELPER"
|
|
28729
|
+
]);
|
|
28730
|
+
REMOTE_EMBEDDING_ENV_KEYS = new Set([
|
|
28731
|
+
"mailcrawl_embedder_base_url",
|
|
28732
|
+
"mailcrawl_embedding_base_url",
|
|
28733
|
+
"mailcrawl_allow_remote_embeddings"
|
|
28734
|
+
]);
|
|
28735
|
+
});
|
|
28736
|
+
|
|
28737
|
+
// src/datasource/skills/mailcrawl/methods.ts
|
|
28738
|
+
class MailcrawlMethod {
|
|
28739
|
+
client;
|
|
28740
|
+
mode;
|
|
28741
|
+
instanceId;
|
|
28742
|
+
tags;
|
|
28743
|
+
account;
|
|
28744
|
+
mailbox;
|
|
28745
|
+
constructor(client, mode, instanceId, tags, account, mailbox) {
|
|
28746
|
+
this.client = client;
|
|
28747
|
+
this.mode = mode;
|
|
28748
|
+
this.instanceId = instanceId;
|
|
28749
|
+
this.tags = tags;
|
|
28750
|
+
this.account = account;
|
|
28751
|
+
this.mailbox = mailbox;
|
|
28575
28752
|
}
|
|
28576
|
-
|
|
28753
|
+
describe() {
|
|
28577
28754
|
return {
|
|
28578
|
-
|
|
28579
|
-
|
|
28580
|
-
|
|
28581
|
-
|
|
28582
|
-
|
|
28583
|
-
|
|
28755
|
+
name: `mailcrawl-${this.mode}`,
|
|
28756
|
+
type: this.mode === "bm25" ? "bm25" : this.mode === "semantic" ? "vector" : "hybrid",
|
|
28757
|
+
description: `${this.mode} retrieval over archived email via the external mailcrawl CLI`,
|
|
28758
|
+
status: "active",
|
|
28759
|
+
capabilities: ["email", this.mode, "scoped", "external-cli", "path-opaque-sources"],
|
|
28760
|
+
datasourceId: "mailcrawl",
|
|
28761
|
+
tags: this.tags
|
|
28584
28762
|
};
|
|
28585
28763
|
}
|
|
28764
|
+
async retrieve(query, options) {
|
|
28765
|
+
if (!query.trim())
|
|
28766
|
+
return [];
|
|
28767
|
+
const result = await this.client.search(this.mode, query, {
|
|
28768
|
+
topK: options.topK,
|
|
28769
|
+
signal: options.signal
|
|
28770
|
+
});
|
|
28771
|
+
if (!result.ok)
|
|
28772
|
+
throw datasourceCliError("mailcrawl", `search --mode ${this.mode}`, result);
|
|
28773
|
+
return result.hits.filter((hit) => {
|
|
28774
|
+
if (this.account !== undefined && hit.accountId !== this.account)
|
|
28775
|
+
return false;
|
|
28776
|
+
if (this.mailbox !== undefined && hit.mailbox !== this.mailbox)
|
|
28777
|
+
return false;
|
|
28778
|
+
const source = datasourceSourcePath("mailcrawl", this.instanceId, hit.chunkId);
|
|
28779
|
+
return matchesDatasourceScope(source, options.scope);
|
|
28780
|
+
}).map((hit) => {
|
|
28781
|
+
const source = datasourceSourcePath("mailcrawl", this.instanceId, hit.chunkId);
|
|
28782
|
+
return {
|
|
28783
|
+
id: `mailcrawl:${this.instanceId}:${hit.chunkId}`,
|
|
28784
|
+
content: hit.snippet,
|
|
28785
|
+
source,
|
|
28786
|
+
score: hit.score,
|
|
28787
|
+
metadata: {
|
|
28788
|
+
method: `mailcrawl-${this.mode}`,
|
|
28789
|
+
datasourceId: "mailcrawl",
|
|
28790
|
+
instanceId: this.instanceId,
|
|
28791
|
+
mode: this.mode,
|
|
28792
|
+
chunkId: hit.chunkId,
|
|
28793
|
+
messageId: hit.messageId,
|
|
28794
|
+
threadId: hit.threadId,
|
|
28795
|
+
accountId: hit.accountId,
|
|
28796
|
+
mailbox: hit.mailbox,
|
|
28797
|
+
subject: hit.subject,
|
|
28798
|
+
from: hit.from,
|
|
28799
|
+
to: hit.to,
|
|
28800
|
+
date: hit.date
|
|
28801
|
+
}
|
|
28802
|
+
};
|
|
28803
|
+
}).slice(0, options.topK ?? 20);
|
|
28804
|
+
}
|
|
28586
28805
|
}
|
|
28587
|
-
|
|
28588
|
-
|
|
28589
|
-
|
|
28590
|
-
|
|
28591
|
-
|
|
28592
|
-
|
|
28593
|
-
}
|
|
28594
|
-
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";
|
|
28595
|
-
var init_sidecar = () => {};
|
|
28596
|
-
|
|
28597
|
-
// src/datasource/sidecar/methods.ts
|
|
28598
|
-
function sidecarMethodName(datasourceId, mode) {
|
|
28599
|
-
return mode === "bm25" ? `${datasourceId}-sidecar-bm25` : `${datasourceId}-${mode}`;
|
|
28600
|
-
}
|
|
28806
|
+
var modes, MAILCRAWL_MODES;
|
|
28807
|
+
var init_methods5 = __esm(() => {
|
|
28808
|
+
init_errors4();
|
|
28809
|
+
init_scope2();
|
|
28810
|
+
modes = ["bm25", "semantic", "hybrid"];
|
|
28811
|
+
MAILCRAWL_MODES = modes;
|
|
28812
|
+
});
|
|
28601
28813
|
|
|
28602
|
-
|
|
28603
|
-
|
|
28604
|
-
|
|
28605
|
-
|
|
28814
|
+
// src/datasource/skills/mailcrawl/skill.ts
|
|
28815
|
+
class MailcrawlSkill {
|
|
28816
|
+
client;
|
|
28817
|
+
instanceId;
|
|
28818
|
+
instances;
|
|
28819
|
+
interval;
|
|
28820
|
+
tags;
|
|
28821
|
+
account;
|
|
28822
|
+
mailbox;
|
|
28823
|
+
lastIndexedAt;
|
|
28824
|
+
constructor(options = {}) {
|
|
28825
|
+
const connectorOptions = options.connectorOptions ?? options;
|
|
28826
|
+
this.instanceId = options.instanceId ?? "default";
|
|
28827
|
+
this.instances = options.instances?.length ? options.instances : [this.instanceId];
|
|
28828
|
+
validateMailcrawlInstanceId(this.instanceId);
|
|
28829
|
+
for (const instance of this.instances)
|
|
28830
|
+
validateMailcrawlInstanceId(instance);
|
|
28831
|
+
this.interval = options.pollingIntervalMs ?? 15 * 60 * 1000;
|
|
28832
|
+
this.tags = options.tags ?? TAGS;
|
|
28833
|
+
this.account = connectorOptions.account;
|
|
28834
|
+
this.mailbox = connectorOptions.mailbox;
|
|
28835
|
+
this.client = options.client ?? new MailcrawlClient({ ...connectorOptions, instanceId: this.instanceId });
|
|
28606
28836
|
}
|
|
28607
28837
|
describe() {
|
|
28608
|
-
const { datasourceId, skillType, mode, tags } = this.options;
|
|
28609
|
-
const { type, text, capabilities } = DESCRIPTIONS[mode];
|
|
28610
28838
|
return {
|
|
28611
|
-
name:
|
|
28612
|
-
|
|
28613
|
-
|
|
28839
|
+
name: "mailcrawl",
|
|
28840
|
+
id: "mailcrawl",
|
|
28841
|
+
type: "mailcrawl-archive",
|
|
28842
|
+
description: "Local email archive datasource via the external mailcrawl CLI",
|
|
28843
|
+
capabilities: ["email", "external-cli", "polling", "incremental", "bm25", "semantic", "hybrid"],
|
|
28844
|
+
tags: this.tags,
|
|
28614
28845
|
status: "active",
|
|
28615
|
-
|
|
28616
|
-
datasourceId,
|
|
28617
|
-
|
|
28846
|
+
requiresExternalCli: true,
|
|
28847
|
+
datasourceId: "mailcrawl",
|
|
28848
|
+
instanceId: this.instanceId,
|
|
28849
|
+
instances: this.instances
|
|
28618
28850
|
};
|
|
28619
28851
|
}
|
|
28620
|
-
|
|
28621
|
-
|
|
28622
|
-
|
|
28623
|
-
|
|
28624
|
-
|
|
28625
|
-
|
|
28626
|
-
|
|
28627
|
-
|
|
28628
|
-
|
|
28629
|
-
const result = this.
|
|
28630
|
-
if (result
|
|
28631
|
-
|
|
28852
|
+
polling() {
|
|
28853
|
+
return {
|
|
28854
|
+
mode: this.interval > 0 ? "poll" : "none",
|
|
28855
|
+
...this.interval > 0 ? { intervalMs: this.interval } : {},
|
|
28856
|
+
lastIndexedAt: this.lastIndexedAt
|
|
28857
|
+
};
|
|
28858
|
+
}
|
|
28859
|
+
async index(options) {
|
|
28860
|
+
try {
|
|
28861
|
+
const result = await this.client.sync(options?.signal);
|
|
28862
|
+
if (!result.ok) {
|
|
28863
|
+
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);
|
|
28864
|
+
}
|
|
28865
|
+
const indexed = await this.client.index(options?.signal);
|
|
28866
|
+
const diagnostics = [];
|
|
28867
|
+
if (!indexed.ok) {
|
|
28868
|
+
diagnostics.push({
|
|
28869
|
+
code: indexed.reason === "remote-embedding-rejected" ? "datasource-embedding-egress-rejected" : "datasource-index-failed",
|
|
28870
|
+
severity: "warning",
|
|
28871
|
+
message: `mailcrawl semantic index unavailable (${indexed.reason}); BM25 remains available`,
|
|
28872
|
+
instanceId: this.instanceId,
|
|
28873
|
+
source: "mailcrawl"
|
|
28874
|
+
});
|
|
28875
|
+
}
|
|
28876
|
+
this.lastIndexedAt = Date.now();
|
|
28877
|
+
return {
|
|
28878
|
+
ok: true,
|
|
28879
|
+
instanceId: this.instanceId,
|
|
28880
|
+
skill: "mailcrawl",
|
|
28881
|
+
chunkCount: result.data.chunksAdded ?? result.data.messages,
|
|
28882
|
+
indexedAt: this.lastIndexedAt,
|
|
28883
|
+
diagnostics
|
|
28884
|
+
};
|
|
28885
|
+
} catch {
|
|
28886
|
+
return this.fail("datasource-unavailable", "mailcrawl sync failed unexpectedly");
|
|
28632
28887
|
}
|
|
28633
|
-
return results;
|
|
28634
28888
|
}
|
|
28635
|
-
|
|
28636
|
-
|
|
28637
|
-
|
|
28638
|
-
|
|
28639
|
-
|
|
28640
|
-
|
|
28641
|
-
|
|
28889
|
+
retrievalMethods() {
|
|
28890
|
+
return [
|
|
28891
|
+
new MailcrawlMethod(this.client, "bm25", this.instanceId, this.tags, this.account, this.mailbox),
|
|
28892
|
+
new MailcrawlMethod(this.client, "semantic", this.instanceId, this.tags, this.account, this.mailbox),
|
|
28893
|
+
new MailcrawlMethod(this.client, "hybrid", this.instanceId, this.tags, this.account, this.mailbox)
|
|
28894
|
+
];
|
|
28895
|
+
}
|
|
28896
|
+
describeSources() {
|
|
28897
|
+
return this.instances.map((instanceId) => ({
|
|
28898
|
+
source: datasourceSourcePath("mailcrawl", instanceId),
|
|
28899
|
+
datasourceId: "mailcrawl",
|
|
28900
|
+
skill: "mailcrawl",
|
|
28901
|
+
instanceId,
|
|
28902
|
+
contentType: "email",
|
|
28903
|
+
metadata: { datasourceId: "mailcrawl", instanceId, tags: this.tags }
|
|
28904
|
+
}));
|
|
28905
|
+
}
|
|
28906
|
+
skillManifest() {
|
|
28907
|
+
const scopes = this.instances.map((id) => `- \`${datasourceSourcePath("mailcrawl", id)}\``).join(`
|
|
28908
|
+
`);
|
|
28642
28909
|
return {
|
|
28643
|
-
|
|
28644
|
-
|
|
28645
|
-
|
|
28646
|
-
|
|
28647
|
-
|
|
28648
|
-
|
|
28649
|
-
|
|
28650
|
-
|
|
28651
|
-
|
|
28652
|
-
|
|
28653
|
-
|
|
28654
|
-
|
|
28655
|
-
|
|
28656
|
-
|
|
28910
|
+
name: "datasource-mailcrawl",
|
|
28911
|
+
description: "Search local email synchronized and indexed by mailcrawl.",
|
|
28912
|
+
content: `# mailcrawl email datasource
|
|
28913
|
+
|
|
28914
|
+
Use this skill for authorized local email retrieval. AutoRAG invokes the external \`mailcrawl\` CLI and never opens its SQLite archive directly.
|
|
28915
|
+
|
|
28916
|
+
## Indexing
|
|
28917
|
+
Indexing is server-managed and incremental; do not trigger sync yourself.
|
|
28918
|
+
|
|
28919
|
+
## Search
|
|
28920
|
+
Call the dedicated \`${datasourceSearchToolName("mailcrawl")}\` tool with a natural-language query. Available scopes:
|
|
28921
|
+
${scopes}
|
|
28922
|
+
|
|
28923
|
+
The selectable retrieval modes are BM25, semantic, and hybrid. Scope arguments can only narrow trusted access. Source identifiers remain opaque and hierarchical.
|
|
28924
|
+
|
|
28925
|
+
## Native CLI
|
|
28926
|
+
The external \`mailcrawl\` CLI owns the archive and index. When the dedicated tool cannot express what you need, call mailcrawl directly through \`bash\`:
|
|
28927
|
+
- \`mailcrawl search --mode hybrid --limit 20 --json "<query>"\` (\`bm25\` and \`semantic\` modes also available; \`--account\` / \`--mailbox\` narrow the archive)
|
|
28928
|
+
Never pass datasource virtual paths (\`/mailcrawl/...\`) to bash; they are not OS paths.`
|
|
28929
|
+
};
|
|
28930
|
+
}
|
|
28931
|
+
fail(code, message) {
|
|
28932
|
+
return {
|
|
28933
|
+
ok: false,
|
|
28934
|
+
instanceId: this.instanceId,
|
|
28935
|
+
skill: "mailcrawl",
|
|
28936
|
+
indexedAt: Date.now(),
|
|
28937
|
+
diagnostics: [
|
|
28938
|
+
{
|
|
28939
|
+
code,
|
|
28940
|
+
severity: code === "datasource-unavailable" ? "warning" : "error",
|
|
28941
|
+
message: `mailcrawl ${message}`,
|
|
28942
|
+
instanceId: this.instanceId,
|
|
28943
|
+
source: "mailcrawl"
|
|
28944
|
+
}
|
|
28945
|
+
],
|
|
28946
|
+
error: message,
|
|
28947
|
+
code,
|
|
28948
|
+
message
|
|
28657
28949
|
};
|
|
28658
28950
|
}
|
|
28659
28951
|
}
|
|
28660
|
-
var
|
|
28661
|
-
var
|
|
28952
|
+
var TAGS;
|
|
28953
|
+
var init_skill9 = __esm(() => {
|
|
28662
28954
|
init_scope2();
|
|
28663
|
-
|
|
28664
|
-
|
|
28665
|
-
|
|
28666
|
-
|
|
28667
|
-
|
|
28668
|
-
|
|
28669
|
-
|
|
28670
|
-
|
|
28671
|
-
|
|
28955
|
+
init_client9();
|
|
28956
|
+
init_config3();
|
|
28957
|
+
init_methods5();
|
|
28958
|
+
TAGS = ["mailcrawl", "email", "pii"];
|
|
28959
|
+
});
|
|
28960
|
+
|
|
28961
|
+
// src/datasource/skills/mailcrawl/index.ts
|
|
28962
|
+
var init_mailcrawl = __esm(() => {
|
|
28963
|
+
init_client9();
|
|
28964
|
+
init_methods5();
|
|
28965
|
+
init_skill9();
|
|
28672
28966
|
});
|
|
28673
28967
|
|
|
28674
28968
|
// src/datasource/crawler-skill.ts
|
|
@@ -28951,7 +29245,7 @@ var init_crawler_skill = __esm(() => {
|
|
|
28951
29245
|
init_errors4();
|
|
28952
29246
|
init_scope2();
|
|
28953
29247
|
init_sidecar();
|
|
28954
|
-
|
|
29248
|
+
init_methods4();
|
|
28955
29249
|
DEFAULT_POLLING_INTERVAL_MS4 = 15 * 60 * 1000;
|
|
28956
29250
|
});
|
|
28957
29251
|
|
|
@@ -30740,14 +31034,27 @@ var init_factory = __esm(() => {
|
|
|
30740
31034
|
...config.channels?.ids !== undefined ? { chatIds: config.channels.ids } : {},
|
|
30741
31035
|
...larkConnectorOptions(config.connector)
|
|
30742
31036
|
}),
|
|
30743
|
-
kakao: (config,
|
|
30744
|
-
|
|
30745
|
-
|
|
30746
|
-
|
|
30747
|
-
|
|
30748
|
-
|
|
30749
|
-
|
|
30750
|
-
|
|
31037
|
+
kakao: (config, workspaceRoot, registrationName) => {
|
|
31038
|
+
const connector = config.connector ?? {};
|
|
31039
|
+
const dataDir = typeof connector.workspacePath === "string" && connector.workspacePath.length > 0 ? connector.workspacePath : join39(homedir4(), "Library", "Application Support", "katok");
|
|
31040
|
+
const sidecar = resolveSidecarOptions({
|
|
31041
|
+
descriptor: CRAWLER_DESCRIPTORS.kakao,
|
|
31042
|
+
connector: config.connector,
|
|
31043
|
+
workspaceRoot,
|
|
31044
|
+
registrationName,
|
|
31045
|
+
instanceId: config.instanceId ?? "default",
|
|
31046
|
+
defaultCrawlerDb: join39(dataDir, "archive.sqlite3")
|
|
31047
|
+
});
|
|
31048
|
+
return new LazykatokSkill({
|
|
31049
|
+
client: new LazykatokClient(connector),
|
|
31050
|
+
...config.instanceId !== undefined ? { instanceId: config.instanceId } : {},
|
|
31051
|
+
...config.pollingIntervalMs !== undefined ? { pollingIntervalMs: config.pollingIntervalMs } : {},
|
|
31052
|
+
...config.tags !== undefined ? { tags: config.tags } : {},
|
|
31053
|
+
channelIds: config.channels?.ids,
|
|
31054
|
+
channelNames: config.channels?.names,
|
|
31055
|
+
...sidecar === undefined ? {} : { sidecar }
|
|
31056
|
+
});
|
|
31057
|
+
},
|
|
30751
31058
|
github: (config, workspaceRoot, registrationName) => new GitHubSkill({
|
|
30752
31059
|
...common(config, workspaceRoot),
|
|
30753
31060
|
skillName: registrationName,
|
|
@@ -30893,7 +31200,7 @@ function migrateLegacyConfig(configPath, legacyPath) {
|
|
|
30893
31200
|
const legacy = readConfigFile(legacyPath, true);
|
|
30894
31201
|
const legacyBytes = readFileSync25(legacyPath);
|
|
30895
31202
|
const migrated = normalizeLegacyConfigPaths(legacy ?? {}, dirname26(legacyPath));
|
|
30896
|
-
const migratedPathsUnchanged = legacy?.workspacePath === migrated.workspacePath && legacy?.memoryPath === migrated.memoryPath && JSON.stringify(legacy?.searchPaths) === JSON.stringify(migrated.searchPaths);
|
|
31203
|
+
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);
|
|
30897
31204
|
const migratedBytes = migratedPathsUnchanged ? legacyBytes : `${JSON.stringify(migrated, null, 2)}
|
|
30898
31205
|
`;
|
|
30899
31206
|
mkdirSync25(dirname26(configPath), { recursive: true });
|
|
@@ -30931,15 +31238,25 @@ function resolveConfigFileReadOnly(configPath, explicit, legacyPath) {
|
|
|
30931
31238
|
function resolveSearchPaths(searchPaths, origin) {
|
|
30932
31239
|
return searchPaths.map((searchPath) => resolvePersistedPath(searchPath, origin));
|
|
30933
31240
|
}
|
|
31241
|
+
function resolveExcludePaths(value, origin) {
|
|
31242
|
+
if (value === undefined)
|
|
31243
|
+
return;
|
|
31244
|
+
if (!Array.isArray(value) || value.some((path) => typeof path !== "string" || path.length === 0)) {
|
|
31245
|
+
throw new ConfigError("Config field 'excludePaths' must be a non-empty string array");
|
|
31246
|
+
}
|
|
31247
|
+
return resolveSearchPaths(value, origin);
|
|
31248
|
+
}
|
|
30934
31249
|
function resolvePersistedPath(path, origin) {
|
|
30935
31250
|
return isAbsolute10(path) ? path : resolve16(origin, path);
|
|
30936
31251
|
}
|
|
30937
31252
|
function normalizeLegacyConfigPaths(partial, origin) {
|
|
30938
31253
|
const workspacePath = resolvePersistedPath(partial.workspacePath ?? ".", origin);
|
|
31254
|
+
const excludePaths = resolveExcludePaths(partial.excludePaths, workspacePath);
|
|
30939
31255
|
return {
|
|
30940
31256
|
...partial,
|
|
30941
31257
|
workspacePath,
|
|
30942
31258
|
...partial.searchPaths === undefined ? {} : { searchPaths: resolveSearchPaths(partial.searchPaths, workspacePath) },
|
|
31259
|
+
...excludePaths === undefined ? {} : { excludePaths },
|
|
30943
31260
|
...partial.memoryPath === undefined ? {} : { memoryPath: resolvePersistedPath(partial.memoryPath, workspacePath) }
|
|
30944
31261
|
};
|
|
30945
31262
|
}
|
|
@@ -31394,10 +31711,7 @@ function resolveConfig(input) {
|
|
|
31394
31711
|
const configOrigin = dirname26(resolve16(configPath));
|
|
31395
31712
|
const fileWorkspacePath = typeof file.workspacePath === "string" ? resolvePersistedPath(file.workspacePath, configOrigin) : undefined;
|
|
31396
31713
|
const fileSearchPaths = file.searchPaths ? resolveSearchPaths(file.searchPaths, fileWorkspacePath ?? configOrigin) : undefined;
|
|
31397
|
-
|
|
31398
|
-
throw new ConfigError("Config field 'excludePaths' must be a non-empty string array");
|
|
31399
|
-
}
|
|
31400
|
-
const fileExcludePaths = file.excludePaths ? resolveSearchPaths(file.excludePaths, fileWorkspacePath ?? configOrigin) : undefined;
|
|
31714
|
+
const fileExcludePaths = resolveExcludePaths(file.excludePaths, fileWorkspacePath ?? configOrigin);
|
|
31401
31715
|
const searchPaths = flagSearchPaths ?? envSearchPaths ?? fileSearchPaths ?? defaultSearchPaths;
|
|
31402
31716
|
const flagWorkspacePath = flagString(flags, "workspace");
|
|
31403
31717
|
const envWorkspacePath = envString(env, "AUTORAG_WORKSPACE");
|
|
@@ -32133,7 +32447,7 @@ function writeDefaultConfig(path, partial, opts = {}) {
|
|
|
32133
32447
|
full.dupey = partial.dupey ?? { enabled: true };
|
|
32134
32448
|
full.excludeExactDuplicates = partial.excludeExactDuplicates ?? true;
|
|
32135
32449
|
if (partial.excludePaths !== undefined)
|
|
32136
|
-
full.excludePaths =
|
|
32450
|
+
full.excludePaths = resolveExcludePaths(partial.excludePaths, workspacePath);
|
|
32137
32451
|
if (partial.limits !== undefined)
|
|
32138
32452
|
full.limits = normalizeLimitsConfig(partial.limits);
|
|
32139
32453
|
if (partial.refresh !== undefined)
|
|
@@ -32834,7 +33148,7 @@ var init_results = __esm(() => {
|
|
|
32834
33148
|
confidence: Type18.Optional(Type18.Number({ description: "Evidence confidence, 0..1", minimum: 0, maximum: 1 }))
|
|
32835
33149
|
});
|
|
32836
33150
|
answerSchema = Type18.String({
|
|
32837
|
-
description: `Answer for the caller.
|
|
33151
|
+
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}`
|
|
32838
33152
|
});
|
|
32839
33153
|
warningsSchema = Type18.Optional(Type18.Array(Type18.String(), { description: "Optional warnings about this result set" }));
|
|
32840
33154
|
curatedResultFields = {
|