@autorag/librarian 2.10.3 → 2.10.4

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