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