@autorag/librarian 2.10.3 → 2.10.5

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/index.js CHANGED
@@ -2109,6 +2109,25 @@ function errorMessage(error) {
2109
2109
  var MANIFEST_FILENAME = "manifest.json";
2110
2110
  var init_bundle = () => {};
2111
2111
 
2112
+ // src/filesystem/path-exclusions.ts
2113
+ import { posix as posix2, win32 as win322 } from "node:path";
2114
+ function createPathExclusionMatcher(excludedPaths, platform = process.platform) {
2115
+ const paths = platform === "win32" ? win322 : posix2;
2116
+ const normalize = (path) => {
2117
+ const absolute = paths.resolve(path);
2118
+ return platform === "win32" ? absolute.toLowerCase() : absolute;
2119
+ };
2120
+ const excluded = excludedPaths.map((path) => {
2121
+ const absolute = normalize(path);
2122
+ return { absolute, prefix: absolute.endsWith(paths.sep) ? absolute : `${absolute}${paths.sep}` };
2123
+ });
2124
+ return (path) => {
2125
+ const absolute = normalize(path);
2126
+ return excluded.some((entry) => absolute === entry.absolute || absolute.startsWith(entry.prefix));
2127
+ };
2128
+ }
2129
+ var init_path_exclusions = () => {};
2130
+
2112
2131
  // src/everything/client.ts
2113
2132
  import { spawn as spawn2 } from "node:child_process";
2114
2133
  import { createHash as createHash4 } from "node:crypto";
@@ -2226,6 +2245,7 @@ class EverythingClient {
2226
2245
  run;
2227
2246
  launch;
2228
2247
  stateDir;
2248
+ isExcludedPath;
2229
2249
  binaries;
2230
2250
  operation = Promise.resolve();
2231
2251
  constructor(options) {
@@ -2235,6 +2255,7 @@ class EverythingClient {
2235
2255
  this.run = options.run ?? runProcess;
2236
2256
  this.launch = options.launch ?? launchDetached;
2237
2257
  this.stateDir = join6(options.root, ".autorag", "everything");
2258
+ this.isExcludedPath = createPathExclusionMatcher(options.excludeFolders ?? [], "win32");
2238
2259
  }
2239
2260
  isSupported() {
2240
2261
  return this.platform === "win32";
@@ -2254,6 +2275,9 @@ class EverythingClient {
2254
2275
  }
2255
2276
  const es = { ok: true, esPath: binaries.esPath };
2256
2277
  const timeoutMs = this.options.timeoutMs ?? DEFAULT_TIMEOUT_MS2;
2278
+ if ((this.options.excludeFolders?.length ?? 0) > 0) {
2279
+ return this.searchWithExclusions(es.esPath, request, timeoutMs);
2280
+ }
2257
2281
  const args = buildEverythingSearchArgs(this.instanceName, request, timeoutMs);
2258
2282
  const result = await this.run(es.esPath, args, timeoutMs + 5000);
2259
2283
  if (result.code !== 0)
@@ -2265,6 +2289,50 @@ class EverythingClient {
2265
2289
  }
2266
2290
  });
2267
2291
  }
2292
+ async searchWithExclusions(esPath, request, timeoutMs) {
2293
+ const deadline = Date.now() + timeoutMs;
2294
+ const offset = request.offset ?? 0;
2295
+ const maxResults = request.maxResults ?? DEFAULT_MAX_RESULTS;
2296
+ const results = [];
2297
+ let skipped = 0;
2298
+ let nativeOffset = 0;
2299
+ const timedOut = () => ({
2300
+ ok: false,
2301
+ reason: "search-failed",
2302
+ message: `Everything search exceeded ${timeoutMs}ms while filtering excluded paths; narrow the query or increase the Everything timeout.`
2303
+ });
2304
+ for (;; ) {
2305
+ const remainingMs = deadline - Date.now();
2306
+ if (remainingMs <= 0)
2307
+ return timedOut();
2308
+ const args = buildEverythingSearchArgs(this.instanceName, { ...request, offset: nativeOffset, maxResults: MAX_FILTER_CANDIDATES }, remainingMs);
2309
+ const response = await this.run(esPath, args, remainingMs + 5000);
2310
+ if (response.code !== 0)
2311
+ return this.failure("search-failed", esPath, args, response);
2312
+ if (Date.now() >= deadline)
2313
+ return timedOut();
2314
+ let entries;
2315
+ try {
2316
+ entries = parseEverythingJson(response.stdout);
2317
+ } catch (error) {
2318
+ return { ok: false, reason: "search-failed", message: error.message };
2319
+ }
2320
+ for (const entry of entries) {
2321
+ if (this.isExcludedPath(entry.path))
2322
+ continue;
2323
+ if (skipped < offset) {
2324
+ skipped += 1;
2325
+ continue;
2326
+ }
2327
+ results.push(entry);
2328
+ if (results.length >= maxResults)
2329
+ return { ok: true, results };
2330
+ }
2331
+ if (entries.length < MAX_FILTER_CANDIDATES)
2332
+ return { ok: true, results };
2333
+ nativeOffset += entries.length;
2334
+ }
2335
+ }
2268
2336
  index() {
2269
2337
  return this.serialize(async () => {
2270
2338
  const es = await this.ensureRunning(true);
@@ -2338,7 +2406,8 @@ class EverythingClient {
2338
2406
  excludeFolders: [
2339
2407
  ...new Set([
2340
2408
  join6(this.options.root, ".autorag"),
2341
- ...this.options.folders.map((folder) => join6(folder, ".autorag"))
2409
+ ...this.options.folders.map((folder) => join6(folder, ".autorag")),
2410
+ ...this.options.excludeFolders ?? []
2342
2411
  ])
2343
2412
  ]
2344
2413
  }));
@@ -2432,8 +2501,9 @@ function launchDetached(command, args) {
2432
2501
  child.on("error", () => {});
2433
2502
  child.unref();
2434
2503
  }
2435
- var DEFAULT_TIMEOUT_MS2 = 30000, DEFAULT_STARTUP_TIMEOUT_MS = 20000, DEFAULT_INDEX_TIMEOUT_MS, DEFAULT_POLL_INTERVAL_MS = 250, DEFAULT_MAX_RESULTS = 100, MAX_OUTPUT_BYTES, VERSION_PATTERN;
2504
+ var DEFAULT_TIMEOUT_MS2 = 30000, DEFAULT_STARTUP_TIMEOUT_MS = 20000, DEFAULT_INDEX_TIMEOUT_MS, DEFAULT_POLL_INTERVAL_MS = 250, DEFAULT_MAX_RESULTS = 100, MAX_FILTER_CANDIDATES = 1000, MAX_OUTPUT_BYTES, VERSION_PATTERN;
2436
2505
  var init_client = __esm(() => {
2506
+ init_path_exclusions();
2437
2507
  init_bundle();
2438
2508
  DEFAULT_INDEX_TIMEOUT_MS = 10 * 60000;
2439
2509
  MAX_OUTPUT_BYTES = 8 * 1024 * 1024;
@@ -2453,6 +2523,7 @@ function compare(a, b) {
2453
2523
  return a < b ? -1 : a > b ? 1 : 0;
2454
2524
  }
2455
2525
  async function walkFileSearch(folders, request, options = {}) {
2526
+ const isExcluded = createPathExclusionMatcher(options.excludeFolders ?? []);
2456
2527
  const maxVisited = options.maxVisited ?? DEFAULT_MAX_VISITED;
2457
2528
  const maxMatches = options.maxMatches ?? DEFAULT_MAX_MATCHES;
2458
2529
  const deadline = Date.now() + (options.deadlineMs ?? DEFAULT_DEADLINE_MS);
@@ -2475,6 +2546,8 @@ async function walkFileSearch(folders, request, options = {}) {
2475
2546
  outer:
2476
2547
  while (pending.length > 0) {
2477
2548
  const dir = pending.pop();
2549
+ if (isExcluded(dir))
2550
+ continue;
2478
2551
  if (Date.now() >= deadline) {
2479
2552
  truncated = true;
2480
2553
  break;
@@ -2492,6 +2565,8 @@ async function walkFileSearch(folders, request, options = {}) {
2492
2565
  }
2493
2566
  visited += 1;
2494
2567
  const path = join7(dir, dirent.name);
2568
+ if (isExcluded(path))
2569
+ continue;
2495
2570
  if (dirent.isDirectory()) {
2496
2571
  if (dirent.name === ".autorag")
2497
2572
  continue;
@@ -2526,6 +2601,8 @@ async function walkFileSearch(folders, request, options = {}) {
2526
2601
  }
2527
2602
  }
2528
2603
  for (const folder of folders) {
2604
+ if (isExcluded(folder))
2605
+ continue;
2529
2606
  if (!underPath(folder) || !matches(folder))
2530
2607
  continue;
2531
2608
  if (request.kind === "files")
@@ -2552,6 +2629,7 @@ async function walkFileSearch(folders, request, options = {}) {
2552
2629
  }
2553
2630
  var DEFAULT_MAX_VISITED = 500000, DEFAULT_DEADLINE_MS = 20000, DEFAULT_MAX_MATCHES = 1e4, SORT_ORDERS;
2554
2631
  var init_walk = __esm(() => {
2632
+ init_path_exclusions();
2555
2633
  SORT_ORDERS = {
2556
2634
  "name-ascending": (a, b) => compare(a.name, b.name),
2557
2635
  "name-descending": (a, b) => compare(b.name, a.name),
@@ -2661,6 +2739,7 @@ class FSearchClient {
2661
2739
  launch;
2662
2740
  isAlive;
2663
2741
  kill;
2742
+ isExcludedPath;
2664
2743
  stateDir;
2665
2744
  dbPath;
2666
2745
  pidPath;
@@ -2673,6 +2752,7 @@ class FSearchClient {
2673
2752
  this.launch = options.launch ?? launchDetached2;
2674
2753
  this.isAlive = options.isProcessAlive ?? defaultIsProcessAlive;
2675
2754
  this.kill = options.killProcess ?? ((pid) => process.kill(pid, "SIGTERM"));
2755
+ this.isExcludedPath = createPathExclusionMatcher(options.excludeFolders ?? []);
2676
2756
  this.stateDir = join8(options.root, ".autorag", "fsearch");
2677
2757
  this.dbPath = join8(this.stateDir, "fsearch.db");
2678
2758
  this.pidPath = join8(this.stateDir, "watch.pid");
@@ -2690,6 +2770,7 @@ class FSearchClient {
2690
2770
  if (unavailable !== undefined || !binary.ok) {
2691
2771
  try {
2692
2772
  const walked = await walkFileSearch(this.options.folders, request, {
2773
+ excludeFolders: this.options.excludeFolders,
2693
2774
  maxVisited: this.options.walkMaxVisited,
2694
2775
  deadlineMs: this.options.walkDeadlineMs
2695
2776
  });
@@ -2704,7 +2785,9 @@ class FSearchClient {
2704
2785
  return { ok: false, reason: "search-failed", message: error.message };
2705
2786
  }
2706
2787
  }
2707
- const args = buildFSearchSearchArgs(this.dbPath, this.socketPath(), request);
2788
+ const hasExclusions = (this.options.excludeFolders?.length ?? 0) > 0;
2789
+ const nativeRequest = hasExclusions ? { ...request, offset: 0, maxResults: MAX_RESULTS_CAP } : request;
2790
+ const args = buildFSearchSearchArgs(this.dbPath, this.socketPath(), nativeRequest);
2708
2791
  const result = await this.run(binary.path, args, this.options.timeoutMs ?? DEFAULT_TIMEOUT_MS3);
2709
2792
  if (result.code !== 0)
2710
2793
  return this.failure("search-failed", binary.path, args, result);
@@ -2715,6 +2798,10 @@ class FSearchClient {
2715
2798
  return { ok: false, reason: "search-failed", message: error.message };
2716
2799
  }
2717
2800
  let entries = parsed.entries;
2801
+ if (hasExclusions) {
2802
+ const isExcluded = this.isExcludedPath;
2803
+ entries = entries.filter((entry) => !isExcluded(entry.path));
2804
+ }
2718
2805
  if (request.path !== undefined) {
2719
2806
  const prefix = request.path.endsWith(sep3) ? request.path : `${request.path}${sep3}`;
2720
2807
  entries = entries.filter((entry) => entry.path === request.path || entry.path.startsWith(prefix));
@@ -2724,7 +2811,16 @@ class FSearchClient {
2724
2811
  const maxResults = Math.min(request.maxResults ?? DEFAULT_MAX_RESULTS2, MAX_RESULTS_CAP);
2725
2812
  if (entries.length > maxResults)
2726
2813
  entries = entries.slice(0, maxResults);
2727
- return { ok: true, backend: "fsearch-cli", results: entries, total: parsed.total };
2814
+ return {
2815
+ ok: true,
2816
+ backend: "fsearch-cli",
2817
+ results: entries,
2818
+ total: parsed.total,
2819
+ ...hasExclusions && parsed.total !== undefined && parsed.total > parsed.entries.length && entries.length < maxResults ? {
2820
+ truncated: true,
2821
+ note: `FSearch checked ${parsed.entries.length} of ${parsed.total} native matches before exclusions (candidate limit ${MAX_RESULTS_CAP}); results may be incomplete. Narrow the query.`
2822
+ } : {}
2823
+ };
2728
2824
  });
2729
2825
  }
2730
2826
  index() {
@@ -2918,6 +3014,7 @@ function defaultIsProcessAlive(pid) {
2918
3014
  }
2919
3015
  var DEFAULT_TIMEOUT_MS3 = 30000, DEFAULT_STARTUP_TIMEOUT_MS2 = 1e4, STOP_TIMEOUT_MS = 2000, DEFAULT_INDEX_TIMEOUT_MS2, DEFAULT_POLL_INTERVAL_MS2 = 250, DEFAULT_MAX_RESULTS2 = 100, MAX_RESULTS_CAP = 1000, MAX_OUTPUT_BYTES2, SORT_KEYS;
2920
3016
  var init_client2 = __esm(() => {
3017
+ init_path_exclusions();
2921
3018
  init_walk();
2922
3019
  DEFAULT_INDEX_TIMEOUT_MS2 = 10 * 60000;
2923
3020
  MAX_OUTPUT_BYTES2 = 8 * 1024 * 1024;
@@ -5045,7 +5142,20 @@ function configuredVectorDimension(workspacePath) {
5045
5142
  const dimension = options.dimension;
5046
5143
  return typeof dimension === "number" ? dimension : undefined;
5047
5144
  }
5048
- function rewriteEmbedderConfig(workspacePath, embedder, chunkerConfig = {}) {
5145
+ function configuredLexicalLanguage(workspacePath) {
5146
+ let raw;
5147
+ try {
5148
+ raw = readFileSync8(minSyncConfigPath(workspacePath), "utf8");
5149
+ } catch {
5150
+ return;
5151
+ }
5152
+ const lexical = parse(raw).lexical;
5153
+ if (typeof lexical !== "object" || lexical === null || Array.isArray(lexical))
5154
+ return "simple";
5155
+ const language = lexical.language;
5156
+ return typeof language === "string" ? language : "simple";
5157
+ }
5158
+ function rewriteEmbedderConfig(workspacePath, embedder, managed = {}) {
5049
5159
  const configPath = minSyncConfigPath(workspacePath);
5050
5160
  let raw;
5051
5161
  try {
@@ -5088,8 +5198,13 @@ function rewriteEmbedderConfig(workspacePath, embedder, chunkerConfig = {}) {
5088
5198
  }
5089
5199
  if (embedder.dimension !== undefined)
5090
5200
  options.dimension = embedder.dimension;
5091
- if (chunkerConfig.maxChunkSize !== undefined)
5092
- chunkerOptions.max_chunk_size = chunkerConfig.maxChunkSize;
5201
+ if (managed.maxChunkSize !== undefined)
5202
+ chunkerOptions.max_chunk_size = managed.maxChunkSize;
5203
+ if (managed.lexicalLanguage !== undefined) {
5204
+ const lexical = typeof parsed.lexical === "object" && parsed.lexical !== null && !Array.isArray(parsed.lexical) ? parsed.lexical : {};
5205
+ parsed.lexical = lexical;
5206
+ lexical.language = managed.lexicalLanguage;
5207
+ }
5093
5208
  writeFileSync6(configPath, stringify(parsed));
5094
5209
  return true;
5095
5210
  }
@@ -5101,6 +5216,66 @@ var init_embedder_config = __esm(() => {
5101
5216
  };
5102
5217
  });
5103
5218
 
5219
+ // src/minsync/kiwi.ts
5220
+ import { existsSync as existsSync10 } from "node:fs";
5221
+ import { homedir as homedir2 } from "node:os";
5222
+ import { posix as posix3, win32 as win323 } from "node:path";
5223
+ function kiwiAvailable(environment = defaultEnvironment()) {
5224
+ const { env, exists } = environment;
5225
+ const library = env.KIWI_LIBRARY_PATH;
5226
+ const model = env.KIWI_MODEL_PATH;
5227
+ const libraryFound = library !== undefined && library !== "" ? exists(library) : libraryCandidates(environment).some(exists);
5228
+ const modelFound = model !== undefined && model !== "" ? exists(model) : modelCandidates(environment).some(exists);
5229
+ return libraryFound && modelFound;
5230
+ }
5231
+ function defaultEnvironment() {
5232
+ let home;
5233
+ try {
5234
+ home = homedir2();
5235
+ } catch {
5236
+ home = undefined;
5237
+ }
5238
+ return { env: process.env, platform: process.platform, home, exists: existsSync10 };
5239
+ }
5240
+ function userRoots({ env, platform, home }) {
5241
+ if (platform === "win32") {
5242
+ const roots = [];
5243
+ if (env.LOCALAPPDATA)
5244
+ roots.push(win323.join(env.LOCALAPPDATA, "kiwi"));
5245
+ const profile = env.USERPROFILE ?? home;
5246
+ if (profile)
5247
+ roots.push(win323.join(profile, "AppData", "Local", "kiwi"));
5248
+ roots.push("C:\\kiwi", "C:\\Program Files\\Kiwi");
5249
+ return roots;
5250
+ }
5251
+ return home ? [posix3.join(home, ".local", "kiwi")] : [];
5252
+ }
5253
+ function libraryCandidates(environment) {
5254
+ const { platform } = environment;
5255
+ if (platform === "win32")
5256
+ return userRoots(environment).map((root) => win323.join(root, "lib", "kiwi.dll"));
5257
+ const file = platform === "darwin" ? "libkiwi.dylib" : "libkiwi.so";
5258
+ const system = platform === "darwin" ? ["/usr/local/lib", "/opt/homebrew/lib"] : ["/usr/local/lib", "/usr/lib"];
5259
+ return [
5260
+ ...userRoots(environment).map((root) => posix3.join(root, "lib", file)),
5261
+ ...system.map((dir) => posix3.join(dir, file))
5262
+ ];
5263
+ }
5264
+ function modelCandidates(environment) {
5265
+ const { platform } = environment;
5266
+ if (platform === "win32")
5267
+ return userRoots(environment).map((root) => win323.join(root, "models", "cong", "base"));
5268
+ const system = platform === "darwin" ? ["/usr/local/models", "/opt/homebrew/models", "/usr/local/share/kiwi/models"] : ["/usr/local/models", "/usr/local/share/kiwi/models", "/usr/share/kiwi/models"];
5269
+ return [
5270
+ ...userRoots(environment).map((root) => posix3.join(root, "models", "cong", "base")),
5271
+ ...system.map((dir) => posix3.join(dir, "cong", "base"))
5272
+ ];
5273
+ }
5274
+ var init_kiwi = () => {};
5275
+
5276
+ // src/minsync/lexical-language.ts
5277
+ var MINSYNC_LEXICAL_LANGUAGE = "multilingual";
5278
+
5104
5279
  // src/minsync/local-embedder.ts
5105
5280
  import { spawn as spawn7 } from "node:child_process";
5106
5281
  import { request } from "node:http";
@@ -5289,7 +5464,7 @@ var init_process = __esm(() => {
5289
5464
  });
5290
5465
 
5291
5466
  // src/minsync/client.ts
5292
- import { existsSync as existsSync10, readFileSync as readFileSync9, writeFileSync as writeFileSync7 } from "node:fs";
5467
+ import { existsSync as existsSync11, readFileSync as readFileSync9, writeFileSync as writeFileSync7 } from "node:fs";
5293
5468
  import { join as join14 } from "node:path";
5294
5469
 
5295
5470
  class MinSyncClient {
@@ -5384,7 +5559,7 @@ class MinSyncClient {
5384
5559
  return previous === undefined || JSON.stringify(previous) !== JSON.stringify(identity);
5385
5560
  }
5386
5561
  async sync(force = false, signal) {
5387
- if (!existsSync10(this.binaryPath))
5562
+ if (!existsSync11(this.binaryPath))
5388
5563
  throw minSyncBinaryMissingError();
5389
5564
  let effective = { config: {} };
5390
5565
  try {
@@ -5422,14 +5597,16 @@ class MinSyncClient {
5422
5597
  }
5423
5598
  }
5424
5599
  const spawnOptions = signal === undefined ? {} : { signal };
5425
- const initialized = existsSync10(minSyncConfigPath(this.workspacePath));
5600
+ const lexical = this.resolveLexicalLanguage();
5601
+ const initialized = existsSync11(minSyncConfigPath(this.workspacePath));
5426
5602
  if (!initialized) {
5427
5603
  const initArgs = ["init", "--format", "json"];
5428
5604
  if (this.embedder?.id) {
5429
5605
  initArgs.push("--embedder", this.embedder.id);
5430
5606
  }
5607
+ initArgs.push("--language", lexical.language);
5431
5608
  const init = await this.spawn(initArgs, spawnOptions);
5432
- if (!init.ok || !existsSync10(minSyncConfigPath(this.workspacePath))) {
5609
+ if (!init.ok || !existsSync11(minSyncConfigPath(this.workspacePath))) {
5433
5610
  return {
5434
5611
  ok: false,
5435
5612
  synced: 0,
@@ -5441,20 +5618,29 @@ class MinSyncClient {
5441
5618
  const chunkSizeChanged = this.maxChunkSize !== undefined && configuredMaxChunkSize(this.workspacePath) !== this.maxChunkSize;
5442
5619
  const dimensionChanged = embedder.dimension !== undefined && configuredVectorDimension(this.workspacePath) !== embedder.dimension;
5443
5620
  const configPath = minSyncConfigPath(this.workspacePath);
5444
- const shouldRewriteConfig = this.embedder !== undefined || this.maxChunkSize !== undefined || effective.identity !== undefined;
5445
- const originalConfig = shouldRewriteConfig ? readConfigSnapshot(configPath) : undefined;
5446
- const configRewritten = shouldRewriteConfig && rewriteEmbedderConfig(this.workspacePath, embedder, { maxChunkSize: this.maxChunkSize });
5621
+ const originalConfig = readConfigSnapshot(configPath);
5622
+ const configRewritten = rewriteEmbedderConfig(this.workspacePath, embedder, {
5623
+ maxChunkSize: this.maxChunkSize,
5624
+ lexicalLanguage: lexical.language
5625
+ });
5447
5626
  const restoreConfig = () => {
5448
5627
  if (configRewritten && originalConfig !== undefined)
5449
5628
  writeFileSync7(configPath, originalConfig);
5450
5629
  };
5451
5630
  try {
5452
- return await this.checkAndSync(force || chunkSizeChanged || dimensionChanged, signal, effective, restoreConfig);
5631
+ const result = await this.checkAndSync(force || chunkSizeChanged || dimensionChanged, signal, effective, restoreConfig);
5632
+ return result.ok && lexical.fallback !== undefined ? { ...result, lexicalFallback: lexical.fallback } : result;
5453
5633
  } catch (error) {
5454
5634
  restoreConfig();
5455
5635
  throw error;
5456
5636
  }
5457
5637
  }
5638
+ resolveLexicalLanguage() {
5639
+ const requested = MINSYNC_LEXICAL_LANGUAGE;
5640
+ if (kiwiAvailable() || configuredLexicalLanguage(this.workspacePath) === requested)
5641
+ return { language: requested };
5642
+ return { language: "simple", fallback: { requested, used: "simple" } };
5643
+ }
5458
5644
  lockHeldResult(stderr) {
5459
5645
  return {
5460
5646
  ok: false,
@@ -5507,7 +5693,7 @@ class MinSyncClient {
5507
5693
  }
5508
5694
  const identityChanged = effective.identity !== undefined && this.identityMismatch(effective.identity);
5509
5695
  const fullReindex = forceFull || identityChanged;
5510
- const syncArgs = existsSync10(cursorPath) && !fullReindex ? ["sync", "--format", "json"] : ["sync", "--full", "--format", "json"];
5696
+ const syncArgs = existsSync11(cursorPath) && !fullReindex ? ["sync", "--format", "json"] : ["sync", "--full", "--format", "json"];
5511
5697
  const result = await this.spawn(syncArgs, spawnOptions);
5512
5698
  if (!result.ok && result.code === MINSYNC_LOCK_HELD_EXIT_CODE) {
5513
5699
  restoreConfig();
@@ -5528,7 +5714,7 @@ class MinSyncClient {
5528
5714
  }
5529
5715
  };
5530
5716
  }
5531
- if (!existsSync10(cursorPath)) {
5717
+ if (!existsSync11(cursorPath)) {
5532
5718
  restoreConfig();
5533
5719
  return { ok: false, synced: 0, workspacePath: this.workspacePath, reason: "not-ready: missing cursor" };
5534
5720
  }
@@ -5549,7 +5735,7 @@ class MinSyncClient {
5549
5735
  };
5550
5736
  }
5551
5737
  async query(text, topK, mode = "vector", signal) {
5552
- if (!existsSync10(this.binaryPath))
5738
+ if (!existsSync11(this.binaryPath))
5553
5739
  throw minSyncBinaryMissingError();
5554
5740
  const result = await this.spawn(["query", "--format", "json", "--mode", mode, "-k", String(topK), text], {
5555
5741
  ...signal === undefined ? {} : { signal }
@@ -5643,6 +5829,7 @@ var MINSYNC_OLLAMA_MIGRATION_MESSAGE = "This workspace uses the legacy 768-dimen
5643
5829
  var init_client4 = __esm(() => {
5644
5830
  init_embedder_config();
5645
5831
  init_errors();
5832
+ init_kiwi();
5646
5833
  init_local_embedder();
5647
5834
  init_process();
5648
5835
  API_KEY_ENV_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
@@ -5665,14 +5852,14 @@ var init_client4 = __esm(() => {
5665
5852
 
5666
5853
  // src/minsync/installer.ts
5667
5854
  import { createHash as createHash9 } from "node:crypto";
5668
- import { copyFileSync, createWriteStream as createWriteStream2, existsSync as existsSync11, mkdirSync as mkdirSync8, renameSync as renameSync4, rmSync as rmSync5 } from "node:fs";
5855
+ import { copyFileSync, createWriteStream as createWriteStream2, existsSync as existsSync12, mkdirSync as mkdirSync8, renameSync as renameSync4, rmSync as rmSync5 } from "node:fs";
5669
5856
  import { chmod as chmod2, mkdtemp } from "node:fs/promises";
5670
5857
  import { get } from "node:https";
5671
5858
  import { basename as basename7, delimiter as delimiter2, dirname as dirname7, join as join15 } from "node:path";
5672
5859
  import { pipeline } from "node:stream/promises";
5673
5860
  async function ensureMinSyncBinary(options) {
5674
5861
  const binaryPath = join15(options.root, ".autorag", "bin", executableName(options.platform ?? process.platform));
5675
- if (existsSync11(binaryPath))
5862
+ if (existsSync12(binaryPath))
5676
5863
  return { binaryPath, version: "cached" };
5677
5864
  mkdirSync8(dirname7(binaryPath), { recursive: true });
5678
5865
  const cargoInstaller = options.cargoInstaller ?? ((destination) => installMinSyncFromCargoIfAvailable(options, destination));
@@ -5720,7 +5907,7 @@ async function installMinSyncFromCargo(options, destination, cargo) {
5720
5907
  throw new MinSyncReleaseError(result.stderr || `Could not install MinSync ${MINSYNC_VERSION} from crates.io`);
5721
5908
  }
5722
5909
  const installedBinary = join15(cargoRoot, "bin", executableName(options.platform ?? process.platform));
5723
- if (!existsSync11(installedBinary)) {
5910
+ if (!existsSync12(installedBinary)) {
5724
5911
  throw new MinSyncReleaseError(`Cargo did not produce the MinSync ${MINSYNC_VERSION} binary`);
5725
5912
  }
5726
5913
  copyFileSync(installedBinary, destination);
@@ -5852,7 +6039,7 @@ function lookupCargo(env, platform) {
5852
6039
  if (dir.length === 0)
5853
6040
  continue;
5854
6041
  const candidate = join15(dir, execName);
5855
- if (existsSync11(candidate))
6042
+ if (existsSync12(candidate))
5856
6043
  return candidate;
5857
6044
  }
5858
6045
  return;
@@ -5883,7 +6070,7 @@ function isGitHubAsset(value) {
5883
6070
  function isRecord5(value) {
5884
6071
  return typeof value === "object" && value !== null;
5885
6072
  }
5886
- var LATEST_RELEASE_URL = "https://api.github.com/repos/NomaDamas/MinSync/releases/latest", SHA256_HEX_PATTERN, MINSYNC_VERSION = "0.4.8", MINSYNC_CARGO_INSTALL_TIMEOUT_MS, CARGO_PROBE_TIMEOUT_MS = 5000, MinSyncReleaseError;
6073
+ var LATEST_RELEASE_URL = "https://api.github.com/repos/NomaDamas/MinSync/releases/latest", SHA256_HEX_PATTERN, MINSYNC_VERSION = "0.4.10", MINSYNC_CARGO_INSTALL_TIMEOUT_MS, CARGO_PROBE_TIMEOUT_MS = 5000, MinSyncReleaseError;
5887
6074
  var init_installer2 = __esm(() => {
5888
6075
  init_process();
5889
6076
  SHA256_HEX_PATTERN = /^[a-f0-9]{64}$/;
@@ -5911,7 +6098,7 @@ var init_paths2 = __esm(() => {
5911
6098
  import { randomUUID as randomUUID4 } from "node:crypto";
5912
6099
  import {
5913
6100
  copyFileSync as copyFileSync2,
5914
- existsSync as existsSync12,
6101
+ existsSync as existsSync13,
5915
6102
  lstatSync as lstatSync2,
5916
6103
  mkdirSync as mkdirSync9,
5917
6104
  readdirSync as readdirSync2,
@@ -5926,8 +6113,8 @@ function syncMinSyncWorkspace(root, options = {}) {
5926
6113
  const filesRoot = join17(workspacePath, "files");
5927
6114
  const index = loadMirrorIndex(root);
5928
6115
  const candidates = Object.values(index.entries).sort((a, b) => a.virtualPath.localeCompare(b.virtualPath));
5929
- const excluded = candidates.filter((entry) => normalizeVirtualPath(entry.virtualPath) !== entry.virtualPath || !existsSync12(entry.outputPath)).map((entry) => entry.virtualPath);
5930
- const entries = candidates.filter((entry) => normalizeVirtualPath(entry.virtualPath) === entry.virtualPath && existsSync12(entry.outputPath)).map((entry) => {
6116
+ const excluded = candidates.filter((entry) => normalizeVirtualPath(entry.virtualPath) !== entry.virtualPath || !existsSync13(entry.outputPath)).map((entry) => entry.virtualPath);
6117
+ const entries = candidates.filter((entry) => normalizeVirtualPath(entry.virtualPath) === entry.virtualPath && existsSync13(entry.outputPath)).map((entry) => {
5931
6118
  const minSyncPath = minSyncDocumentPath(workspacePath, entry.virtualPath);
5932
6119
  return {
5933
6120
  virtualPath: entry.virtualPath,
@@ -6064,7 +6251,7 @@ var init_workspace = __esm(() => {
6064
6251
  });
6065
6252
 
6066
6253
  // src/minsync/method.ts
6067
- import { accessSync, constants, existsSync as existsSync13, realpathSync as realpathSync4 } from "node:fs";
6254
+ import { accessSync, constants, existsSync as existsSync14, realpathSync as realpathSync4 } from "node:fs";
6068
6255
  import { basename as basename9, delimiter as delimiter3, join as join18, normalize as normalize2 } from "node:path";
6069
6256
  function lookupInPath(env) {
6070
6257
  const pathEnv = env.PATH;
@@ -6160,19 +6347,19 @@ class MinSyncVectorMethod {
6160
6347
  }
6161
6348
  isBinaryMissing() {
6162
6349
  if (this.binaryPath !== undefined)
6163
- return !existsSync13(this.binaryPath);
6350
+ return !existsSync14(this.binaryPath);
6164
6351
  if (lookupInPath(process.env) !== undefined)
6165
6352
  return false;
6166
6353
  const cachedBinary = join18(this.root, ".autorag", "bin", executableName(process.platform));
6167
- if (existsSync13(cachedBinary))
6354
+ if (existsSync14(cachedBinary))
6168
6355
  return false;
6169
6356
  return true;
6170
6357
  }
6171
6358
  isExplicitBinaryMissing() {
6172
- return this.binaryPath !== undefined && !existsSync13(this.binaryPath);
6359
+ return this.binaryPath !== undefined && !existsSync14(this.binaryPath);
6173
6360
  }
6174
6361
  isReady() {
6175
- return existsSync13(join18(this.workspacePath, ".minsync", "cursor.json"));
6362
+ return existsSync14(join18(this.workspacePath, ".minsync", "cursor.json"));
6176
6363
  }
6177
6364
  async retrieve(query, options) {
6178
6365
  if ((workspaceSyncInFlight.get(this.workspacePath) ?? 0) > 0) {
@@ -6225,7 +6412,7 @@ class MinSyncVectorMethod {
6225
6412
  return results;
6226
6413
  }
6227
6414
  async resolveBinary(options = {}) {
6228
- if (this.binaryPath && existsSync13(this.binaryPath))
6415
+ if (this.binaryPath && existsSync14(this.binaryPath))
6229
6416
  return this.binaryPath;
6230
6417
  if (this.binaryPath !== undefined)
6231
6418
  throw minSyncBinaryMissingError();
@@ -6233,7 +6420,7 @@ class MinSyncVectorMethod {
6233
6420
  if (pathBinary)
6234
6421
  return pathBinary;
6235
6422
  const cachedBinary = join18(this.root, ".autorag", "bin", executableName(process.platform));
6236
- if (existsSync13(cachedBinary))
6423
+ if (existsSync14(cachedBinary))
6237
6424
  return cachedBinary;
6238
6425
  if (this.autoInstall && options.install !== false) {
6239
6426
  try {
@@ -6936,7 +7123,7 @@ var init_defaults = __esm(() => {
6936
7123
  });
6937
7124
 
6938
7125
  // src/parser/isolated.ts
6939
- import { existsSync as existsSync14 } from "node:fs";
7126
+ import { existsSync as existsSync15 } from "node:fs";
6940
7127
  import { dirname as dirname10, join as join21 } from "node:path";
6941
7128
  import { fileURLToPath as fileURLToPath2, pathToFileURL } from "node:url";
6942
7129
  import { Worker } from "node:worker_threads";
@@ -7075,7 +7262,7 @@ function defaultWorkerUrl() {
7075
7262
  join21(here, "parser", "parse-worker.js"),
7076
7263
  join21(here, "..", "parser", "parse-worker.js")
7077
7264
  ]) {
7078
- if (existsSync14(candidate))
7265
+ if (existsSync15(candidate))
7079
7266
  return pathToFileURL(candidate);
7080
7267
  }
7081
7268
  throw new Error(`The AutoRAG parse worker entry was not found near ${here}.`);
@@ -7087,7 +7274,7 @@ var init_isolated = __esm(() => {
7087
7274
  });
7088
7275
 
7089
7276
  // src/mirror/sync.ts
7090
- import { existsSync as existsSync15, rmSync as rmSync7 } from "node:fs";
7277
+ import { existsSync as existsSync16, rmSync as rmSync7 } from "node:fs";
7091
7278
  import { opendir, readFile as readFile3, stat as stat5 } from "node:fs/promises";
7092
7279
  import { extname as extname3, resolve as resolve11, sep as sep6 } from "node:path";
7093
7280
  import { setTimeout as sleep3 } from "node:timers/promises";
@@ -7279,7 +7466,7 @@ async function syncWithRegistry(options, registry, isolated) {
7279
7466
  }
7280
7467
  const previousEntry = previous.entries[entry.virtualPath];
7281
7468
  const outputPath = parsedOutputPath(options.root, entry.virtualPath);
7282
- const unchanged = !options.force && previousEntry?.sourceMtimeNs === entry.mtimeNs && previousEntry.sourceSizeBytes === entry.sizeBytes && previousEntry.parserName === parser.name && existsSync15(outputPath);
7469
+ const unchanged = !options.force && previousEntry?.sourceMtimeNs === entry.mtimeNs && previousEntry.sourceSizeBytes === entry.sizeBytes && previousEntry.parserName === parser.name && existsSync16(outputPath);
7283
7470
  if (!unchanged) {
7284
7471
  let bytes;
7285
7472
  try {
@@ -7660,7 +7847,7 @@ function removePrevious(root, index, virtualPath) {
7660
7847
  return 1;
7661
7848
  }
7662
7849
  function removeFile(path) {
7663
- if (existsSync15(path))
7850
+ if (existsSync16(path))
7664
7851
  rmSync7(path, { force: true });
7665
7852
  }
7666
7853
  var DEFAULT_MAX_SOURCE_BYTES, MIRROR_CHECKPOINT_EVERY = 25, DEFAULT_MAX_DELETE_RATIO = 0.3, MASS_DELETE_FLOOR = 100, DEFAULT_MAX_SCANNED_ENTRIES = 2000000, DEFAULT_MAX_WALK_DEPTH = 64, PRODUCT_ARTIFACT_FILE_NAMES, SKIP_DIR_NAMES, PARSER_FAILURE_MESSAGE = "The registered parser failed on this file; it was skipped during indexing.", PARSER_CRASH_MESSAGE = "The registered parser crashed unexpectedly on this file; it was skipped during indexing.", DEFAULT_TRANSIENT_RETRIES = 3, DEFAULT_TRANSIENT_BACKOFF_MS = 50, TRANSIENT_ERROR_CODES, WORKSPACE_ERROR_CODES, MirrorWorkspaceError, STANDALONE_OCR_IMAGE_EXTENSIONS, RETRYABLE_SKIP_MESSAGES, SEARCH_ROOT_UNAVAILABLE_MESSAGES;
@@ -9186,12 +9373,43 @@ function digitsEndAt(text, from) {
9186
9373
  end += 1;
9187
9374
  return end;
9188
9375
  }
9189
- function continuesAsProse(answer, from) {
9190
- if (answer[from] !== " " && answer[from] !== " ")
9376
+ function continuesAsProse(answer, openIndex, from) {
9377
+ let cursor = from;
9378
+ let leadingEmphasis = 0;
9379
+ while (openIndex - 1 - leadingEmphasis >= 0 && (answer[openIndex - 1 - leadingEmphasis] === "*" || answer[openIndex - 1 - leadingEmphasis] === "_")) {
9380
+ leadingEmphasis += 1;
9381
+ }
9382
+ if (leadingEmphasis > 0) {
9383
+ let trailingEmphasis = 0;
9384
+ while (cursor + trailingEmphasis < answer.length && trailingEmphasis < leadingEmphasis && answer[cursor + trailingEmphasis] === answer[openIndex - 1 - trailingEmphasis]) {
9385
+ trailingEmphasis += 1;
9386
+ }
9387
+ if (trailingEmphasis === leadingEmphasis)
9388
+ cursor += trailingEmphasis;
9389
+ }
9390
+ const tokenStart = openIndex - leadingEmphasis;
9391
+ const linePrefix = answer.slice(answer.lastIndexOf(`
9392
+ `, tokenStart - 1) + 1, tokenStart);
9393
+ const atHeadingStart = /^[ \t>*-]*$/u.test(linePrefix);
9394
+ if (atHeadingStart && answer[cursor] === ":")
9395
+ cursor += 1;
9396
+ if (answer[cursor] !== " " && answer[cursor] !== " ")
9191
9397
  return false;
9192
- const next = answer.codePointAt(from + 1);
9398
+ cursor += 1;
9399
+ while (cursor < answer.length && (answer[cursor] === "*" || answer[cursor] === "_" || answer[cursor] === '"' || answer[cursor] === "'" || answer[cursor] === "“" || answer[cursor] === "‘")) {
9400
+ cursor += 1;
9401
+ }
9402
+ const next = answer.codePointAt(cursor);
9193
9403
  return next !== undefined && /[\p{L}\p{N}]/u.test(String.fromCodePoint(next));
9194
9404
  }
9405
+ function isLegalPinpoint(answer, openIndex, afterCloseIndex) {
9406
+ if (PINPOINT_RANGE_AHEAD.test(answer.slice(afterCloseIndex, afterCloseIndex + 16)))
9407
+ return true;
9408
+ if (PINPOINT_RANGE_BEHIND.test(answer.slice(Math.max(0, openIndex - 16), openIndex)))
9409
+ return true;
9410
+ const prefixWindow = answer.slice(Math.max(0, openIndex - 120), openIndex);
9411
+ return PARA_PINPOINT_PREFIX.test(prefixWindow) || CLAUSE_START_AT_PREFIX.test(prefixWindow) || CASE_CITATION_AT_PREFIX.test(prefixWindow);
9412
+ }
9195
9413
  function evidenceMarkerAt(answer, index) {
9196
9414
  const ids = [];
9197
9415
  let cursor = index + 1;
@@ -9257,7 +9475,7 @@ function answerMarkers(answer, isFile = () => false) {
9257
9475
  }
9258
9476
  if (char === "[") {
9259
9477
  const digitsEnd = digitsEndAt(answer, index + 1);
9260
- if (digitsEnd > index + 1 && answer[digitsEnd] === "]" && answer[digitsEnd + 1] !== "(" && !continuesAsProse(answer, digitsEnd + 1)) {
9478
+ if (digitsEnd > index + 1 && answer[digitsEnd] === "]" && answer[digitsEnd + 1] !== "(" && !continuesAsProse(answer, index, digitsEnd + 1) && !isLegalPinpoint(answer, index, digitsEnd + 1)) {
9261
9479
  markers.push({
9262
9480
  kind: "number",
9263
9481
  start: leadingStart(index),
@@ -9329,7 +9547,14 @@ function assertResultsMappingOneToOne(label, results, mapping) {
9329
9547
  ].filter((note) => note !== undefined);
9330
9548
  throw new Error(`${label}: result numbers and mapping numbers must be one-to-one, but results contain ${formatCitationList(resultNumbers)} ` + `and mapping contains ${formatCitationList(mappingNumbers)}` + (duplicateNotes.length > 0 ? ` (${duplicateNotes.join("; ")})` : "") + ". Give every result a unique number with exactly one mapping entry carrying the same number and no mapping entry without a result.");
9331
9549
  }
9332
- var MAX_FILE_MARKER_CLOSERS = 8, FILE_MARKER_PREFIX = "[file:";
9550
+ 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;
9551
+ var init_citations = __esm(() => {
9552
+ 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;
9553
+ 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;
9554
+ 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;
9555
+ PINPOINT_RANGE_AHEAD = /^[ \t\u00a0]*[-–—][ \t\u00a0]*\[\d+\]/u;
9556
+ PINPOINT_RANGE_BEHIND = /\[\d+\][ \t\u00a0]*[-–—][ \t\u00a0]*$/u;
9557
+ });
9333
9558
 
9334
9559
  // src/agent/evidence-ledger.ts
9335
9560
  import { createHash as createHash10 } from "node:crypto";
@@ -9613,6 +9838,7 @@ function deriveResultsFromAnswer(text, ledger, options) {
9613
9838
  }
9614
9839
  var MAX_EXCERPT_CHARS = 400, MAX_CLAIM_CHARS = 280, CLAIM_LOOKBACK_CHARS = 600, SENTENCE_END, LEADING_LIST_MARK, EVIDENCE_MARKER_TEXT, LEADING_BLANK;
9615
9840
  var init_answer_citations = __esm(() => {
9841
+ init_citations();
9616
9842
  init_evidence_ledger();
9617
9843
  SENTENCE_END = /[.!?。!?]/u;
9618
9844
  LEADING_LIST_MARK = /^[\s\-*•>]+/u;
@@ -9621,7 +9847,7 @@ var init_answer_citations = __esm(() => {
9621
9847
  });
9622
9848
 
9623
9849
  // src/agent/answer-guidelines.ts
9624
- var ANSWER_IMAGE_EMBED_RULE = "**Images**: when a retrieved result is itself an image file (png, jpg, jpeg, gif, webp, svg, bmp, avif, heic, tiff) and showing it answers the question, embed it in the answer as a markdown image on its own line: `![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.";
9850
+ 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.";
9625
9851
 
9626
9852
  // src/agent/datasource-skill.ts
9627
9853
  import { Type as Type3 } from "typebox";
@@ -11346,6 +11572,20 @@ function interactiveKeyOf(data) {
11346
11572
  return "followUp";
11347
11573
  return;
11348
11574
  }
11575
+ function stripTelegramEnvelope(text) {
11576
+ return text.replace(/^\s*\[telegram\]\s?/u, "").replace(/\n*\[time\][^\n]*/gu, "").trim();
11577
+ }
11578
+ function userInputText(event) {
11579
+ if (event.source === "interactive")
11580
+ return event.text.trim();
11581
+ if (event.source === "extension" && event.text.trimStart().startsWith(TELEGRAM_INPUT_PREFIX)) {
11582
+ return stripTelegramEnvelope(event.text);
11583
+ }
11584
+ if (event.source === "extension" && event.text.trimStart().startsWith(WHATSAPP_INPUT_PREFIX)) {
11585
+ return event.text.trim();
11586
+ }
11587
+ return;
11588
+ }
11349
11589
  function createAutoRAGInteractiveExtension(getSystemPrompt, contextTransform, options, updateNotice) {
11350
11590
  return (pi) => {
11351
11591
  pi.on("before_agent_start", () => ({ systemPrompt: getSystemPrompt() }));
@@ -11376,8 +11616,8 @@ function createAutoRAGInteractiveExtension(getSystemPrompt, contextTransform, op
11376
11616
  });
11377
11617
  }
11378
11618
  pi.on("input", (event) => {
11379
- const text = event.text.trim();
11380
- if (event.source !== "interactive" || text.length === 0)
11619
+ const text = userInputText(event);
11620
+ if (text === undefined || text.length === 0)
11381
11621
  return;
11382
11622
  if (text.startsWith("/") && isSearchRunning?.() !== true)
11383
11623
  return;
@@ -11474,7 +11714,9 @@ async function createAutoRAGPiInteractiveRuntime(options) {
11474
11714
  agentDir: runtimeAgentDir,
11475
11715
  modelRuntime,
11476
11716
  settingsManager,
11717
+ ...options.extensionFlags === undefined ? {} : { extensionFlagValues: new Map(Object.entries(options.extensionFlags)) },
11477
11718
  resourceLoaderOptions: {
11719
+ ...options.extensionPaths === undefined ? {} : { additionalExtensionPaths: [...options.extensionPaths] },
11478
11720
  extensionFactories: [
11479
11721
  createAutoRAGInteractiveExtension(options.getSystemPrompt, options.contextTransform, { onInput: options.onInput, isSearchRunning: options.isSearchRunning, onKey: options.onKey }, options.updateNotice),
11480
11722
  ...options.extensionFactories ?? []
@@ -11508,7 +11750,7 @@ async function createAutoRAGPiInteractiveRuntime(options) {
11508
11750
  });
11509
11751
  return { runtime, dispose: () => runtime.dispose() };
11510
11752
  }
11511
- var PI_BUILTIN_TOOL_NAMES, EMBEDDED_AUTH_MARKER = "<autorag-embedded-model>";
11753
+ var PI_BUILTIN_TOOL_NAMES, EMBEDDED_AUTH_MARKER = "<autorag-embedded-model>", TELEGRAM_INPUT_PREFIX = "[telegram]", WHATSAPP_INPUT_PREFIX = "Message from ";
11512
11754
  var init_pi_session = __esm(() => {
11513
11755
  init_provider();
11514
11756
  init_context_token_guard();
@@ -11764,11 +12006,11 @@ var init_jikji_find_tool = __esm(() => {
11764
12006
 
11765
12007
  // src/agent/local-model.ts
11766
12008
  import { readFileSync as readFileSync12 } from "node:fs";
11767
- import { homedir as homedir2 } from "node:os";
12009
+ import { homedir as homedir3 } from "node:os";
11768
12010
  import { join as join24 } from "node:path";
11769
12011
  import { parse as parse2 } from "smol-toml";
11770
12012
  function loadLocalAutoRAGModel(options = {}) {
11771
- const configPath = options.configPath ?? join24(homedir2(), ".codex", "config.toml");
12013
+ const configPath = options.configPath ?? join24(homedir3(), ".codex", "config.toml");
11772
12014
  const config = parse2(readFileSync12(configPath, "utf8"));
11773
12015
  const provider = config.model_provider;
11774
12016
  if (typeof provider !== "string" || provider.length === 0) {
@@ -11811,7 +12053,7 @@ var init_local_model = () => {};
11811
12053
 
11812
12054
  // src/p2p/approval-store.ts
11813
12055
  import {
11814
- existsSync as existsSync16,
12056
+ existsSync as existsSync17,
11815
12057
  mkdirSync as mkdirSync11,
11816
12058
  readdirSync as readdirSync3,
11817
12059
  readFileSync as readFileSync13,
@@ -11876,7 +12118,7 @@ function savePendingPeerRequest(workspacePath, request) {
11876
12118
  }
11877
12119
  function loadPendingPeerRequest(workspacePath, id) {
11878
12120
  const path = requestPath(workspacePath, id);
11879
- if (!existsSync16(path))
12121
+ if (!existsSync17(path))
11880
12122
  return;
11881
12123
  try {
11882
12124
  return parsePending(JSON.parse(readFileSync13(path, "utf8")));
@@ -11886,7 +12128,7 @@ function loadPendingPeerRequest(workspacePath, id) {
11886
12128
  }
11887
12129
  function listPendingPeerRequests(workspacePath) {
11888
12130
  const dir = requestsDir(workspacePath);
11889
- if (!existsSync16(dir))
12131
+ if (!existsSync17(dir))
11890
12132
  return [];
11891
12133
  const requests = [];
11892
12134
  for (const name of readdirSync3(dir)) {
@@ -11908,7 +12150,7 @@ function writePeerRequestDecision(workspacePath, id, decision) {
11908
12150
  }
11909
12151
  function loadPeerRequestDecision(workspacePath, id) {
11910
12152
  const path = decisionPath(workspacePath, id);
11911
- if (!existsSync16(path))
12153
+ if (!existsSync17(path))
11912
12154
  return;
11913
12155
  try {
11914
12156
  return parseDecision(JSON.parse(readFileSync13(path, "utf8")));
@@ -12242,7 +12484,7 @@ var init_egress_gate = __esm(() => {
12242
12484
  });
12243
12485
 
12244
12486
  // src/p2p/policy.ts
12245
- import { existsSync as existsSync17, mkdirSync as mkdirSync12, readFileSync as readFileSync14, writeFileSync as writeFileSync10 } from "node:fs";
12487
+ import { existsSync as existsSync18, mkdirSync as mkdirSync12, readFileSync as readFileSync14, writeFileSync as writeFileSync10 } from "node:fs";
12246
12488
  import { basename as basename10, dirname as dirname13, join as join26, resolve as resolve12 } from "node:path";
12247
12489
  import { parse as parseToml } from "smol-toml";
12248
12490
  function isRecord11(value) {
@@ -12322,7 +12564,7 @@ function normalizePolicyEntries(value, workspacePath, context) {
12322
12564
  return entries;
12323
12565
  }
12324
12566
  function readJsonFile(path) {
12325
- if (!existsSync17(path))
12567
+ if (!existsSync18(path))
12326
12568
  return;
12327
12569
  let raw;
12328
12570
  try {
@@ -12337,7 +12579,7 @@ function readJsonFile(path) {
12337
12579
  }
12338
12580
  }
12339
12581
  function readTomlFile(path) {
12340
- if (!existsSync17(path))
12582
+ if (!existsSync18(path))
12341
12583
  return;
12342
12584
  let raw;
12343
12585
  try {
@@ -12478,7 +12720,7 @@ function globToRegExp(pattern) {
12478
12720
  return new RegExp(`${expression}$`, "u");
12479
12721
  }
12480
12722
  function loadSeenSources(path, workspacePath) {
12481
- if (!existsSync17(path))
12723
+ if (!existsSync18(path))
12482
12724
  return { promoted: new Set, pending: new Set };
12483
12725
  let parsed;
12484
12726
  try {
@@ -12785,7 +13027,7 @@ var init_wire = __esm(() => {
12785
13027
 
12786
13028
  // src/p2p/simplex-query-store.ts
12787
13029
  import { randomUUID as randomUUID5 } from "node:crypto";
12788
- import { existsSync as existsSync18, mkdirSync as mkdirSync13, readdirSync as readdirSync4, readFileSync as readFileSync15, renameSync as renameSync7, unlinkSync as unlinkSync4, writeFileSync as writeFileSync11 } from "node:fs";
13030
+ import { existsSync as existsSync19, mkdirSync as mkdirSync13, readdirSync as readdirSync4, readFileSync as readFileSync15, renameSync as renameSync7, unlinkSync as unlinkSync4, writeFileSync as writeFileSync11 } from "node:fs";
12789
13031
  import { dirname as dirname14, join as join27 } from "node:path";
12790
13032
  import { Value as Value2 } from "typebox/value";
12791
13033
  function statePath(workspacePath, directory, id) {
@@ -12821,7 +13063,7 @@ function parseState(value) {
12821
13063
  return value;
12822
13064
  }
12823
13065
  function readState(path) {
12824
- if (!existsSync18(path))
13066
+ if (!existsSync19(path))
12825
13067
  return;
12826
13068
  try {
12827
13069
  return parseState(JSON.parse(readFileSync15(path, "utf8")));
@@ -12848,14 +13090,14 @@ function moveState(workspacePath, from, to, state) {
12848
13090
  }
12849
13091
  function recoverClaims(workspacePath) {
12850
13092
  const directory = join27(workspacePath, QUERY_DIR, PENDING_DIR);
12851
- if (!existsSync18(directory))
13093
+ if (!existsSync19(directory))
12852
13094
  return;
12853
13095
  for (const name of readdirSync4(directory)) {
12854
13096
  if (!name.endsWith(".json.claim"))
12855
13097
  continue;
12856
13098
  const claim = join27(directory, name);
12857
13099
  const restored = join27(directory, name.slice(0, -".claim".length));
12858
- if (!existsSync18(restored))
13100
+ if (!existsSync19(restored))
12859
13101
  renameSync7(claim, restored);
12860
13102
  }
12861
13103
  }
@@ -12879,7 +13121,7 @@ function loadSimplexQueryState(workspacePath, id) {
12879
13121
  }
12880
13122
  function listPendingSimplexQueryStates(workspacePath) {
12881
13123
  const directory = join27(workspacePath, QUERY_DIR, PENDING_DIR);
12882
- if (!existsSync18(directory))
13124
+ if (!existsSync19(directory))
12883
13125
  return [];
12884
13126
  return readdirSync4(directory).filter((name) => name.endsWith(".json")).map((name) => readState(join27(directory, name))).filter((state) => state?.status === "pending").filter((state) => loadSimplexQueryState(workspacePath, state.id)?.status === "pending").sort((left, right) => left.createdAt.localeCompare(right.createdAt));
12885
13127
  }
@@ -13022,7 +13264,7 @@ var init_simplex_query_store = __esm(() => {
13022
13264
  });
13023
13265
 
13024
13266
  // src/p2p/simplex-server.ts
13025
- import { existsSync as existsSync19, mkdirSync as mkdirSync14, readFileSync as readFileSync16, writeFileSync as writeFileSync12 } from "node:fs";
13267
+ import { existsSync as existsSync20, mkdirSync as mkdirSync14, readFileSync as readFileSync16, writeFileSync as writeFileSync12 } from "node:fs";
13026
13268
  import { join as join28 } from "node:path";
13027
13269
  import { Type as Type10 } from "typebox";
13028
13270
  import { Value as Value3 } from "typebox/value";
@@ -13051,7 +13293,7 @@ function rankSimplexPeerTargets(query, registry) {
13051
13293
  }
13052
13294
  function loadSimplexPeerRegistry(workspacePath) {
13053
13295
  const path = join28(workspacePath, PEERS_DIR, PEERS_FILENAME);
13054
- if (!existsSync19(path))
13296
+ if (!existsSync20(path))
13055
13297
  return {};
13056
13298
  try {
13057
13299
  const parsed = JSON.parse(readFileSync16(path, "utf8"));
@@ -13957,6 +14199,7 @@ function recordStructuredResultsSession(sessionId, query, details, memory, compo
13957
14199
  }
13958
14200
  var init_search_documents = __esm(() => {
13959
14201
  init_memory();
14202
+ init_citations();
13960
14203
  });
13961
14204
 
13962
14205
  // src/agent/search-all-tool.ts
@@ -14175,14 +14418,14 @@ var init_search_single_datasource_tool = __esm(() => {
14175
14418
  });
14176
14419
 
14177
14420
  // src/agent/self-config.ts
14178
- import { existsSync as existsSync20, readFileSync as readFileSync18, rmSync as rmSync9, writeFileSync as writeFileSync14 } from "node:fs";
14421
+ import { existsSync as existsSync21, readFileSync as readFileSync18, rmSync as rmSync9, writeFileSync as writeFileSync14 } from "node:fs";
14179
14422
  import { dirname as dirname16, join as join29 } from "node:path";
14180
14423
  import { fileURLToPath as fileURLToPath3 } from "node:url";
14181
14424
  function findPackagedSetupSkill() {
14182
14425
  let dir = dirname16(fileURLToPath3(import.meta.url));
14183
14426
  for (let depth = 0;depth < 6; depth += 1) {
14184
14427
  const candidate = join29(dir, SETUP_SKILL_RELATIVE_PATH);
14185
- if (existsSync20(candidate))
14428
+ if (existsSync21(candidate))
14186
14429
  return candidate;
14187
14430
  const parent = dirname16(dir);
14188
14431
  if (parent === dir)
@@ -14205,7 +14448,7 @@ function loadSetupSkill(skillPath) {
14205
14448
  return text.replace(/^---\r?\n[\s\S]*?\r?\n---(?:\r?\n)+/u, "");
14206
14449
  }
14207
14450
  function snapshotConfigFile(configPath) {
14208
- return existsSync20(configPath) ? readFileSync18(configPath, "utf8") : undefined;
14451
+ return existsSync21(configPath) ? readFileSync18(configPath, "utf8") : undefined;
14209
14452
  }
14210
14453
  async function rollbackIfBroken(options, before) {
14211
14454
  if (options.validate === undefined)
@@ -14273,6 +14516,10 @@ function buildSystemPrompt(config) {
14273
14516
  const modelId = config.modelId ?? "the configured model";
14274
14517
  const toolLines = [
14275
14518
  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)"),
14519
+ ...config.remoteSession ? [] : [
14520
+ 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"),
14521
+ toolLine(config, "edit", "surgically update an existing deliverable or config file when the user asks (local sessions only)")
14522
+ ],
14276
14523
  toolLine(config, "jikji_find", "local discovery through Jikji answer packs"),
14277
14524
  toolLine(config, "everything_search", "instant Windows file/folder name search (voidtools Everything) across the configured search folders"),
14278
14525
  toolLine(config, "fsearch_search", "instant macOS/Linux file/folder name search (fsearch-cli/FSearch) across the configured search folders, with a slow filesystem-walk fallback"),
@@ -14289,6 +14536,12 @@ function buildSystemPrompt(config) {
14289
14536
  ...config.toolNames.filter((name) => name.startsWith("search_datasource_")).map((name) => `- **${name}**: search only the ${name.slice("search_datasource_".length).replace(/_/g, "-")} datasource connection, spawning no other datasource CLIs`),
14290
14537
  ...config.toolNames.filter((name) => ![
14291
14538
  "bash",
14539
+ "read",
14540
+ "write",
14541
+ "edit",
14542
+ "grep",
14543
+ "find",
14544
+ "ls",
14292
14545
  "jikji_find",
14293
14546
  "everything_search",
14294
14547
  "fsearch_search",
@@ -14421,8 +14674,9 @@ Your answer is your final reply, written as plain text, never as a tool call. Ci
14421
14674
  ## Answer Guidelines
14422
14675
 
14423
14676
  - **Complete vs. delta answer**: When no first answer reached the caller, give the complete core answer. When a first answer was already delivered, return only the delta against it — corrections and newly verified findings — and never repeat its unchanged content; if nothing changed, confirm the first answer in one short line.
14424
- - **Bullet-point core answer**: 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.
14425
- - **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.
14677
+ - **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.
14678
+ - **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.
14679
+ - ${ANSWER_DELIVERABLE_RULE} ${ANSWER_DELIVERABLE_DELTA_RULE}
14426
14680
  - ${ANSWER_CITATION_RULE}
14427
14681
  - ${ANSWER_IMAGE_EMBED_RULE} ${ANSWER_IMAGE_DELTA_RULE}
14428
14682
  - **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.
@@ -16717,11 +16971,6 @@ var init_types3 = __esm(() => {
16717
16971
  label: "xAI",
16718
16972
  description: "xAI Responses web_search via the agent's xAI model credentials (XAI_API_KEY)"
16719
16973
  },
16720
- {
16721
- value: "perplexity",
16722
- label: "Perplexity",
16723
- description: "Anonymous Perplexity ask endpoint; no key or signup required"
16724
- },
16725
16974
  {
16726
16975
  value: "parallel",
16727
16976
  label: "Parallel",
@@ -16732,16 +16981,21 @@ var init_types3 = __esm(() => {
16732
16981
  label: "SearXNG",
16733
16982
  description: "Advanced: requires a self-hosted SEARXNG_ENDPOINT; never part of the default experience"
16734
16983
  },
16735
- {
16736
- value: "startpage",
16737
- label: "Startpage",
16738
- description: "Credential-free scrape of Startpage (Google-backed) results; may be bot-challenged"
16739
- },
16740
16984
  {
16741
16985
  value: "duckduckgo",
16742
16986
  label: "DuckDuckGo",
16743
16987
  description: "Credential-free best-effort fallback; may be bot-challenged on datacenter/shared-egress IPs"
16744
16988
  },
16989
+ {
16990
+ value: "perplexity",
16991
+ label: "Perplexity",
16992
+ description: "Anonymous Perplexity ask endpoint; no key or signup required"
16993
+ },
16994
+ {
16995
+ value: "startpage",
16996
+ label: "Startpage",
16997
+ description: "Credential-free scrape of Startpage (Google-backed) results; may be bot-challenged"
16998
+ },
16745
16999
  {
16746
17000
  value: "ecosia",
16747
17001
  label: "Ecosia",
@@ -18089,12 +18343,11 @@ function eventAnswer(event) {
18089
18343
  }
18090
18344
  return "";
18091
18345
  }
18092
- var PERPLEXITY_ASK_URL = "https://www.perplexity.ai/rest/sse/perplexity_ask", API_VERSION2 = "2.18", DEFLECTION_PATTERN, MAX_ERROR_BYTES4, ANONYMOUS_USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36", PERPLEXITY_QUERY_SYNTAX, PerplexityProvider;
18346
+ var PERPLEXITY_ASK_URL = "https://www.perplexity.ai/rest/sse/perplexity_ask", API_VERSION2 = "2.18", MAX_ERROR_BYTES4, ANONYMOUS_USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36", PERPLEXITY_QUERY_SYNTAX, PerplexityProvider;
18093
18347
  var init_perplexity = __esm(() => {
18094
18348
  init_query();
18095
18349
  init_types3();
18096
18350
  init_utils();
18097
- DEFLECTION_PATTERN = /sign up|log in|create an account|verify you are human|are you a robot|가입|로그인|회원 ?가입/i;
18098
18351
  MAX_ERROR_BYTES4 = 8 * 1024;
18099
18352
  PERPLEXITY_QUERY_SYNTAX = {
18100
18353
  phrases: true,
@@ -18208,8 +18461,8 @@ var init_perplexity = __esm(() => {
18208
18461
  break;
18209
18462
  }
18210
18463
  const sources = [...sourcesByUrl.values()];
18211
- if (sources.length === 0 && DEFLECTION_PATTERN.test(answer)) {
18212
- throw new SearchProviderError("perplexity", "Perplexity anonymous ask was deflected with a sign-up wall", 403);
18464
+ if (sources.length === 0) {
18465
+ throw new SearchProviderError("perplexity", `Perplexity anonymous ask returned no web sources (sign-up wall or conversational deflection): ${answer.slice(0, 120)}`, 403);
18213
18466
  }
18214
18467
  const numResults = params.numSearchResults ?? params.limit;
18215
18468
  return {
@@ -18627,7 +18880,7 @@ var init_browser_headers = __esm(() => {
18627
18880
  });
18628
18881
 
18629
18882
  // src/web/search/browser-loader.ts
18630
- import { existsSync as existsSync21 } from "node:fs";
18883
+ import { existsSync as existsSync22 } from "node:fs";
18631
18884
  async function closeBounded(close) {
18632
18885
  let timer;
18633
18886
  try {
@@ -18706,7 +18959,7 @@ function ensureWebSearchBrowserLoader(options = {}) {
18706
18959
  return true;
18707
18960
  const exists = options.exists ?? ((path) => {
18708
18961
  try {
18709
- return existsSync21(path);
18962
+ return existsSync22(path);
18710
18963
  } catch {
18711
18964
  return false;
18712
18965
  }
@@ -18811,6 +19064,8 @@ function normalizeText(value) {
18811
19064
  function isChallengeResponse(page) {
18812
19065
  if (/\/(?:errors|captcha)\//.test(page.url) || page.url.includes("/sp/captcha"))
18813
19066
  return true;
19067
+ if (ANUBIS_CHALLENGE_PATTERN.test(page.html))
19068
+ return true;
18814
19069
  return page.html.includes("component---src-pages-captcha") || page.html.includes("/sp/captcha");
18815
19070
  }
18816
19071
  function parseSearchFormInputs(html) {
@@ -18915,7 +19170,7 @@ async function callStartpageHtml(params) {
18915
19170
  });
18916
19171
  }
18917
19172
  if (isChallengeResponse(page)) {
18918
- throw new SearchProviderError("startpage", "Startpage blocked the request with a CAPTCHA challenge. Startpage rate-limits automated searches from datacenter/shared-egress IPs; try another provider such as DuckDuckGo or Mojeek, or retry later.", 429);
19173
+ throw new SearchProviderError("startpage", "Startpage blocked the request with a bot challenge (CAPTCHA or Anubis proof-of-work). Startpage rate-limits automated searches from datacenter/shared-egress IPs; try another provider such as DuckDuckGo or Mojeek, or retry later.", 429);
18919
19174
  }
18920
19175
  if (page.status < 200 || page.status >= 300) {
18921
19176
  const classified = classifyProviderHttpError("startpage", page.status, page.html);
@@ -18941,7 +19196,7 @@ async function searchStartpage(params) {
18941
19196
  }
18942
19197
  return { provider: "startpage", sources };
18943
19198
  }
18944
- var STARTPAGE_HOME_URL = "https://www.startpage.com/", STARTPAGE_SEARCH_URL = "https://www.startpage.com/sp/search", DEFAULT_NUM_RESULTS3 = 10, MAX_NUM_RESULTS3 = 20, RECENCY_TO_STARTPAGE_WITH_DATE, StartpageProvider;
19199
+ var STARTPAGE_HOME_URL = "https://www.startpage.com/", STARTPAGE_SEARCH_URL = "https://www.startpage.com/sp/search", DEFAULT_NUM_RESULTS3 = 10, MAX_NUM_RESULTS3 = 20, RECENCY_TO_STARTPAGE_WITH_DATE, ANUBIS_CHALLENGE_PATTERN, StartpageProvider;
18945
19200
  var init_startpage = __esm(() => {
18946
19201
  init_entities();
18947
19202
  init_browser_page();
@@ -18954,6 +19209,7 @@ var init_startpage = __esm(() => {
18954
19209
  month: "m",
18955
19210
  year: "y"
18956
19211
  };
19212
+ ANUBIS_CHALLENGE_PATTERN = /<script\b[^>]*\bid\s*=\s*["']anubis_(?:challenge|version)["']/i;
18957
19213
  StartpageProvider = class StartpageProvider extends SearchProvider {
18958
19214
  id = "startpage";
18959
19215
  label = "Startpage";
@@ -20171,7 +20427,7 @@ var init_web_search_tool = __esm(() => {
20171
20427
 
20172
20428
  // src/agent/agent.ts
20173
20429
  import { randomUUID as randomUUID7 } from "node:crypto";
20174
- import { existsSync as existsSync22, watch as fsWatch, mkdirSync as mkdirSync16, realpathSync as realpathSync5, statSync as statSync2, writeFileSync as writeFileSync15 } from "node:fs";
20430
+ import { existsSync as existsSync23, watch as fsWatch, mkdirSync as mkdirSync16, realpathSync as realpathSync5, statSync as statSync2, writeFileSync as writeFileSync15 } from "node:fs";
20175
20431
  import { basename as basename11, dirname as dirname17, join as join31, resolve as resolve13 } from "node:path";
20176
20432
  import { clampThinkingLevel } from "@earendil-works/pi-ai/compat";
20177
20433
  import { getAgentDir as getAgentDir3 } from "@earendil-works/pi-coding-agent";
@@ -20302,6 +20558,9 @@ class AutoRAGAgent {
20302
20558
  piSessionDir;
20303
20559
  persistPiSessions;
20304
20560
  updateNotice;
20561
+ piExtensionPaths;
20562
+ piExtensionFlags;
20563
+ telegram;
20305
20564
  boundPiRuntime;
20306
20565
  memoryEmbedder;
20307
20566
  memoryVectorPath;
@@ -20327,6 +20586,9 @@ class AutoRAGAgent {
20327
20586
  this.piSessionDir = options.piSessionDir;
20328
20587
  this.persistPiSessions = options.persistPiSessions ?? true;
20329
20588
  this.updateNotice = options.updateNotice;
20589
+ this.piExtensionPaths = options.piExtensionPaths;
20590
+ this.piExtensionFlags = options.piExtensionFlags;
20591
+ this.telegram = options.telegram === true;
20330
20592
  this.datasourceSkills = options.datasourceSkills ?? [];
20331
20593
  this.datasourceRetry = options.datasourceRetry ?? {};
20332
20594
  this.datasourceVirtualScopePrefixes = this.datasourceSkills.map((skill) => normalizeVirtualPath2(`/${skill.describe().name}`));
@@ -20379,7 +20641,8 @@ class AutoRAGAgent {
20379
20641
  const everythingClient = new EverythingClient({
20380
20642
  ...options.everything ?? {},
20381
20643
  root: this.workspaceProjectRoot,
20382
- folders: this.searchPaths
20644
+ folders: this.searchPaths,
20645
+ excludeFolders: this.excludePaths
20383
20646
  });
20384
20647
  if (everythingClient.isSupported())
20385
20648
  this.everythingClient = everythingClient;
@@ -20485,8 +20748,9 @@ class AutoRAGAgent {
20485
20748
  return true;
20486
20749
  });
20487
20750
  this.tools = tools;
20751
+ const builtinToolNames = this.remoteSession ? PI_BUILTIN_TOOL_NAMES.filter((name) => name !== "write" && name !== "edit") : PI_BUILTIN_TOOL_NAMES;
20488
20752
  const toolNames = [
20489
- ...PI_BUILTIN_TOOL_NAMES,
20753
+ ...builtinToolNames,
20490
20754
  ...tools.map((tool) => tool.name),
20491
20755
  ...this.jevExtension !== undefined ? [JEV_TOOL_NAME] : []
20492
20756
  ];
@@ -20502,7 +20766,7 @@ class AutoRAGAgent {
20502
20766
  this.innerAgent = {
20503
20767
  state: {
20504
20768
  systemPrompt,
20505
- tools: [...PI_BUILTIN_TOOL_NAMES.map((name) => ({ name })), ...tools],
20769
+ tools: [...builtinToolNames.map((name) => ({ name })), ...tools],
20506
20770
  messages: []
20507
20771
  },
20508
20772
  transformContext: (messages) => this.withMemoryContext(messages)
@@ -20796,6 +21060,7 @@ ${context.text}
20796
21060
  }
20797
21061
  async createPiInteractiveRuntime() {
20798
21062
  const resolved = this.configuredModel === undefined ? undefined : await this.resolveSessionModel();
21063
+ const telegramBridge = this.telegram ? (await import("@llblab/pi-telegram")).default : undefined;
20799
21064
  const runtime = await createAutoRAGPiInteractiveRuntime({
20800
21065
  cwd: this.workspaceProjectRoot,
20801
21066
  agentDir: this.piAgentDir,
@@ -20816,10 +21081,14 @@ ${context.text}
20816
21081
  onKey: (key) => this.handleInteractiveKey(key),
20817
21082
  extensionFactories: [
20818
21083
  createEvidencePersistence(this.evidenceLedger),
20819
- ...this.jevExtension !== undefined ? [this.jevExtension] : []
21084
+ ...this.jevExtension !== undefined ? [this.jevExtension] : [],
21085
+ ...telegramBridge !== undefined ? [telegramBridge] : []
20820
21086
  ],
20821
21087
  ...this.jevExtension !== undefined ? { extensionToolNames: [JEV_TOOL_NAME] } : {},
20822
- ...this.updateNotice === undefined ? {} : { updateNotice: this.updateNotice }
21088
+ remoteSession: this.remoteSession,
21089
+ ...this.updateNotice === undefined ? {} : { updateNotice: this.updateNotice },
21090
+ ...this.piExtensionPaths === undefined ? {} : { extensionPaths: this.piExtensionPaths },
21091
+ ...this.piExtensionFlags === undefined ? {} : { extensionFlags: this.piExtensionFlags }
20823
21092
  });
20824
21093
  this.boundPiRuntime = runtime.runtime;
20825
21094
  const dispose = runtime.dispose;
@@ -21739,21 +22008,22 @@ ${result.content.replace(/\s+/gu, " ")}`).join(`
21739
22008
  const scope = options.scope ? ` Restrict search to virtual path scope ${options.scope}.` : "";
21740
22009
  return `Answer this original query immediately: ${query}${limit}${scope}
21741
22010
 
21742
- ` + `Baseline retrieval evidence (already gathered for you):
22011
+ Baseline retrieval evidence (already gathered for you):
21743
22012
  ${baseline}
21744
22013
 
21745
- ` + `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.
22014
+ Produce the best complete, self-contained answer you can RIGHT NOW from this evidence. Do NOT call any search, retrieval, or file-reading tools and do NOT wait for more evidence. If the query is answerable from general knowledge alone, answer directly.
21746
22015
 
21747
- ` + `Formatting and content rules for the answer:
21748
- ` + `- 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.
21749
- ` + `- Answer the question directly. Do not include specific file paths, datasource descriptions, or retrieval mechanics in the answer text.
21750
- ` + `- ${ANSWER_CITATION_RULE}
21751
- ` + `- ${ANSWER_IMAGE_EMBED_RULE}
21752
- ` + `- Do not report per-source negative findings (e.g. "no information found in Slack" or "checked Drive but found nothing").
21753
- ` + `- When evidence conflicts, treat the freshest (most recent) information as the correct source of truth.
21754
- ` + `- If information is incomplete or uncertain, acknowledge it briefly without lengthy explanations, stating that it is difficult to answer fully with the given information and searching continues. If there are partial clues or leads (even if not the exact answer), mention those clues concisely.
22016
+ Formatting and content rules for the answer:
22017
+ - 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.
22018
+ - 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.
22019
+ - ${ANSWER_DELIVERABLE_RULE}
22020
+ - ${ANSWER_CITATION_RULE}
22021
+ - ${ANSWER_IMAGE_EMBED_RULE}
22022
+ - Do not report per-source negative findings (e.g. "no information found in Slack" or "checked Drive but found nothing").
22023
+ - When evidence conflicts, treat the freshest (most recent) information as the correct source of truth.
22024
+ - If information is incomplete or uncertain, acknowledge it briefly without lengthy explanations, stating that it is difficult to answer fully with the given information and searching continues. If there are partial clues or leads (even if not the exact answer), mention those clues concisely.
21755
22025
 
21756
- ` + `Write the answer as your reply, then stop.`;
22026
+ Write the answer as your reply, then stop.`;
21757
22027
  }
21758
22028
  isConversationTurn() {
21759
22029
  if (this.remoteSession || this.boundPiRuntime === undefined)
@@ -21763,7 +22033,7 @@ ${baseline}
21763
22033
  buildDirectAnswerPrompt(query) {
21764
22034
  return `Answer this query directly from your own general knowledge: ${query}
21765
22035
 
21766
- ` + `It needs no search: it is general knowledge, simple reasoning, or conversation. Do NOT call any search, retrieval, web, or file-reading tools. ` + `Reply naturally and concisely; for small talk, just respond conversationally. Do not cite sources or mention retrieval.`;
22036
+ It needs no search: it is general knowledge, simple reasoning, or conversation. Do NOT call any search, retrieval, web, or file-reading tools. Reply naturally and concisely; for small talk, just respond conversationally. Do not cite sources or mention retrieval.`;
21767
22037
  }
21768
22038
  discoveryHint(sentence) {
21769
22039
  const names = this.tools.map((tool) => tool.name).filter((name) => name === JIKJI_FIND_TOOL_NAME || name === EVERYTHING_SEARCH_TOOL_NAME);
@@ -21781,52 +22051,55 @@ ${baseline}
21781
22051
  const scope = options.scope ? ` Restrict search to virtual path scope ${options.scope}.` : "";
21782
22052
  const firstAnswer = formatFirstAnswerContext(fastAnswer, fastAnswerDelivered);
21783
22053
  const answerRules = fastAnswerDelivered ? `Formatting and content rules for your final answer (DELTA ONLY):
21784
- ` + `- The user already has the first answer above. Your final answer MUST contain only the delta against it: (a) corrections to anything in the first answer that is wrong, unsupported, or outdated, and (b) newly verified findings that were not present in the first answer.
21785
- ` + `- NEVER restate or re-list first-answer facts that remain correct, and never repeat its bullet list.
21786
- ` + `- Mark each item clearly as a correction or as a new finding.
21787
- ` + `- 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.
21788
- ` + `- ${ANSWER_CITATION_RULE} A correction or new finding that relies on evidence the first answer cited must cite that evidence's id again.
21789
- ` + `- ${ANSWER_IMAGE_EMBED_RULE} ${ANSWER_IMAGE_DELTA_RULE}
21790
- ` + `- Do not report per-source negative findings (e.g. "no information found in Slack").
21791
- ` + `- When evidence conflicts, treat the freshest (most recent) information as the correct source of truth.` : `Formatting and content rules for the final answer (COMPLETE — no first answer reached the caller):
21792
- ` + `- 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.
21793
- ` + `- Answer the question directly. Do not include specific file paths, datasource descriptions, or retrieval mechanics in the answer text.
21794
- ` + `- ${ANSWER_CITATION_RULE}
21795
- ` + `- ${ANSWER_IMAGE_EMBED_RULE}
21796
- ` + `- Do not report per-source negative findings (e.g. "no information found in Slack").
21797
- ` + `- When evidence conflicts, treat the freshest (most recent) information as the correct source of truth.`;
22054
+ - The user already has the first answer above. Your final answer MUST contain only the delta against it: (a) corrections to anything in the first answer that is wrong, unsupported, or outdated, and (b) newly verified findings that were not present in the first answer.
22055
+ - NEVER restate or re-list first-answer facts that remain correct, and never repeat its bullet list.
22056
+ - Mark each item clearly as a correction or as a new finding.
22057
+ - 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.
22058
+ - ${ANSWER_DELIVERABLE_RULE} ${ANSWER_DELIVERABLE_DELTA_RULE}
22059
+ - ${ANSWER_CITATION_RULE} A correction or new finding that relies on evidence the first answer cited must cite that evidence's id again.
22060
+ - ${ANSWER_IMAGE_EMBED_RULE} ${ANSWER_IMAGE_DELTA_RULE}
22061
+ - Do not report per-source negative findings (e.g. "no information found in Slack").
22062
+ - 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):
22063
+ ` + `- 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.
22064
+ - 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.
22065
+ - ${ANSWER_DELIVERABLE_RULE}${fastAnswer !== undefined ? ` ${ANSWER_DELIVERABLE_DELTA_RULE}` : ""}
22066
+ - ${ANSWER_CITATION_RULE}
22067
+ - ${ANSWER_IMAGE_EMBED_RULE}
22068
+ - Do not report per-source negative findings (e.g. "no information found in Slack").
22069
+ - When evidence conflicts, treat the freshest (most recent) information as the correct source of truth.`;
21798
22070
  return `Original query: ${query}${limit}${scope}
21799
22071
 
21800
- ` + `${firstAnswer}
22072
+ ${firstAnswer}
21801
22073
 
21802
22074
  ` + formatSteerContext(steers) + `Now verify it rigorously. ${this.discoveryHint((tools) => `Actively use ${tools} when discovering or exploring local files and folders. `)}Check important claims against source files with bash when needed, correct anything wrong or unsupported, fill gaps with retrieval tools, and resolve conflicts and freshness. ${this.webFallbackHint()}
21803
22075
 
21804
- ` + `${answerRules}
22076
+ ${answerRules}
21805
22077
 
21806
- ` + `Do not use broad grep/find or recursive filesystem scans: only inspect a path or narrow neighborhood surfaced by retrieval, and only when evidence clearly points there. ` + `Avoid spinning repeated near-identical queries against the same datasource; once additional attempts stop surfacing new evidence, conclude from the evidence available. ` + `If more search is needed, first write a brief 1–2 line progress update stating the best current hypothesis and what you are checking next, then call retrieval tools. ` + `When finished, reply with the final answer and no tool call: that last reply is what the user receives, so keep progress notes out of it.`;
22078
+ Do not use broad grep/find or recursive filesystem scans: only inspect a path or narrow neighborhood surfaced by retrieval, and only when evidence clearly points there. Avoid spinning repeated near-identical queries against the same datasource; once additional attempts stop surfacing new evidence, conclude from the evidence available. ` + `If more search is needed, first write a brief 1–2 line progress update stating the best current hypothesis and what you are checking next, then call retrieval tools. ` + `When finished, reply with the final answer and no tool call: that last reply is what the user receives, so keep progress notes out of it.`;
21807
22079
  }
21808
22080
  buildSearchPrompt(query, options, initialRetrievalContext) {
21809
22081
  const limit = typeof options.topK === "number" ? ` Cite at most ${options.topK} sources.` : "";
21810
22082
  const scope = options.scope ? ` Restrict search to virtual path scope ${options.scope}.` : "";
21811
22083
  return `Find and curate information for this original query: ${query}${limit}${scope}
21812
22084
 
21813
- ` + `Start by deciding whether this is answerable from general knowledge or memory. If it is a generic, stable question, answer it immediately without retrieval. ` + `Otherwise, baseline MinSync and Jikji retrieval is already running in parallel; do not give the final answer until its next message arrives.
22085
+ Start by deciding whether this is answerable from general knowledge or memory. If it is a generic, stable question, answer it immediately without retrieval. Otherwise, baseline MinSync and Jikji retrieval is already running in parallel; do not give the final answer until its next message arrives.
21814
22086
 
21815
- ` + `Baseline retrieval context:
22087
+ Baseline retrieval context:
21816
22088
  ${initialRetrievalContext ?? "Pending; continue only with a brief progress statement."}
21817
22089
 
21818
- ` + `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.
22090
+ Treat candidates as unverified evidence, verify important claims against source files when needed, and use additional tools when needed. Judge relevance, conflicts, freshness, and sufficiency in this agent loop.
21819
22091
 
21820
- ` + `Formatting and content rules for the answer:
21821
- ` + `- 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.
21822
- ` + `- Answer the question directly. Do not include specific file paths, datasource descriptions, or retrieval mechanics in the answer text.
21823
- ` + `- ${ANSWER_CITATION_RULE}
21824
- ` + `- ${ANSWER_IMAGE_EMBED_RULE}
21825
- ` + `- Do not report per-source negative findings (e.g. "no information found in Slack").
21826
- ` + `- When evidence conflicts, treat the freshest (most recent) information as the correct source of truth.
21827
- ` + `- If information is incomplete or uncertain, acknowledge it briefly without lengthy explanations. If there are partial clues or leads, mention them concisely.
22092
+ Formatting and content rules for the answer:
22093
+ - 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.
22094
+ - 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.
22095
+ - ${ANSWER_DELIVERABLE_RULE}
22096
+ - ${ANSWER_CITATION_RULE}
22097
+ - ${ANSWER_IMAGE_EMBED_RULE}
22098
+ - Do not report per-source negative findings (e.g. "no information found in Slack").
22099
+ - When evidence conflicts, treat the freshest (most recent) information as the correct source of truth.
22100
+ - If information is incomplete or uncertain, acknowledge it briefly without lengthy explanations. If there are partial clues or leads, mention them concisely.
21828
22101
 
21829
- ` + `${this.discoveryHint((tools) => `When exploring local files and folders, actively use ${tools} rather than exploratory bash commands. `)}If more search is needed, first write a brief 1–2 line progress update stating the best current hypothesis and what you are checking next, then call retrieval tools. ` + `Never repeat a generic status message. Do not use broad grep/find or recursive filesystem scans: only inspect a path or narrow neighborhood surfaced by retrieval, and only when evidence clearly points there. ` + `Avoid spinning repeated near-identical queries against the same datasource; once additional attempts stop surfacing new evidence, conclude from the evidence available. ` + `When finished, reply with the final answer and no tool call: that last reply is what the user receives, so keep progress notes out of it.`;
22102
+ ${this.discoveryHint((tools) => `When exploring local files and folders, actively use ${tools} rather than exploratory bash commands. `)}If more search is needed, first write a brief 1–2 line progress update stating the best current hypothesis and what you are checking next, then call retrieval tools. ` + `Never repeat a generic status message. Do not use broad grep/find or recursive filesystem scans: only inspect a path or narrow neighborhood surfaced by retrieval, and only when evidence clearly points there. Avoid spinning repeated near-identical queries against the same datasource; once additional attempts stop surfacing new evidence, conclude from the evidence available. When finished, reply with the final answer and no tool call: that last reply is what the user receives, so keep progress notes out of it.`;
21830
22103
  }
21831
22104
  async refresh(force = false, opts) {
21832
22105
  const signal = opts?.signal;
@@ -22062,7 +22335,7 @@ ${initialRetrievalContext ?? "Pending; continue only with a brief progress state
22062
22335
  });
22063
22336
  }
22064
22337
  const state = this.refreshState.inFlight || persistedRunning && ownerAlive ? "indexing" : interrupted ? "failed" : persistedProgress?.state === "success" || persistedProgress?.state === "failed" ? persistedProgress.state : this.refreshState.lastOutcome === "never" ? "idle" : this.refreshState.lastOutcome;
22065
- const parsedMirrorReady = this.refreshState.lastOutcome === "success" || persistedProgress?.state === "success" || existsSync22(refreshReadinessPath(this.workspaceProjectRoot));
22338
+ const parsedMirrorReady = this.refreshState.lastOutcome === "success" || persistedProgress?.state === "success" || existsSync23(refreshReadinessPath(this.workspaceProjectRoot));
22066
22339
  const effectiveProgress = this.refreshState.inFlight || persistedRunning ? persistedProgress : undefined;
22067
22340
  return {
22068
22341
  state,
@@ -22522,7 +22795,7 @@ function buildSteerMessage(text) {
22522
22795
 
22523
22796
  ${text}
22524
22797
 
22525
- ` + `Treat it as direction for the search in progress, not as a new question: adjust which sources, queries, or claims you pursue, then continue. ` + `When finished, reply with the final answer to the original question, updated for this correction, and no tool call.`;
22798
+ Treat it as direction for the search in progress, not as a new question: adjust which sources, queries, or claims you pursue, then continue. When finished, reply with the final answer to the original question, updated for this correction, and no tool call.`;
22526
22799
  }
22527
22800
  function frameSteers(messages, steerTexts) {
22528
22801
  if (steerTexts.size === 0)
@@ -22542,12 +22815,12 @@ function formatSteerContext(steers) {
22542
22815
  return `While the search ran, the user corrected it:
22543
22816
  ${steers.map((steer) => `- ${steer}`).join(`
22544
22817
  `)}
22545
- ` + `The first answer predates these corrections; follow them over it.
22818
+ The first answer predates these corrections; follow them over it.
22546
22819
 
22547
22820
  `;
22548
22821
  }
22549
22822
  function buildSteerContinuationPrompt(steers) {
22550
- return `${formatSteerContext(steers)}Apply the latest correction: search or verify further if it needs that, ` + `then reply with the complete final answer to the original question and no tool call.`;
22823
+ return `${formatSteerContext(steers)}Apply the latest correction: search or verify further if it needs that, then reply with the complete final answer to the original question and no tool call.`;
22551
22824
  }
22552
22825
  function formatFirstAnswerContext(fastAnswer, delivered) {
22553
22826
  if (fastAnswer === undefined) {
@@ -22670,11 +22943,20 @@ function fsearchRefreshDiagnostics(fsearch) {
22670
22943
  function minSyncRefreshDiagnostics(minsync) {
22671
22944
  if (!minsync)
22672
22945
  return [];
22946
+ const diagnostics = [];
22673
22947
  if (minsync.diagnostic)
22674
- return [toMinSyncDiagnostic(minsync.diagnostic, minsync.ok)];
22675
- if (!minsync.ok && minsync.reason)
22676
- return [toMinSyncReasonDiagnostic(minsync.reason)];
22677
- return [];
22948
+ diagnostics.push(toMinSyncDiagnostic(minsync.diagnostic, minsync.ok));
22949
+ else if (!minsync.ok && minsync.reason)
22950
+ diagnostics.push(toMinSyncReasonDiagnostic(minsync.reason));
22951
+ if (minsync.lexicalFallback) {
22952
+ diagnostics.push({
22953
+ code: "lexical-tokenizer-unavailable",
22954
+ severity: "warning",
22955
+ message: `The "${minsync.lexicalFallback.requested}" BM25 tokenizer needs the Kiwi native library and model, which were not found, so MinSync tokenizes on whitespace and a Korean word with a particle (인증서를) does not match its stem (인증서). Install Kiwi (set KIWI_LIBRARY_PATH and KIWI_MODEL_PATH, or install it under ~/.local/kiwi), then refresh again.`,
22956
+ source: "minsync"
22957
+ });
22958
+ }
22959
+ return diagnostics;
22678
22960
  }
22679
22961
  function toMinSyncDiagnostic(diag, ok) {
22680
22962
  const code = diag.code === "embedder-unavailable" ? "embedder-unavailable" : diag.code === "embedding-identity-mismatch" ? "embedding-identity-mismatch" : diag.code === "minsync-lock-held" ? "minsync-lock-held" : "minsync-sync-failed";
@@ -22837,7 +23119,7 @@ var init_results = __esm(() => {
22837
23119
  confidence: Type18.Optional(Type18.Number({ description: "Evidence confidence, 0..1", minimum: 0, maximum: 1 }))
22838
23120
  });
22839
23121
  answerSchema = Type18.String({
22840
- 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}`
23122
+ description: `Answer for the caller. Unless the user explicitly requests otherwise, provide at most 5 bullet points (plus optional explanation) and answer directly; reference results by bracketed number (e.g. [1], [2]) without file paths or raw chunk text, except the image-embed exception below. ${ANSWER_DELIVERABLE_RULE} ${ANSWER_CITATION_RULE} ${ANSWER_IMAGE_EMBED_RULE}`
22841
23123
  });
22842
23124
  warningsSchema = Type18.Optional(Type18.Array(Type18.String(), { description: "Optional warnings about this result set" }));
22843
23125
  curatedResultFields = {
@@ -23064,7 +23346,7 @@ var init_connector = () => {};
23064
23346
 
23065
23347
  // src/datasource/sidecar/config.ts
23066
23348
  import { readFileSync as readFileSync19 } from "node:fs";
23067
- import { homedir as homedir3 } from "node:os";
23349
+ import { homedir as homedir4 } from "node:os";
23068
23350
  import { dirname as dirname18, isAbsolute as isAbsolute8, join as join32, resolve as resolve14, sep as sep7 } from "node:path";
23069
23351
  import { parse as parseToml2 } from "smol-toml";
23070
23352
  function resolveSidecarOptions(input) {
@@ -23104,9 +23386,9 @@ function archivePathFromCrawlerConfig(input) {
23104
23386
  }
23105
23387
  function expandHome(path) {
23106
23388
  if (path === "~")
23107
- return homedir3();
23389
+ return homedir4();
23108
23390
  const prefixed = path.startsWith("~/") || sep7 === "\\" && path.startsWith("~\\");
23109
- return prefixed ? join32(homedir3(), path.slice(2)) : path;
23391
+ return prefixed ? join32(homedir4(), path.slice(2)) : path;
23110
23392
  }
23111
23393
  var init_config2 = __esm(() => {
23112
23394
  init_connector();
@@ -23135,7 +23417,7 @@ function chatCrawler(id) {
23135
23417
  hierarchy: (metadata) => ["chats", str(metadata.chatName) ?? str(metadata.chatId) ?? "unknown"]
23136
23418
  };
23137
23419
  }
23138
- var str = (value) => typeof value === "string" && value.length > 0 ? value : undefined, slack, CRAWLER_DESCRIPTORS;
23420
+ var str = (value) => typeof value === "string" && value.length > 0 ? value : undefined, slack, kakao, CRAWLER_DESCRIPTORS;
23139
23421
  var init_descriptors = __esm(() => {
23140
23422
  slack = {
23141
23423
  id: "slack",
@@ -23166,16 +23448,38 @@ var init_descriptors = __esm(() => {
23166
23448
  str(metadata.channelName) ?? str(metadata.channelId) ?? "unknown"
23167
23449
  ]
23168
23450
  };
23451
+ kakao = {
23452
+ id: "kakao",
23453
+ keyExpression: "m.chunk_id",
23454
+ chunkId: (key) => key,
23455
+ fromClause: "chunks m",
23456
+ 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))",
23457
+ textExpression: "coalesce(m.text, '')",
23458
+ titleExpression: "m.chat_name",
23459
+ epochSecondsExpression: "unixepoch(m.started_at)",
23460
+ fts: { table: "chunks_fts", joinOn: "chunks_fts.rowid = m.rowid" },
23461
+ metadata: [
23462
+ { sql: "m.chat_id", key: "chatId" },
23463
+ { sql: "m.chat_name", key: "chatName" },
23464
+ { sql: "m.sender_nickname", key: "senderNickname" },
23465
+ { sql: "m.started_at", key: "startedAt" },
23466
+ { sql: "m.ended_at", key: "endedAt" }
23467
+ ],
23468
+ channelIdExpression: "m.chat_id",
23469
+ channelNameExpression: "m.chat_name",
23470
+ hierarchy: (metadata) => ["chats", str(metadata.chatName) ?? str(metadata.chatId) ?? "unknown"]
23471
+ };
23169
23472
  CRAWLER_DESCRIPTORS = {
23170
23473
  slack,
23171
23474
  telegram: chatCrawler("telegram"),
23172
- whatsapp: chatCrawler("whatsapp")
23475
+ whatsapp: chatCrawler("whatsapp"),
23476
+ kakao
23173
23477
  };
23174
23478
  });
23175
23479
 
23176
23480
  // src/datasource/skills/clawgallery/client.ts
23177
23481
  import { spawn as spawn10 } from "node:child_process";
23178
- import { existsSync as existsSync23 } from "node:fs";
23482
+ import { existsSync as existsSync24 } from "node:fs";
23179
23483
 
23180
23484
  class ClawGalleryClient {
23181
23485
  options;
@@ -23183,7 +23487,7 @@ class ClawGalleryClient {
23183
23487
  this.options = options;
23184
23488
  }
23185
23489
  async bootstrap(signal) {
23186
- if (this.options.path !== undefined && !existsSync23(this.options.path)) {
23490
+ if (this.options.path !== undefined && !existsSync24(this.options.path)) {
23187
23491
  return {
23188
23492
  ok: false,
23189
23493
  reason: "nonzero-exit",
@@ -24101,7 +24405,7 @@ var init_rclone_connector = __esm(() => {
24101
24405
  });
24102
24406
 
24103
24407
  // src/datasource/chunk-store.ts
24104
- import { existsSync as existsSync24, mkdirSync as mkdirSync18, readFileSync as readFileSync21, writeFileSync as writeFileSync17 } from "node:fs";
24408
+ import { existsSync as existsSync25, mkdirSync as mkdirSync18, readFileSync as readFileSync21, writeFileSync as writeFileSync17 } from "node:fs";
24105
24409
  import { dirname as dirname20, join as join34 } from "node:path";
24106
24410
  function datasourceChunkStorePath(workspaceRoot, skillName, instanceId) {
24107
24411
  return join34(workspaceRoot, ".autorag", "datasources", sanitizeIdSegment(skillName), sanitizeIdSegment(instanceId), "chunks.json");
@@ -24141,7 +24445,7 @@ class DatasourceChunkStore {
24141
24445
  if (this.storePath === undefined)
24142
24446
  return false;
24143
24447
  try {
24144
- if (!existsSync24(this.storePath))
24448
+ if (!existsSync25(this.storePath))
24145
24449
  return false;
24146
24450
  const parsed = JSON.parse(readFileSync21(this.storePath, "utf8"));
24147
24451
  if (parsed.version !== STORE_VERSION || !Array.isArray(parsed.chunks))
@@ -25668,7 +25972,7 @@ var init_github = __esm(() => {
25668
25972
 
25669
25973
  // src/datasource/skills/github-gist/connector.ts
25670
25974
  import { execFile as execFile2 } from "node:child_process";
25671
- import { existsSync as existsSync25, mkdirSync as mkdirSync20, readFileSync as readFileSync23, writeFileSync as writeFileSync19 } from "node:fs";
25975
+ import { existsSync as existsSync26, mkdirSync as mkdirSync20, readFileSync as readFileSync23, writeFileSync as writeFileSync19 } from "node:fs";
25672
25976
  import { dirname as dirname22 } from "node:path";
25673
25977
  async function defaultGhTokenRunner() {
25674
25978
  return new Promise((resolve) => {
@@ -25788,7 +26092,7 @@ class GitHubGistConnector {
25788
26092
  }
25789
26093
  loadState() {
25790
26094
  const statePath = this.options.statePath;
25791
- if (statePath === undefined || !existsSync25(statePath))
26095
+ if (statePath === undefined || !existsSync26(statePath))
25792
26096
  return { version: STATE_VERSION, gists: {} };
25793
26097
  try {
25794
26098
  const parsed = JSON.parse(readFileSync23(statePath, "utf8"));
@@ -25851,7 +26155,7 @@ var init_connector3 = () => {};
25851
26155
 
25852
26156
  // src/datasource/skills/github-gist/semantic.ts
25853
26157
  import { createHash as createHash13 } from "node:crypto";
25854
- import { existsSync as existsSync26, mkdirSync as mkdirSync21, readFileSync as readFileSync24, writeFileSync as writeFileSync20 } from "node:fs";
26158
+ import { existsSync as existsSync27, mkdirSync as mkdirSync21, readFileSync as readFileSync24, writeFileSync as writeFileSync20 } from "node:fs";
25855
26159
  import { dirname as dirname23 } from "node:path";
25856
26160
  function contentHash(chunk) {
25857
26161
  return createHash13("sha256").update(`${chunk.title ?? ""}
@@ -25942,7 +26246,7 @@ ${chunk.content}`));
25942
26246
  if (this.loaded)
25943
26247
  return;
25944
26248
  this.loaded = true;
25945
- if (this.statePath === undefined || !existsSync26(this.statePath))
26249
+ if (this.statePath === undefined || !existsSync27(this.statePath))
25946
26250
  return;
25947
26251
  try {
25948
26252
  const parsed = JSON.parse(readFileSync24(this.statePath, "utf8"));
@@ -26805,7 +27109,7 @@ var init_types5 = __esm(() => {
26805
27109
 
26806
27110
  // src/datasource/skills/lazykatok/client.ts
26807
27111
  import { spawn as spawn14 } from "node:child_process";
26808
- import { existsSync as existsSync27 } from "node:fs";
27112
+ import { existsSync as existsSync28 } from "node:fs";
26809
27113
  import { delimiter as delimiter4, join as join36 } from "node:path";
26810
27114
 
26811
27115
  class LazykatokClient {
@@ -26995,7 +27299,7 @@ function lookupInPath2(executable, env) {
26995
27299
  if (dir.length === 0)
26996
27300
  continue;
26997
27301
  const candidate = join36(dir, executable);
26998
- if (existsSync27(candidate))
27302
+ if (existsSync28(candidate))
26999
27303
  return candidate;
27000
27304
  }
27001
27305
  return;
@@ -27317,1436 +27621,1528 @@ var init_methods3 = __esm(() => {
27317
27621
  DEFAULT_KAKAO_TAGS = ["kakaotalk", "personal", "pii"];
27318
27622
  });
27319
27623
 
27320
- // src/datasource/skills/lazykatok/skill.ts
27321
- class LazykatokSkill {
27322
- client;
27323
- instanceId;
27324
- instances;
27325
- pollingIntervalMs;
27326
- tags;
27327
- lastIndexedAt;
27328
- constructor(options) {
27329
- this.client = options.client;
27330
- this.instanceId = options.instanceId ?? DEFAULT_INSTANCE_ID4;
27331
- this.instances = options.instances !== undefined && options.instances.length > 0 ? options.instances : [this.instanceId];
27332
- this.pollingIntervalMs = options.pollingIntervalMs ?? DEFAULT_POLLING_INTERVAL_MS3;
27333
- this.tags = options.tags ?? DEFAULT_KAKAO_TAGS2;
27334
- this.lastIndexedAt = options.lastIndexedAt;
27335
- }
27336
- describe() {
27337
- return {
27338
- name: KAKAO_DATASOURCE_ID2,
27339
- id: KAKAO_DATASOURCE_ID2,
27340
- type: KAKAO_SKILL_TYPE,
27341
- description: "KakaoTalk datasource via the external lazykatok CLI",
27342
- capabilities: ["chat", "external-cli", "polling", "bm25", "semantic"],
27343
- tags: this.tags,
27344
- status: "active",
27345
- requiresExternalCli: true,
27346
- datasourceId: KAKAO_DATASOURCE_ID2,
27347
- instanceId: this.instanceId,
27348
- instances: this.instances
27349
- };
27624
+ // src/datasource/sidecar/fusion.ts
27625
+ function encodeVector(vector) {
27626
+ const floats = Float32Array.from(vector);
27627
+ return new Uint8Array(floats.buffer, floats.byteOffset, floats.byteLength);
27628
+ }
27629
+ function decodeVector(blob) {
27630
+ if (blob.byteLength % Float32Array.BYTES_PER_ELEMENT !== 0)
27631
+ return;
27632
+ return new Float32Array(new Uint8Array(blob).buffer);
27633
+ }
27634
+ function cosine3(a, b) {
27635
+ if (a.length === 0 || a.length !== b.length)
27636
+ return 0;
27637
+ let dot = 0;
27638
+ let normA = 0;
27639
+ let normB = 0;
27640
+ for (let index = 0;index < a.length; index += 1) {
27641
+ const x = a[index] ?? 0;
27642
+ const y = b[index] ?? 0;
27643
+ dot += x * y;
27644
+ normA += x * x;
27645
+ normB += y * y;
27350
27646
  }
27351
- polling() {
27352
- return {
27353
- mode: "poll",
27354
- intervalMs: this.pollingIntervalMs,
27355
- lastIndexedAt: this.lastIndexedAt
27356
- };
27647
+ if (normA === 0 || normB === 0)
27648
+ return 0;
27649
+ return dot / Math.sqrt(normA * normB);
27650
+ }
27651
+ function topKByCosine(query, rows, k) {
27652
+ if (k <= 0)
27653
+ return [];
27654
+ const scored = [];
27655
+ for (const row of rows) {
27656
+ const score = cosine3(query, row.vector);
27657
+ if (score > 0)
27658
+ scored.push({ key: row.key, score });
27357
27659
  }
27358
- async index(options) {
27359
- try {
27360
- const doctor = await this.client.doctor(options?.signal);
27361
- if (!doctor.ok)
27362
- return this.fail("datasource-unavailable", doctor);
27363
- const sync = await this.client.sync(options?.signal);
27364
- if (!sync.ok)
27365
- return this.fail("datasource-index-failed", sync);
27366
- const indexResult = await this.client.index(options?.signal);
27367
- if (!indexResult.ok)
27368
- return this.fail("datasource-index-failed", indexResult);
27369
- this.lastIndexedAt = Date.now();
27370
- const chunkCount = "data" in indexResult && typeof indexResult.data === "object" && indexResult.data !== null && "chunkCount" in indexResult.data && typeof indexResult.data.chunkCount === "number" ? indexResult.data.chunkCount : 0;
27371
- return {
27372
- ok: true,
27373
- instanceId: this.instanceId,
27374
- skill: KAKAO_DATASOURCE_ID2,
27375
- chunkCount,
27376
- indexedAt: this.lastIndexedAt,
27377
- diagnostics: []
27378
- };
27379
- } catch (error) {
27380
- return this.fail("datasource-unavailable", {
27381
- ok: false,
27382
- reason: "spawn-error",
27383
- stdout: "",
27384
- stderr: describeRetrievalError(error),
27385
- code: null
27386
- });
27660
+ scored.sort((a, b) => b.score - a.score || a.key.localeCompare(b.key));
27661
+ return scored.slice(0, k);
27662
+ }
27663
+ function reciprocalRankFusion(lists) {
27664
+ const scores = new Map;
27665
+ for (const list of lists) {
27666
+ const seen = new Set;
27667
+ for (const [index, key] of list.entries()) {
27668
+ if (seen.has(key))
27669
+ continue;
27670
+ seen.add(key);
27671
+ scores.set(key, (scores.get(key) ?? 0) + 1 / (RRF_K2 + index + 1));
27387
27672
  }
27388
27673
  }
27389
- retrievalMethods() {
27390
- return [
27391
- new LazykatokBm25Method({ client: this.client, instanceId: this.instanceId, tags: this.tags }),
27392
- new LazykatokSemanticMethod({ client: this.client, instanceId: this.instanceId, tags: this.tags })
27393
- ];
27394
- }
27395
- describeSources() {
27396
- return this.instances.map((instanceId) => ({
27397
- source: datasourceSourcePath(KAKAO_DATASOURCE_ID2, instanceId),
27398
- datasourceId: KAKAO_DATASOURCE_ID2,
27399
- skill: KAKAO_DATASOURCE_ID2,
27400
- instanceId,
27401
- contentType: "chat",
27402
- metadata: {
27403
- datasourceId: KAKAO_DATASOURCE_ID2,
27404
- instanceId,
27405
- tags: this.tags
27406
- }
27407
- }));
27408
- }
27409
- skillManifest() {
27410
- const instanceSources = this.instances.map((instanceId) => `- \`${datasourceSourcePath(KAKAO_DATASOURCE_ID2, instanceId)}\``).join(`
27411
- `);
27412
- const cadence = this.pollingIntervalMs > 0 ? `roughly every ${Math.round(this.pollingIntervalMs / 60000)} minute(s) when auto-refresh runs` : "on manual refresh only";
27413
- return {
27414
- name: `datasource-${KAKAO_DATASOURCE_ID2}`,
27415
- description: "Search indexed KakaoTalk chat history (messages, senders, room context). Use for questions about KakaoTalk conversations, decisions made in chats, or who said what.",
27416
- content: [
27417
- `# KakaoTalk datasource (${KAKAO_SKILL_TYPE})`,
27418
- "",
27419
- "This skill searches KakaoTalk chats that are indexed through the external `lazykatok` CLI. AutoRAG never reads KakaoTalk databases directly.",
27420
- "",
27421
- "## When to use",
27422
- "Use this skill when the question is about KakaoTalk conversations, chat participants, or content shared inside chats.",
27423
- "",
27424
- "## Indexing",
27425
- `Indexing is server-managed and refreshed ${cadence}. You do not trigger indexing; just search.`,
27426
- "",
27427
- "## How to search",
27428
- `Call the dedicated \`${datasourceSearchToolName(KAKAO_DATASOURCE_ID2)}\` tool with a natural-language \`query\` and \`topK\`. This datasource does not support per-source scope narrowing. Configured datasource instances:`,
27429
- instanceSources.length > 0 ? instanceSources : "- (no configured instances)",
27430
- "",
27431
- "Chat/channel filtering is owned by lazykatok.",
27432
- "",
27433
- "## Native CLI",
27434
- "The external `lazykatok` CLI owns the archive, index, and credentials. When the dedicated tool cannot express what you need, call lazykatok directly through `bash`:",
27435
- '- `lazykatok search bm25 "<query>" --json --limit 20` — lexical search (`semantic` mode also available)',
27436
- "- `lazykatok chunk get <chunkId> --json` — fetch one chunk by id (chunk ids appear in result metadata)",
27437
- "- `lazykatok chunk context <chunkId> --json` — surrounding messages of a chunk",
27438
- "Never pass datasource virtual paths (`/kakao/...`) to bash; they are not OS paths.",
27439
- "",
27440
- "## Output rules",
27441
- "Datasource source identifiers such as `/kakao/<instance>/chunks/<id>` are internal and opaque. Never put them, real file paths, account IDs, or phone numbers in the visible answer."
27442
- ].join(`
27443
- `)
27444
- };
27445
- }
27446
- fail(code, result) {
27447
- const base = result.stderr.length > 0 ? `${result.reason}: ${result.stderr.trim().slice(0, 4000)}` : result.reason;
27448
- const message = code === "datasource-index-failed" && result.reason === "timeout" ? `${base} — ${connectorSyncTimeoutHint()}` : base;
27449
- const diagnostic = {
27450
- code,
27451
- severity: code === "datasource-unavailable" ? "warning" : "error",
27452
- message,
27453
- instanceId: this.instanceId,
27454
- source: KAKAO_DATASOURCE_ID2
27455
- };
27456
- return {
27457
- ok: false,
27458
- instanceId: this.instanceId,
27459
- skill: KAKAO_DATASOURCE_ID2,
27460
- indexedAt: Date.now(),
27461
- diagnostics: [diagnostic],
27462
- error: result.reason,
27463
- code,
27464
- message
27465
- };
27466
- }
27674
+ return [...scores.entries()].map(([key, score]) => ({ key, score })).sort((a, b) => b.score - a.score);
27467
27675
  }
27468
- var KAKAO_DATASOURCE_ID2 = "kakao", KAKAO_SKILL_TYPE = "kakaotalk", DEFAULT_INSTANCE_ID4 = "default", DEFAULT_POLLING_INTERVAL_MS3, DEFAULT_KAKAO_TAGS2;
27469
- var init_skill7 = __esm(() => {
27470
- init_skip();
27471
- init_scope2();
27472
- init_methods3();
27473
- DEFAULT_POLLING_INTERVAL_MS3 = 15 * 60 * 1000;
27474
- DEFAULT_KAKAO_TAGS2 = ["kakaotalk", "personal", "pii"];
27475
- });
27476
-
27477
- // src/datasource/skills/lazykatok/index.ts
27478
- var init_lazykatok = __esm(() => {
27479
- init_client8();
27480
- init_methods3();
27481
- init_skill7();
27482
- init_types5();
27483
- });
27676
+ var RRF_K2 = 60;
27484
27677
 
27485
- // src/datasource/skills/mail-export/connector.ts
27486
- import { readdir as readdir4, readFile as readFile5, stat as stat6 } from "node:fs/promises";
27487
- import { basename as basename12, dirname as dirname24, extname as extname6, join as join37 } from "node:path";
27488
- import { simpleParser as simpleParser2 } from "mailparser";
27678
+ // src/datasource/sidecar/index.ts
27679
+ import { createHash as createHash14 } from "node:crypto";
27680
+ import { mkdirSync as mkdirSync22, statSync as statSync3 } from "node:fs";
27681
+ import { dirname as dirname24 } from "node:path";
27682
+ import { DatabaseSync as DatabaseSync2 } from "node:sqlite";
27683
+ import { pathToFileURL as pathToFileURL2 } from "node:url";
27489
27684
 
27490
- class MailExportConnector {
27685
+ class CrawlerSidecar {
27686
+ descriptor;
27491
27687
  options;
27492
- constructor(options = {}) {
27688
+ select;
27689
+ where;
27690
+ opened;
27691
+ attached;
27692
+ lastSemantic;
27693
+ constructor(options) {
27493
27694
  this.options = options;
27695
+ const { descriptor } = options;
27696
+ this.descriptor = descriptor;
27697
+ const metadataColumns = descriptor.metadata.map((column, index) => `${column.sql} as m${index}`);
27698
+ this.select = [
27699
+ `${descriptor.keyExpression} as k`,
27700
+ `${descriptor.textExpression} as content`,
27701
+ `${descriptor.titleExpression ?? "null"} as title`,
27702
+ `${descriptor.epochSecondsExpression ?? "null"} as epoch`,
27703
+ ...metadataColumns
27704
+ ].join(", ");
27705
+ const scope = descriptor.scopeExpression !== undefined && options.workspaceId !== undefined;
27706
+ const allowlist = [
27707
+ ...(options.channelIds?.length ?? 0) > 0 ? [`${descriptor.channelIdExpression} in (${options.channelIds?.map(() => "?").join(", ")})`] : [],
27708
+ ...(options.channelNames?.length ?? 0) > 0 ? [`${descriptor.channelNameExpression} in (${options.channelNames?.map(() => "?").join(", ")})`] : []
27709
+ ];
27710
+ this.where = [
27711
+ descriptor.liveWhere,
27712
+ ...scope ? [`${descriptor.scopeExpression} = ?`] : [],
27713
+ ...allowlist.length > 0 ? [`(${allowlist.join(" or ")})`] : []
27714
+ ].join(" and ");
27494
27715
  }
27495
- async fetch() {
27496
- const paths = this.options.paths ?? [];
27497
- if (paths.length === 0)
27498
- return { ok: false, reason: "not-configured", message: "no mail export paths configured" };
27499
- const maxDocuments = this.options.maxDocuments ?? DEFAULT_MAX_DOCUMENTS3;
27500
- const maxBytes = this.options.maxBytesPerFile ?? DEFAULT_MAX_BYTES_PER_FILE2;
27501
- const files = [];
27502
- let missingPaths = 0;
27503
- for (const path of paths) {
27504
- try {
27505
- const info = await stat6(path);
27506
- if (info.isDirectory()) {
27507
- const entries = await readdir4(path, { recursive: true, withFileTypes: true });
27508
- for (const entry of entries) {
27509
- if (!entry.isFile())
27510
- continue;
27511
- const extension = extname6(entry.name).toLowerCase();
27512
- if (extension === ".eml" || extension === ".mbox") {
27513
- files.push(join37(entry.parentPath, entry.name));
27514
- }
27515
- }
27516
- } else if (info.isFile()) {
27517
- files.push(path);
27518
- }
27519
- } catch {
27520
- missingPaths += 1;
27521
- }
27716
+ get db() {
27717
+ if (this.opened !== undefined)
27718
+ return this.opened;
27719
+ mkdirSync22(dirname24(this.options.sidecarDbPath), { recursive: true });
27720
+ const db = new DatabaseSync2(this.options.sidecarDbPath);
27721
+ db.exec(`pragma busy_timeout = ${BUSY_TIMEOUT_MS}`);
27722
+ db.exec("pragma journal_mode = wal");
27723
+ db.exec(`
27724
+ create table if not exists sidecar_meta (k text primary key, v text not null);
27725
+ create table if not exists sidecar_vec (message_key text primary key, hash text not null, vec blob not null);
27726
+ `);
27727
+ this.opened = db;
27728
+ return db;
27729
+ }
27730
+ attachCrawler() {
27731
+ let file;
27732
+ try {
27733
+ const { dev, ino } = statSync3(this.options.crawlerDbPath);
27734
+ file = { dev, ino };
27735
+ } catch {
27736
+ this.detachCrawler();
27737
+ return false;
27522
27738
  }
27523
- if (missingPaths === paths.length) {
27524
- return { ok: false, reason: "unavailable", message: "no configured mail export paths were readable" };
27739
+ if (this.attached !== undefined) {
27740
+ if (this.attached.dev === file.dev && this.attached.ino === file.ino)
27741
+ return true;
27742
+ this.detachCrawler();
27525
27743
  }
27526
- const documents = [];
27527
- const warnings = [];
27528
- if (missingPaths > 0)
27529
- warnings.push(`${missingPaths} configured path(s) were unreadable`);
27530
- let parseFailures = 0;
27531
- let skippedLarge = 0;
27532
- for (const file of files.sort()) {
27533
- if (documents.length >= maxDocuments)
27534
- break;
27535
- let bytes;
27536
- try {
27537
- const info = await stat6(file);
27538
- if (info.size > maxBytes) {
27539
- skippedLarge += 1;
27540
- continue;
27541
- }
27542
- bytes = await readFile5(file);
27543
- } catch {
27544
- parseFailures += 1;
27545
- continue;
27546
- }
27547
- const stem = basename12(file, extname6(file));
27548
- const mailboxDir = basename12(dirname24(file));
27549
- const hierarchy = ["mailboxes", mailboxDir, stem];
27550
- const extension = extname6(file).toLowerCase();
27551
- const rawMessages = extension === ".mbox" ? splitMbox(bytes.toString("utf8")) : [bytes.toString("utf8")];
27552
- for (const [index, raw] of rawMessages.entries()) {
27553
- if (documents.length >= maxDocuments)
27554
- break;
27555
- try {
27556
- const parsed = await simpleParser2(raw);
27557
- const subject = parsed.subject ?? "(no subject)";
27558
- const from = Array.isArray(parsed.from) ? parsed.from[0]?.text : parsed.from?.text;
27559
- const to = Array.isArray(parsed.to) ? parsed.to[0]?.text : parsed.to?.text;
27560
- const body = (parsed.text ?? "").trim().slice(0, MAX_BODY_CHARS);
27561
- const headerBlock = [
27562
- `Subject: ${subject}`,
27563
- ...from !== undefined ? [`From: ${from}`] : [],
27564
- ...to !== undefined ? [`To: ${to}`] : [],
27565
- ...parsed.date !== undefined ? [`Date: ${parsed.date.toISOString()}`] : []
27566
- ].join(`
27567
- `);
27568
- const content = `${headerBlock}
27569
-
27570
- ${body}`.trim();
27571
- if (content.length === 0)
27572
- continue;
27573
- documents.push({
27574
- docId: extension === ".mbox" ? `${stem}-${index + 1}` : stem,
27575
- hierarchy,
27576
- title: subject,
27577
- content,
27578
- ...parsed.date !== undefined ? { publishedAt: parsed.date.getTime() } : {},
27579
- metadata: {
27580
- path: file,
27581
- subject,
27582
- ...from !== undefined ? { from } : {},
27583
- messageIndex: index
27584
- }
27585
- });
27586
- } catch {
27587
- parseFailures += 1;
27588
- }
27589
- }
27744
+ const crawlerUri = `${pathToFileURL2(this.options.crawlerDbPath).href}?mode=ro`.replaceAll("'", "''");
27745
+ try {
27746
+ this.db.exec(`attach database '${crawlerUri}' as crawler`);
27747
+ } catch {
27748
+ return false;
27590
27749
  }
27591
- if (skippedLarge > 0)
27592
- warnings.push(`${skippedLarge} file(s) exceeded the size limit and were skipped`);
27593
- if (parseFailures > 0)
27594
- warnings.push(`${parseFailures} message(s) failed to parse`);
27595
- return { ok: true, documents, ...warnings.length > 0 ? { warnings } : {} };
27750
+ const { descriptor } = this.options;
27751
+ try {
27752
+ this.db.prepare(`select ${this.select} from ${descriptor.fromClause} where ${this.where} limit 0`).all(...this.whereParams());
27753
+ this.db.prepare(`select 1 from ${descriptor.fts.table} limit 0`).all();
27754
+ } catch {
27755
+ this.db.exec("detach database crawler");
27756
+ return false;
27757
+ }
27758
+ this.attached = file;
27759
+ this.lastSemantic = undefined;
27760
+ return true;
27596
27761
  }
27597
- }
27598
- function splitMbox(text) {
27599
- const lines = text.split(/\r?\n/u);
27600
- const messages = [];
27601
- let current = [];
27602
- for (const line of lines) {
27603
- if (MBOX_SEPARATOR.test(line) && line.startsWith("From ")) {
27604
- if (current.length > 0)
27605
- messages.push(current.join(`
27606
- `));
27607
- current = [];
27608
- continue;
27609
- }
27610
- current.push(line);
27611
- }
27612
- if (current.length > 0)
27613
- messages.push(current.join(`
27614
- `));
27615
- return messages.filter((message) => message.trim().length > 0);
27616
- }
27617
- var DEFAULT_MAX_DOCUMENTS3 = 500, DEFAULT_MAX_BYTES_PER_FILE2, MAX_BODY_CHARS = 50000, MBOX_SEPARATOR;
27618
- var init_connector4 = __esm(() => {
27619
- DEFAULT_MAX_BYTES_PER_FILE2 = 50 * 1024 * 1024;
27620
- MBOX_SEPARATOR = /^From .*\d{4}$/mu;
27621
- });
27622
-
27623
- // src/datasource/skills/mail-export/skill.ts
27624
- var MAIL_EXPORT_SKILL_DEFINITION, MailExportSkill;
27625
- var init_skill8 = __esm(() => {
27626
- init_connector_skill();
27627
- init_connector4();
27628
- MAIL_EXPORT_SKILL_DEFINITION = {
27629
- skillName: "mail-export",
27630
- skillType: "mail-export",
27631
- description: "Local mail export datasource",
27632
- capabilities: ["email", "filesystem"],
27633
- defaultTags: ["mail-export", "email", "pii"],
27634
- contentType: "email",
27635
- manifestDescription: "Search indexed local mail exports (mbox/eml archives). Use for questions about archived email conversations.",
27636
- manifestNotes: [
27637
- "Only the server-configured export paths are indexed; archives are read locally and never uploaded."
27638
- ]
27639
- };
27640
- MailExportSkill = class MailExportSkill extends ConnectorDatasourceSkill {
27641
- constructor(options = {}) {
27642
- const { connector, connectorOptions, ...rest } = options;
27643
- super(MAIL_EXPORT_SKILL_DEFINITION, {
27644
- ...rest,
27645
- connector: connector ?? new MailExportConnector(connectorOptions ?? {})
27646
- });
27647
- }
27648
- };
27649
- });
27650
-
27651
- // src/datasource/skills/mail-export/index.ts
27652
- var init_mail_export = __esm(() => {
27653
- init_connector4();
27654
- init_skill8();
27655
- });
27656
-
27657
- // src/datasource/skills/mailcrawl/config.ts
27658
- import { chmodSync, mkdirSync as mkdirSync22 } from "node:fs";
27659
- function validateMailcrawlInstanceId(instanceId) {
27660
- if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/u.test(instanceId)) {
27661
- throw new Error("mailcrawl instanceId must be a safe single path segment");
27762
+ detachCrawler() {
27763
+ if (this.attached === undefined)
27764
+ return;
27765
+ this.attached = undefined;
27766
+ this.lastSemantic = undefined;
27767
+ try {
27768
+ this.opened?.exec("detach database crawler");
27769
+ } catch {}
27662
27770
  }
27663
- }
27664
- function ensurePrivateMailcrawlDataDir(path) {
27665
- mkdirSync22(path, { recursive: true, mode: 448 });
27666
- if (process.platform !== "win32")
27667
- chmodSync(path, 448);
27668
- }
27669
- var init_config3 = () => {};
27670
-
27671
- // src/datasource/skills/mailcrawl/client.ts
27672
- import { spawn as spawn15 } from "node:child_process";
27673
-
27674
- class MailcrawlClient {
27675
- options;
27676
- constructor(options = {}) {
27677
- this.options = options;
27678
- if (options.instanceId !== undefined)
27679
- validateMailcrawlInstanceId(options.instanceId);
27771
+ get semanticEnabled() {
27772
+ return this.options.embedder !== undefined;
27680
27773
  }
27681
- async sync(signal) {
27682
- const args = [
27683
- "sync",
27684
- "--json",
27685
- ...this.options.account === undefined ? [] : ["--account", this.options.account],
27686
- ...this.options.mailbox === undefined ? [] : ["--mailbox", this.options.mailbox],
27687
- ...this.options.backend === undefined ? [] : ["--backend", this.options.backend],
27688
- ...this.options.source === undefined ? [] : ["--source", this.options.source],
27689
- ...this.options.fixture === undefined ? [] : ["--fixture", this.options.fixture],
27690
- ...this.options.himalayaConfig === undefined ? [] : ["--himalaya-config", this.options.himalayaConfig]
27691
- ];
27692
- const result = await this.run(args, signal, this.indexTimeoutMs());
27693
- if (!result.ok)
27694
- return failure3(result);
27695
- const parsed = parseObject2(result.stdout);
27696
- if (parsed === undefined || !hasAnyNumber(parsed, ["added", "updated", "deleted", "unchanged", "chunksAdded"])) {
27697
- return failure3({ ...result, ok: false, reason: "invalid-output" });
27698
- }
27699
- const messages = number(parsed, "added") ?? number(parsed, "updated") ?? number(parsed, "unchanged") ?? 0;
27700
- return ok4({
27701
- messages,
27702
- added: number(parsed, "added"),
27703
- updated: number(parsed, "updated"),
27704
- deleted: number(parsed, "deleted"),
27705
- unchanged: number(parsed, "unchanged"),
27706
- chunksAdded: number(parsed, "chunksAdded"),
27707
- archiveRevision: string(parsed, "archiveRevision")
27708
- }, result);
27774
+ close() {
27775
+ this.opened?.close();
27776
+ this.opened = undefined;
27777
+ this.attached = undefined;
27778
+ this.lastSemantic = undefined;
27709
27779
  }
27710
- async search(mode, query, options) {
27711
- if (query.trim().length === 0)
27712
- return { ok: true, hits: [], stdout: "", stderr: "", code: 0 };
27713
- const args = [
27714
- "search",
27715
- "--mode",
27716
- mode === "keyword" ? "bm25" : mode,
27717
- ...this.options.account === undefined ? [] : ["--account", this.options.account],
27718
- ...this.options.mailbox === undefined ? [] : ["--mailbox", this.options.mailbox],
27719
- "--limit",
27720
- String(options?.topK ?? 20),
27721
- "--json",
27722
- query.trim()
27780
+ whereParams() {
27781
+ const { descriptor, workspaceId, channelIds = [], channelNames = [] } = this.options;
27782
+ return [
27783
+ ...descriptor.scopeExpression !== undefined && workspaceId !== undefined ? [workspaceId] : [],
27784
+ ...channelIds,
27785
+ ...channelNames
27723
27786
  ];
27724
- const result = await this.run(args, options?.signal, this.options.timeoutMs ?? DEFAULT_SEARCH_TIMEOUT_MS2);
27725
- if (!result.ok)
27726
- return failure3(result);
27727
- const parsed = parseJson3(result.stdout);
27728
- if (parsed === undefined)
27729
- return failure3({ ...result, ok: false, reason: "invalid-output" });
27730
- const rows = Array.isArray(parsed) ? parsed : objectArray(parsed, ["hits", "results", "items"]);
27731
- if (rows === undefined)
27732
- return failure3({ ...result, ok: false, reason: "invalid-output" });
27733
- const hits = rows.map((row, index) => normalizeHit2(row, mode, index));
27734
- if (hits.some((hit) => hit === undefined)) {
27735
- return failure3({ ...result, ok: false, reason: "invalid-output" });
27736
- }
27737
- return {
27738
- ok: true,
27739
- hits,
27740
- stdout: result.stdout,
27741
- stderr: result.stderr,
27742
- code: result.code ?? 0
27743
- };
27744
27787
  }
27745
- indexTimeoutMs() {
27746
- return this.options.indexTimeoutMs ?? this.options.timeoutMs ?? DEFAULT_INDEX_TIMEOUT_MS3;
27788
+ searchLexical(query, topK) {
27789
+ const match = toMatchQuery(query);
27790
+ if (match === undefined || topK <= 0 || !this.attachCrawler())
27791
+ return [];
27792
+ const { descriptor } = this.options;
27793
+ const rows = this.db.prepare(`select ${this.select}, bm25(${descriptor.fts.table}) as rank
27794
+ from ${descriptor.fromClause} join ${descriptor.fts.table} on ${descriptor.fts.joinOn}
27795
+ where ${descriptor.fts.table} match ? and ${this.where}
27796
+ order by rank limit ?`).all(match, ...this.whereParams(), topK);
27797
+ return rows.map((row) => this.toHit(this.toRow(row), -Number(row.rank)));
27747
27798
  }
27748
- async run(args, signal, timeoutMs) {
27749
- const violation = findRemoteEmbeddingViolation({ ...process.env, ...this.options.env ?? {} });
27750
- if (violation !== undefined) {
27751
- return { ok: false, reason: "remote-embedding-rejected", stdout: "", stderr: "", code: null };
27799
+ async searchSemantic(query, topK) {
27800
+ const trimmed = query.trim();
27801
+ if (this.options.embedder === undefined || trimmed.length === 0 || topK <= 0 || !this.attachCrawler())
27802
+ return [];
27803
+ const ranked = await this.rankSemantic(trimmed, topK * POOL_FACTOR);
27804
+ if (ranked.length === 0)
27805
+ return [];
27806
+ const live = this.hydrate(ranked.map((entry) => entry.key));
27807
+ const hits = [];
27808
+ for (const entry of ranked) {
27809
+ const row = live.get(entry.key);
27810
+ if (row !== undefined)
27811
+ hits.push(this.toHit(row, entry.score));
27812
+ if (hits.length >= topK)
27813
+ break;
27752
27814
  }
27753
- const dataDir = this.options.dataDir;
27754
- if (dataDir !== undefined)
27755
- ensurePrivateMailcrawlDataDir(dataDir);
27756
- const env = {};
27757
- for (const [key, value] of Object.entries(process.env)) {
27758
- if (value !== undefined && (SAFE_ENV_KEYS2.has(key) || SAFE_MAILCRAWL_ENV_KEYS.has(key)))
27759
- env[key] = value;
27815
+ return hits;
27816
+ }
27817
+ async rankSemantic(query, pool) {
27818
+ const now = Date.now();
27819
+ let cached = this.lastSemantic;
27820
+ if (cached === undefined || cached.query !== query || now - cached.at > SEMANTIC_REUSE_MS) {
27821
+ const embedder = this.options.embedder;
27822
+ if (embedder === undefined)
27823
+ return [];
27824
+ cached = {
27825
+ query,
27826
+ at: now,
27827
+ vector: embedder.embed([query]).then((vectors) => vectors[0], () => {
27828
+ return;
27829
+ }),
27830
+ ranked: undefined
27831
+ };
27832
+ this.lastSemantic = cached;
27760
27833
  }
27761
- for (const [key, value] of Object.entries(this.options.env ?? {})) {
27762
- if (value === undefined)
27763
- delete env[key];
27764
- else if (SAFE_ENV_KEYS2.has(key) || SAFE_MAILCRAWL_ENV_KEYS.has(key))
27765
- env[key] = value;
27834
+ const vector = await cached.vector;
27835
+ if (vector === undefined) {
27836
+ if (this.lastSemantic === cached)
27837
+ this.lastSemantic = undefined;
27838
+ return [];
27766
27839
  }
27767
- if (dataDir !== undefined)
27768
- env.MAILCRAWL_DATA_DIR = dataDir;
27769
- return spawnMailcrawl(this.options.binaryPath ?? DEFAULT_BINARY4, args, env, this.options, signal, timeoutMs);
27770
- }
27771
- async index(signal) {
27772
- const result = await this.run(["index", "--json"], signal, this.indexTimeoutMs());
27773
- if (!result.ok)
27774
- return failure3(result);
27775
- const parsed = parseObject2(result.stdout);
27776
- if (parsed === undefined || number(parsed, "embedded") === undefined && number(parsed, "reused") === undefined && string(parsed, "generation") === undefined) {
27777
- return failure3({ ...result, ok: false, reason: "invalid-output" });
27840
+ if (cached.ranked === undefined || cached.ranked.pool < pool) {
27841
+ const wide = pool * POOL_FACTOR;
27842
+ cached.ranked = { pool: wide, keys: topKByCosine(vector, this.storedVectors(), wide) };
27778
27843
  }
27779
- return ok4({
27780
- embedded: number(parsed, "embedded"),
27781
- reused: number(parsed, "reused"),
27782
- generation: string(parsed, "generation"),
27783
- archiveRevision: string(parsed, "archiveRevision"),
27784
- rebuilt: typeof parsed.rebuilt === "boolean" ? parsed.rebuilt : undefined,
27785
- embedder: string(parsed, "embedder")
27786
- }, result);
27844
+ return cached.ranked.keys.slice(0, pool);
27787
27845
  }
27788
- }
27789
- function spawnMailcrawl(binary, args, env, options, signal, timeoutMs) {
27790
- return new Promise((resolveResult) => {
27791
- const portable = portableSpawnCommand(binary, args);
27792
- const child = spawn15(portable.command, portable.args, {
27793
- env,
27794
- detached: process.platform !== "win32",
27795
- stdio: ["ignore", "pipe", "pipe"]
27796
- });
27797
- let stdout = { text: "", bytes: 0, exceeded: false }, stderr = { text: "", bytes: 0, exceeded: false }, settled = false, reason;
27798
- const max = options.maxBufferBytes ?? DEFAULT_MAX_BUFFER_BYTES4;
27799
- const timer = setTimeout(() => {
27800
- reason = "timeout";
27801
- terminate4(child);
27802
- }, timeoutMs);
27803
- const abort = () => {
27804
- reason = "aborted";
27805
- terminate4(child);
27846
+ async searchHybrid(query, topK) {
27847
+ if (topK <= 0)
27848
+ return [];
27849
+ const pool = topK * POOL_FACTOR;
27850
+ const lexical = this.searchLexical(query, pool);
27851
+ const semantic = await this.searchSemantic(query, pool);
27852
+ if (semantic.length === 0)
27853
+ return lexical.slice(0, topK);
27854
+ const byKey = new Map;
27855
+ for (const hit of [...semantic, ...lexical])
27856
+ byKey.set(hit.key, hit);
27857
+ const fused = [];
27858
+ for (const entry of reciprocalRankFusion([lexical.map((hit) => hit.key), semantic.map((hit) => hit.key)])) {
27859
+ const hit = byKey.get(entry.key);
27860
+ if (hit !== undefined)
27861
+ fused.push({ ...hit, score: entry.score });
27862
+ if (fused.length >= topK)
27863
+ break;
27864
+ }
27865
+ return fused;
27866
+ }
27867
+ async sync() {
27868
+ const { embedder } = this.options;
27869
+ if (embedder === undefined)
27870
+ return { ok: false, message: "no embedder configured" };
27871
+ if (!this.attachCrawler())
27872
+ return { ok: false, message: "crawler archive not available yet" };
27873
+ let identity;
27874
+ try {
27875
+ identity = await embedder.identity();
27876
+ } catch (error) {
27877
+ return { ok: false, message: errorMessage2(error) };
27878
+ }
27879
+ this.adoptIdentity(identity);
27880
+ const pruned = this.prune();
27881
+ const pending = this.pendingKeys();
27882
+ const batchSize = Math.max(1, this.options.batchSize ?? DEFAULT_BATCH_SIZE);
27883
+ const budget = Math.max(1, this.options.maxEmbedPerSync ?? DEFAULT_MAX_EMBED_PER_SYNC);
27884
+ const target = Math.min(pending.length, budget);
27885
+ const embed = async (rows) => {
27886
+ const vectors = await embedder.embed(rows.map((row) => row.input));
27887
+ if (vectors.length !== rows.length) {
27888
+ throw new Error(`embedder returned ${vectors.length} vectors for ${rows.length} messages`);
27889
+ }
27890
+ const wrong = vectors.find((vector) => vector.length !== identity.dimension);
27891
+ if (wrong !== undefined) {
27892
+ throw new Error(`embedder returned a ${wrong.length}-dimension vector, expected ${identity.dimension}`);
27893
+ }
27894
+ return vectors;
27806
27895
  };
27807
- if (signal?.aborted)
27808
- abort();
27809
- signal?.addEventListener("abort", abort, { once: true });
27810
- child.stdout.setEncoding("utf8");
27811
- child.stderr.setEncoding("utf8");
27812
- child.stdout.on("data", (chunk) => {
27813
- stdout = appendOutput(stdout, chunk, max);
27814
- if (stdout.exceeded && reason === undefined) {
27815
- reason = "stdout-too-large";
27816
- terminate4(child);
27896
+ let embedded = 0;
27897
+ let skipped = 0;
27898
+ for (let offset = 0;offset < target; offset += batchSize) {
27899
+ const rows = [...this.hydrate(pending.slice(offset, Math.min(offset + batchSize, target))).values()];
27900
+ if (rows.length === 0)
27901
+ continue;
27902
+ try {
27903
+ this.store(rows, await embed(rows));
27904
+ embedded += rows.length;
27905
+ } catch (batchError) {
27906
+ const rejected = [];
27907
+ let lastError = batchError;
27908
+ for (const row of rows) {
27909
+ try {
27910
+ this.store([row], await embed([row]));
27911
+ embedded += 1;
27912
+ } catch (error) {
27913
+ rejected.push(row);
27914
+ lastError = error;
27915
+ }
27916
+ }
27917
+ if (rejected.length === 0)
27918
+ continue;
27919
+ if (embedded === 0)
27920
+ return { ok: false, message: errorMessage2(lastError) };
27921
+ this.store(rejected, undefined);
27922
+ skipped += rejected.length;
27817
27923
  }
27924
+ }
27925
+ return { ok: true, embedded, skipped, pruned, remaining: pending.length - embedded - skipped };
27926
+ }
27927
+ adoptIdentity(identity) {
27928
+ const serialized = JSON.stringify(identity);
27929
+ const current = this.db.prepare("select v from sidecar_meta where k = ?").get(IDENTITY_KEY);
27930
+ if (current?.v === serialized)
27931
+ return;
27932
+ this.transaction(() => {
27933
+ this.db.exec("delete from sidecar_vec");
27934
+ this.db.prepare("insert or replace into sidecar_meta (k, v) values (?, ?)").run(IDENTITY_KEY, serialized);
27818
27935
  });
27819
- child.stderr.on("data", (chunk) => {
27820
- stderr = appendOutput(stderr, chunk, max);
27821
- if (stderr.exceeded && reason === undefined) {
27822
- reason = "stderr-too-large";
27823
- terminate4(child);
27936
+ this.lastSemantic = undefined;
27937
+ }
27938
+ prune() {
27939
+ const { descriptor } = this.options;
27940
+ const result = this.db.prepare(`delete from sidecar_vec where message_key not in (
27941
+ select ${descriptor.keyExpression} from ${descriptor.fromClause} where ${this.where}
27942
+ )`).run(...this.whereParams());
27943
+ if (Number(result.changes) > 0)
27944
+ this.lastSemantic = undefined;
27945
+ return Number(result.changes);
27946
+ }
27947
+ pendingKeys() {
27948
+ const { descriptor } = this.options;
27949
+ const rows = this.db.prepare(`select ${this.select}, v.hash as stored_hash
27950
+ from ${descriptor.fromClause}
27951
+ left join sidecar_vec v on v.message_key = ${descriptor.keyExpression}
27952
+ where ${this.where}`).iterate(...this.whereParams());
27953
+ const pending = [];
27954
+ for (const row of rows) {
27955
+ if (row.stored_hash !== this.toRow(row).hash)
27956
+ pending.push(String(row.k));
27957
+ }
27958
+ return pending;
27959
+ }
27960
+ hydrate(keys) {
27961
+ const { descriptor } = this.options;
27962
+ const out = new Map;
27963
+ if (keys.length === 0)
27964
+ return out;
27965
+ const rows = this.db.prepare(`select ${this.select} from ${descriptor.fromClause}
27966
+ where ${descriptor.keyExpression} in (${keys.map(() => "?").join(", ")}) and ${this.where}`).all(...keys, ...this.whereParams());
27967
+ const byKey = new Map(rows.map((row) => [String(row.k), this.toRow(row)]));
27968
+ for (const key of keys) {
27969
+ const row = byKey.get(key);
27970
+ if (row !== undefined)
27971
+ out.set(key, row);
27972
+ }
27973
+ return out;
27974
+ }
27975
+ store(rows, vectors) {
27976
+ const upsert = this.db.prepare("insert or replace into sidecar_vec (message_key, hash, vec) values (?, ?, ?)");
27977
+ this.transaction(() => {
27978
+ for (const [index, row] of rows.entries()) {
27979
+ upsert.run(row.key, row.hash, encodeVector(vectors?.[index] ?? []));
27824
27980
  }
27825
27981
  });
27826
- child.on("error", (error) => {
27827
- if (settled)
27828
- return;
27829
- settled = true;
27830
- clearTimeout(timer);
27831
- signal?.removeEventListener("abort", abort);
27832
- resolveResult({
27833
- ok: false,
27834
- reason: error.code === "ENOENT" ? "binary-missing" : "spawn-error",
27835
- stdout: stdout.text,
27836
- stderr: "",
27837
- code: null
27838
- });
27839
- });
27840
- child.on("close", (code) => {
27841
- if (settled)
27842
- return;
27843
- settled = true;
27844
- clearTimeout(timer);
27845
- signal?.removeEventListener("abort", abort);
27846
- resolveResult({
27847
- ok: code === 0 && reason === undefined,
27848
- ...reason === undefined ? code === 0 ? {} : { reason: "nonzero-exit" } : { reason },
27849
- stdout: stdout.text,
27850
- stderr: stderr.text,
27851
- code
27852
- });
27853
- });
27854
- });
27855
- }
27856
- function terminate4(child) {
27857
- if (child.killed)
27858
- return;
27859
- if (process.platform !== "win32" && child.pid !== undefined) {
27982
+ this.lastSemantic = undefined;
27983
+ }
27984
+ *storedVectors() {
27985
+ const rows = this.db.prepare("select message_key, vec from sidecar_vec where length(vec) > 0").iterate();
27986
+ for (const row of rows) {
27987
+ const vector = row.vec instanceof Uint8Array ? decodeVector(row.vec) : undefined;
27988
+ if (vector !== undefined)
27989
+ yield { key: String(row.message_key), vector };
27990
+ }
27991
+ }
27992
+ transaction(work) {
27993
+ this.db.exec("begin immediate");
27860
27994
  try {
27861
- process.kill(-child.pid, "SIGTERM");
27862
- return;
27863
- } catch {}
27995
+ work();
27996
+ this.db.exec("commit");
27997
+ } catch (error) {
27998
+ this.db.exec("rollback");
27999
+ throw error;
28000
+ }
27864
28001
  }
27865
- child.kill("SIGTERM");
27866
- }
27867
- function parseJson3(text) {
27868
- try {
27869
- return JSON.parse(text.trim());
27870
- } catch {
27871
- return;
28002
+ toRow(row) {
28003
+ const metadata = {};
28004
+ for (const [index, column] of this.options.descriptor.metadata.entries()) {
28005
+ const value = row[`m${index}`];
28006
+ if (typeof value === "string" && value.length > 0 || typeof value === "number")
28007
+ metadata[column.key] = String(value);
28008
+ }
28009
+ const title = typeof row.title === "string" && row.title.length > 0 ? row.title : undefined;
28010
+ const epoch = typeof row.epoch === "number" && Number.isFinite(row.epoch) ? row.epoch : undefined;
28011
+ const content = String(row.content ?? "");
28012
+ const input = `${title ?? ""}
28013
+ ${content}`.slice(0, MAX_EMBED_INPUT_CHARS);
28014
+ return {
28015
+ key: String(row.k),
28016
+ content,
28017
+ title,
28018
+ epoch,
28019
+ metadata,
28020
+ input,
28021
+ hash: createHash14("sha1").update(input).digest("hex")
28022
+ };
28023
+ }
28024
+ toHit(row, score) {
28025
+ return {
28026
+ key: row.key,
28027
+ content: row.content,
28028
+ score,
28029
+ ...row.title !== undefined ? { title: row.title } : {},
28030
+ ...row.epoch !== undefined ? { publishedAt: Math.round(row.epoch * 1000) } : {},
28031
+ metadata: row.metadata
28032
+ };
27872
28033
  }
27873
28034
  }
27874
- function parseObject2(text) {
27875
- const value = parseJson3(text);
27876
- return value !== null && typeof value === "object" && !Array.isArray(value) ? value : undefined;
27877
- }
27878
- function objectArray(value, keys) {
27879
- if (value === null || typeof value !== "object" || Array.isArray(value))
27880
- return;
27881
- for (const key of keys)
27882
- if (Array.isArray(value[key]))
27883
- return value[key];
27884
- return;
27885
- }
27886
- function hasAnyNumber(value, keys) {
27887
- return keys.some((key) => number(value, key) !== undefined);
27888
- }
27889
- function normalizeHit2(value, mode, index) {
27890
- if (value === null || typeof value !== "object" || Array.isArray(value))
27891
- return;
27892
- const row = value;
27893
- const text = (key) => typeof row[key] === "string" ? row[key] : "";
27894
- const chunkId = text("chunkId") || text("chunk_id");
27895
- const messageId = text("messageId") || text("message_id");
27896
- const accountId = text("accountId") || text("account_id");
27897
- const mailbox = text("mailbox");
27898
- const snippet = text("snippet") || text("content") || text("text");
27899
- if (!isSafeOpaqueId(chunkId) || !isSafeOpaqueId(messageId) || !snippet)
27900
- return;
27901
- const to = Array.isArray(row.to) ? row.to.filter((item) => typeof item === "string") : [];
27902
- return {
27903
- chunkId,
27904
- messageId,
27905
- threadId: text("threadId") || text("thread_id"),
27906
- accountId,
27907
- mailbox,
27908
- subject: text("subject"),
27909
- from: text("from"),
27910
- to,
27911
- date: text("date"),
27912
- snippet,
27913
- score: typeof row.score === "number" ? row.score : 1 / (index + 1),
27914
- mode
27915
- };
27916
- }
27917
- function number(value, key) {
27918
- return typeof value[key] === "number" && Number.isFinite(value[key]) ? value[key] : undefined;
27919
- }
27920
- function string(value, key) {
27921
- return typeof value[key] === "string" ? value[key] : undefined;
27922
- }
27923
- function ok4(data, result) {
27924
- return { ok: true, data, stdout: result.stdout, stderr: result.stderr, code: result.code ?? 0 };
27925
- }
27926
- function failure3(result) {
27927
- return {
27928
- ok: false,
27929
- reason: result.reason ?? "nonzero-exit",
27930
- stdout: result.stdout,
27931
- stderr: boundDiagnosticText6(result.stderr),
27932
- code: result.code
27933
- };
27934
- }
27935
- function boundDiagnosticText6(value) {
27936
- if (value.length === 0)
27937
- return "";
27938
- return value.trim().slice(0, 4000);
28035
+ function toMatchQuery(text) {
28036
+ const terms = [...new Set(text.match(/[\p{L}\p{N}_]+/gu) ?? [])];
28037
+ return terms.length === 0 ? undefined : terms.map((term) => `"${term}"`).join(" OR ");
27939
28038
  }
27940
- function appendOutput(current, chunk, maxBytes) {
27941
- if (current.exceeded)
27942
- return current;
27943
- const bytes = Buffer.from(chunk, "utf8");
27944
- const remaining = Math.max(0, maxBytes - current.bytes);
27945
- const prefix = truncateUtf8(chunk, remaining);
27946
- const total = current.bytes + bytes.byteLength;
27947
- return {
27948
- text: current.text + prefix,
27949
- bytes: Math.min(total, maxBytes),
27950
- exceeded: total > maxBytes
27951
- };
28039
+ function errorMessage2(error) {
28040
+ return error instanceof Error ? error.message : String(error);
27952
28041
  }
27953
- function isSafeOpaqueId(value) {
27954
- return value.length > 0 && value !== "." && value !== ".." && !/[\\/#\u0000-\u001f\u007f]/u.test(value);
28042
+ 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";
28043
+ var init_sidecar = () => {};
28044
+
28045
+ // src/datasource/sidecar/methods.ts
28046
+ function sidecarMethodName(datasourceId, mode) {
28047
+ return mode === "bm25" ? `${datasourceId}-sidecar-bm25` : `${datasourceId}-${mode}`;
27955
28048
  }
27956
- function findRemoteEmbeddingViolation(env) {
27957
- for (const [key, value] of Object.entries(env)) {
27958
- if (value === undefined)
27959
- continue;
27960
- const lower = key.toLowerCase();
27961
- if (REMOTE_EMBEDDING_ENV_KEYS.has(lower))
27962
- return key;
27963
- if (lower === "mailcrawl_embedder" || lower === "mailcrawl_embed_url") {
27964
- if (isRemoteHttpEndpoint(value))
27965
- return key;
27966
- }
28049
+
28050
+ class SidecarRetrievalMethod {
28051
+ options;
28052
+ constructor(options) {
28053
+ this.options = options;
27967
28054
  }
27968
- return;
27969
- }
27970
- function isRemoteHttpEndpoint(value) {
27971
- const trimmed = value.trim();
27972
- if (!/^https?:\/\//iu.test(trimmed))
27973
- return false;
27974
- try {
27975
- return !LOOPBACK_HOSTS2.has(new URL(trimmed).hostname);
27976
- } catch {
27977
- return true;
28055
+ describe() {
28056
+ const { datasourceId, skillType, mode, tags } = this.options;
28057
+ const { type, text, capabilities } = DESCRIPTIONS[mode];
28058
+ return {
28059
+ name: sidecarMethodName(datasourceId, mode),
28060
+ type,
28061
+ description: `${text} over ${skillType} via a read-only sidecar index`,
28062
+ status: "active",
28063
+ capabilities: [...capabilities, "scoped", "path-opaque-sources"],
28064
+ datasourceId,
28065
+ tags
28066
+ };
27978
28067
  }
27979
- }
27980
- function truncateUtf8(value, maxBytes) {
27981
- if (Buffer.byteLength(value) <= maxBytes)
27982
- return value;
27983
- const decoder = new TextDecoder("utf-8", { fatal: true });
27984
- const bytes = Buffer.from(value, "utf8");
27985
- for (let end = Math.min(maxBytes, bytes.length);end >= 0; end -= 1) {
27986
- try {
27987
- return decoder.decode(bytes.subarray(0, end));
27988
- } catch {}
28068
+ async retrieve(query, options) {
28069
+ const trimmed = query.trim();
28070
+ if (trimmed.length === 0)
28071
+ return [];
28072
+ const topK = options.topK ?? DEFAULT_TOP_K5;
28073
+ const { sidecar, mode } = this.options;
28074
+ const hits = mode === "bm25" ? sidecar.searchLexical(trimmed, topK) : mode === "semantic" ? await sidecar.searchSemantic(trimmed, topK) : await sidecar.searchHybrid(trimmed, topK);
28075
+ const results = [];
28076
+ for (const hit of hits) {
28077
+ const result = this.toResult(hit, options);
28078
+ if (result !== undefined)
28079
+ results.push(result);
28080
+ }
28081
+ return results;
28082
+ }
28083
+ toResult(hit, options) {
28084
+ const { descriptor, datasourceId, instanceId, mode } = this.options;
28085
+ const chunkId = descriptor.chunkId(hit.key, hit.metadata);
28086
+ const source = datasourceSourcePath(datasourceId, instanceId, chunkId);
28087
+ if (!matchesDatasourceScope(source, options.scope))
28088
+ return;
28089
+ const method = sidecarMethodName(datasourceId, mode);
28090
+ return {
28091
+ id: `${datasourceId}:${instanceId}:${chunkId}`,
28092
+ content: hit.content,
28093
+ source,
28094
+ score: hit.score,
28095
+ metadata: {
28096
+ ...hit.metadata,
28097
+ method,
28098
+ datasourceId,
28099
+ instanceId,
28100
+ backend: "sidecar",
28101
+ ...hit.title !== undefined ? { title: hit.title } : {},
28102
+ hierarchy: descriptor.hierarchy(hit.metadata).join("/"),
28103
+ ...hit.publishedAt !== undefined ? { publishedAt: hit.publishedAt } : {}
28104
+ }
28105
+ };
27989
28106
  }
27990
- return "";
27991
28107
  }
27992
- 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;
27993
- var init_client9 = __esm(() => {
27994
- init_portable_spawn();
27995
- init_config3();
27996
- LOOPBACK_HOSTS2 = new Set(["127.0.0.1", "localhost", "[::1]", "::1"]);
27997
- SAFE_ENV_KEYS2 = new Set(["HOME", "LANG", "LC_ALL", "PATH", "TMPDIR", "TMP", "TEMP", "NO_COLOR"]);
27998
- SAFE_MAILCRAWL_ENV_KEYS = new Set([
27999
- "MAILCRAWL_DATA_DIR",
28000
- "MAILCRAWL_EMBEDDER",
28001
- "MAILCRAWL_EMBEDDER_PROVIDER",
28002
- "MAILCRAWL_EMBED_URL",
28003
- "MAILCRAWL_EMBED_MODEL",
28004
- "MAILCRAWL_EMBED_DIM",
28005
- "MAILCRAWL_EMBED_TIMEOUT",
28006
- "MAILCRAWL_EMBED_BATCH_SIZE",
28007
- "MAILCRAWL_QUERY_PREFIX",
28008
- "MAILCRAWL_PASSAGE_PREFIX",
28009
- "MAILCRAWL_NATIVE_MODEL",
28010
- "MAILCRAWL_NATIVE_DEVICE",
28011
- "MAILCRAWL_NATIVE_DTYPE",
28012
- "MAILCRAWL_KIWI_MODEL",
28013
- "MAILCRAWL_JA_HELPER",
28014
- "MAILCRAWL_ZH_HELPER"
28015
- ]);
28016
- REMOTE_EMBEDDING_ENV_KEYS = new Set([
28017
- "mailcrawl_embedder_base_url",
28018
- "mailcrawl_embedding_base_url",
28019
- "mailcrawl_allow_remote_embeddings"
28020
- ]);
28108
+ var DEFAULT_TOP_K5 = 20, DESCRIPTIONS;
28109
+ var init_methods4 = __esm(() => {
28110
+ init_scope2();
28111
+ DESCRIPTIONS = {
28112
+ bm25: { type: "bm25", text: "BM25-ranked FTS5 retrieval", capabilities: ["lexical", "fts5", "bm25"] },
28113
+ semantic: { type: "vector", text: "Semantic (embedding) retrieval", capabilities: ["semantic"] },
28114
+ hybrid: {
28115
+ type: "hybrid",
28116
+ text: "Hybrid BM25 + semantic retrieval (reciprocal rank fusion)",
28117
+ capabilities: ["lexical", "semantic", "hybrid"]
28118
+ }
28119
+ };
28021
28120
  });
28022
28121
 
28023
- // src/datasource/skills/mailcrawl/methods.ts
28024
- class MailcrawlMethod {
28122
+ // src/datasource/skills/lazykatok/skill.ts
28123
+ class LazykatokSkill {
28025
28124
  client;
28026
- mode;
28027
28125
  instanceId;
28126
+ instances;
28127
+ pollingIntervalMs;
28028
28128
  tags;
28029
- account;
28030
- mailbox;
28031
- constructor(client, mode, instanceId, tags, account, mailbox) {
28032
- this.client = client;
28033
- this.mode = mode;
28034
- this.instanceId = instanceId;
28035
- this.tags = tags;
28036
- this.account = account;
28037
- this.mailbox = mailbox;
28129
+ sidecar;
28130
+ lastIndexedAt;
28131
+ constructor(options) {
28132
+ this.client = options.client;
28133
+ this.instanceId = options.instanceId ?? DEFAULT_INSTANCE_ID4;
28134
+ this.instances = options.instances !== undefined && options.instances.length > 0 ? options.instances : [this.instanceId];
28135
+ this.pollingIntervalMs = options.pollingIntervalMs ?? DEFAULT_POLLING_INTERVAL_MS3;
28136
+ this.tags = options.tags ?? DEFAULT_KAKAO_TAGS2;
28137
+ this.lastIndexedAt = options.lastIndexedAt;
28138
+ this.sidecar = options.sidecar === undefined ? undefined : new CrawlerSidecar({
28139
+ ...options.sidecar,
28140
+ channelIds: options.channelIds ?? options.sidecar.channelIds ?? [],
28141
+ channelNames: options.channelNames ?? options.sidecar.channelNames ?? []
28142
+ });
28038
28143
  }
28039
28144
  describe() {
28040
28145
  return {
28041
- name: `mailcrawl-${this.mode}`,
28042
- type: this.mode === "bm25" ? "bm25" : this.mode === "semantic" ? "vector" : "hybrid",
28043
- description: `${this.mode} retrieval over archived email via the external mailcrawl CLI`,
28044
- status: "active",
28045
- capabilities: ["email", this.mode, "scoped", "external-cli", "path-opaque-sources"],
28046
- datasourceId: "mailcrawl",
28047
- tags: this.tags
28048
- };
28049
- }
28050
- async retrieve(query, options) {
28051
- if (!query.trim())
28052
- return [];
28053
- const result = await this.client.search(this.mode, query, {
28054
- topK: options.topK,
28055
- signal: options.signal
28056
- });
28057
- if (!result.ok)
28058
- throw datasourceCliError("mailcrawl", `search --mode ${this.mode}`, result);
28059
- return result.hits.filter((hit) => {
28060
- if (this.account !== undefined && hit.accountId !== this.account)
28061
- return false;
28062
- if (this.mailbox !== undefined && hit.mailbox !== this.mailbox)
28063
- return false;
28064
- const source = datasourceSourcePath("mailcrawl", this.instanceId, hit.chunkId);
28065
- return matchesDatasourceScope(source, options.scope);
28066
- }).map((hit) => {
28067
- const source = datasourceSourcePath("mailcrawl", this.instanceId, hit.chunkId);
28068
- return {
28069
- id: `mailcrawl:${this.instanceId}:${hit.chunkId}`,
28070
- content: hit.snippet,
28071
- source,
28072
- score: hit.score,
28073
- metadata: {
28074
- method: `mailcrawl-${this.mode}`,
28075
- datasourceId: "mailcrawl",
28076
- instanceId: this.instanceId,
28077
- mode: this.mode,
28078
- chunkId: hit.chunkId,
28079
- messageId: hit.messageId,
28080
- threadId: hit.threadId,
28081
- accountId: hit.accountId,
28082
- mailbox: hit.mailbox,
28083
- subject: hit.subject,
28084
- from: hit.from,
28085
- to: hit.to,
28086
- date: hit.date
28087
- }
28088
- };
28089
- }).slice(0, options.topK ?? 20);
28090
- }
28091
- }
28092
- var modes, MAILCRAWL_MODES;
28093
- var init_methods4 = __esm(() => {
28094
- init_errors4();
28095
- init_scope2();
28096
- modes = ["bm25", "semantic", "hybrid"];
28097
- MAILCRAWL_MODES = modes;
28098
- });
28099
-
28100
- // src/datasource/skills/mailcrawl/skill.ts
28101
- class MailcrawlSkill {
28102
- client;
28103
- instanceId;
28104
- instances;
28105
- interval;
28106
- tags;
28107
- account;
28108
- mailbox;
28109
- lastIndexedAt;
28110
- constructor(options = {}) {
28111
- const connectorOptions = options.connectorOptions ?? options;
28112
- this.instanceId = options.instanceId ?? "default";
28113
- this.instances = options.instances?.length ? options.instances : [this.instanceId];
28114
- validateMailcrawlInstanceId(this.instanceId);
28115
- for (const instance of this.instances)
28116
- validateMailcrawlInstanceId(instance);
28117
- this.interval = options.pollingIntervalMs ?? 15 * 60 * 1000;
28118
- this.tags = options.tags ?? TAGS;
28119
- this.account = connectorOptions.account;
28120
- this.mailbox = connectorOptions.mailbox;
28121
- this.client = options.client ?? new MailcrawlClient({ ...connectorOptions, instanceId: this.instanceId });
28122
- }
28123
- describe() {
28124
- return {
28125
- name: "mailcrawl",
28126
- id: "mailcrawl",
28127
- type: "mailcrawl-archive",
28128
- description: "Local email archive datasource via the external mailcrawl CLI",
28129
- capabilities: ["email", "external-cli", "polling", "incremental", "bm25", "semantic", "hybrid"],
28130
- tags: this.tags,
28146
+ name: KAKAO_DATASOURCE_ID2,
28147
+ id: KAKAO_DATASOURCE_ID2,
28148
+ type: KAKAO_SKILL_TYPE,
28149
+ description: "KakaoTalk datasource via the external lazykatok CLI",
28150
+ capabilities: ["chat", "external-cli", "polling", "bm25", "semantic"],
28151
+ tags: this.tags,
28131
28152
  status: "active",
28132
28153
  requiresExternalCli: true,
28133
- datasourceId: "mailcrawl",
28154
+ datasourceId: KAKAO_DATASOURCE_ID2,
28134
28155
  instanceId: this.instanceId,
28135
28156
  instances: this.instances
28136
28157
  };
28137
28158
  }
28138
28159
  polling() {
28139
28160
  return {
28140
- mode: this.interval > 0 ? "poll" : "none",
28141
- ...this.interval > 0 ? { intervalMs: this.interval } : {},
28161
+ mode: "poll",
28162
+ intervalMs: this.pollingIntervalMs,
28142
28163
  lastIndexedAt: this.lastIndexedAt
28143
28164
  };
28144
28165
  }
28145
28166
  async index(options) {
28146
28167
  try {
28147
- const result = await this.client.sync(options?.signal);
28148
- if (!result.ok) {
28149
- 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);
28150
- }
28151
- const indexed = await this.client.index(options?.signal);
28152
- const diagnostics = [];
28153
- if (!indexed.ok) {
28154
- diagnostics.push({
28155
- code: indexed.reason === "remote-embedding-rejected" ? "datasource-embedding-egress-rejected" : "datasource-index-failed",
28156
- severity: "warning",
28157
- message: `mailcrawl semantic index unavailable (${indexed.reason}); BM25 remains available`,
28168
+ const doctor = await this.client.doctor(options?.signal);
28169
+ if (!doctor.ok)
28170
+ return this.fail("datasource-unavailable", doctor);
28171
+ const sync = await this.client.sync(options?.signal);
28172
+ if (!sync.ok)
28173
+ return this.fail("datasource-index-failed", sync);
28174
+ if (this.sidecar !== undefined) {
28175
+ this.lastIndexedAt = Date.now();
28176
+ return {
28177
+ ok: true,
28158
28178
  instanceId: this.instanceId,
28159
- source: "mailcrawl"
28160
- });
28179
+ skill: KAKAO_DATASOURCE_ID2,
28180
+ chunkCount: syncChunkCount(sync),
28181
+ indexedAt: this.lastIndexedAt,
28182
+ diagnostics: await this.syncSidecar()
28183
+ };
28161
28184
  }
28185
+ const indexResult = await this.client.index(options?.signal);
28186
+ if (!indexResult.ok)
28187
+ return this.fail("datasource-index-failed", indexResult);
28162
28188
  this.lastIndexedAt = Date.now();
28189
+ const chunkCount = "data" in indexResult && typeof indexResult.data === "object" && indexResult.data !== null && "chunkCount" in indexResult.data && typeof indexResult.data.chunkCount === "number" ? indexResult.data.chunkCount : 0;
28163
28190
  return {
28164
28191
  ok: true,
28165
28192
  instanceId: this.instanceId,
28166
- skill: "mailcrawl",
28167
- chunkCount: result.data.chunksAdded ?? result.data.messages,
28193
+ skill: KAKAO_DATASOURCE_ID2,
28194
+ chunkCount,
28168
28195
  indexedAt: this.lastIndexedAt,
28169
- diagnostics
28196
+ diagnostics: []
28170
28197
  };
28171
- } catch {
28172
- return this.fail("datasource-unavailable", "mailcrawl sync failed unexpectedly");
28198
+ } catch (error) {
28199
+ return this.fail("datasource-unavailable", {
28200
+ ok: false,
28201
+ reason: "spawn-error",
28202
+ stdout: "",
28203
+ stderr: describeRetrievalError(error),
28204
+ code: null
28205
+ });
28173
28206
  }
28174
28207
  }
28208
+ close() {
28209
+ this.sidecar?.close();
28210
+ }
28175
28211
  retrievalMethods() {
28212
+ const cliBm25 = new LazykatokBm25Method({ client: this.client, instanceId: this.instanceId, tags: this.tags });
28213
+ const { sidecar } = this;
28214
+ if (sidecar === undefined) {
28215
+ return [
28216
+ cliBm25,
28217
+ new LazykatokSemanticMethod({ client: this.client, instanceId: this.instanceId, tags: this.tags })
28218
+ ];
28219
+ }
28220
+ const shared = {
28221
+ sidecar,
28222
+ descriptor: sidecar.descriptor,
28223
+ datasourceId: KAKAO_DATASOURCE_ID2,
28224
+ skillType: KAKAO_SKILL_TYPE,
28225
+ instanceId: this.instanceId,
28226
+ tags: this.tags
28227
+ };
28228
+ const bm25 = new SidecarRetrievalMethod({ ...shared, mode: "bm25" });
28229
+ if (!sidecar.semanticEnabled)
28230
+ return [cliBm25, bm25];
28176
28231
  return [
28177
- new MailcrawlMethod(this.client, "bm25", this.instanceId, this.tags, this.account, this.mailbox),
28178
- new MailcrawlMethod(this.client, "semantic", this.instanceId, this.tags, this.account, this.mailbox),
28179
- new MailcrawlMethod(this.client, "hybrid", this.instanceId, this.tags, this.account, this.mailbox)
28232
+ cliBm25,
28233
+ bm25,
28234
+ new SidecarRetrievalMethod({ ...shared, mode: "semantic" }),
28235
+ new SidecarRetrievalMethod({ ...shared, mode: "hybrid" })
28180
28236
  ];
28181
28237
  }
28238
+ async syncSidecar() {
28239
+ if (this.sidecar === undefined || !this.sidecar.semanticEnabled)
28240
+ return [];
28241
+ const base = { instanceId: this.instanceId, source: KAKAO_DATASOURCE_ID2 };
28242
+ let result;
28243
+ try {
28244
+ result = await this.sidecar.sync();
28245
+ } catch (error) {
28246
+ result = { ok: false, message: error instanceof Error ? error.message : String(error) };
28247
+ }
28248
+ if (!result.ok) {
28249
+ return [
28250
+ {
28251
+ code: "datasource-index-failed",
28252
+ severity: "warning",
28253
+ message: `semantic-unavailable: ${result.message}`.slice(0, 300),
28254
+ ...base
28255
+ }
28256
+ ];
28257
+ }
28258
+ const diagnostics = [];
28259
+ if (result.skipped > 0) {
28260
+ diagnostics.push({
28261
+ code: "datasource-index-partial",
28262
+ severity: "warning",
28263
+ message: `semantic index skipped ${result.skipped} message(s) the embedder rejected; they stay searchable by BM25 and are retried when edited`,
28264
+ ...base
28265
+ });
28266
+ }
28267
+ if (result.remaining > 0) {
28268
+ diagnostics.push({
28269
+ code: "datasource-index-partial",
28270
+ severity: "info",
28271
+ message: `semantic index catching up: ${result.embedded} embedded this refresh, ${result.remaining} remaining; hybrid search uses BM25 for the rest until a later refresh`,
28272
+ ...base
28273
+ });
28274
+ }
28275
+ return diagnostics;
28276
+ }
28182
28277
  describeSources() {
28183
28278
  return this.instances.map((instanceId) => ({
28184
- source: datasourceSourcePath("mailcrawl", instanceId),
28185
- datasourceId: "mailcrawl",
28186
- skill: "mailcrawl",
28279
+ source: datasourceSourcePath(KAKAO_DATASOURCE_ID2, instanceId),
28280
+ datasourceId: KAKAO_DATASOURCE_ID2,
28281
+ skill: KAKAO_DATASOURCE_ID2,
28187
28282
  instanceId,
28188
- contentType: "email",
28189
- metadata: { datasourceId: "mailcrawl", instanceId, tags: this.tags }
28283
+ contentType: "chat",
28284
+ metadata: {
28285
+ datasourceId: KAKAO_DATASOURCE_ID2,
28286
+ instanceId,
28287
+ tags: this.tags
28288
+ }
28190
28289
  }));
28191
28290
  }
28192
28291
  skillManifest() {
28193
- const scopes = this.instances.map((id) => `- \`${datasourceSourcePath("mailcrawl", id)}\``).join(`
28292
+ const instanceSources = this.instances.map((instanceId) => `- \`${datasourceSourcePath(KAKAO_DATASOURCE_ID2, instanceId)}\``).join(`
28194
28293
  `);
28294
+ const cadence = this.pollingIntervalMs > 0 ? `roughly every ${Math.round(this.pollingIntervalMs / 60000)} minute(s) when auto-refresh runs` : "on manual refresh only";
28195
28295
  return {
28196
- name: "datasource-mailcrawl",
28197
- description: "Search local email synchronized and indexed by mailcrawl.",
28198
- content: `# mailcrawl email datasource
28199
-
28200
- Use this skill for authorized local email retrieval. AutoRAG invokes the external \`mailcrawl\` CLI and never opens its SQLite archive directly.
28201
-
28202
- ## Indexing
28203
- Indexing is server-managed and incremental; do not trigger sync yourself.
28204
-
28205
- ## Search
28206
- Call the dedicated \`${datasourceSearchToolName("mailcrawl")}\` tool with a natural-language query. Available scopes:
28207
- ${scopes}
28208
-
28209
- The selectable retrieval modes are BM25, semantic, and hybrid. Scope arguments can only narrow trusted access. Source identifiers remain opaque and hierarchical.
28210
-
28211
- ## Native CLI
28212
- The external \`mailcrawl\` CLI owns the archive and index. When the dedicated tool cannot express what you need, call mailcrawl directly through \`bash\`:
28213
- - \`mailcrawl search --mode hybrid --limit 20 --json "<query>"\` (\`bm25\` and \`semantic\` modes also available; \`--account\` / \`--mailbox\` narrow the archive)
28214
- Never pass datasource virtual paths (\`/mailcrawl/...\`) to bash; they are not OS paths.`
28296
+ name: `datasource-${KAKAO_DATASOURCE_ID2}`,
28297
+ description: "Search indexed KakaoTalk chat history (messages, senders, room context). Use for questions about KakaoTalk conversations, decisions made in chats, or who said what.",
28298
+ content: [
28299
+ `# KakaoTalk datasource (${KAKAO_SKILL_TYPE})`,
28300
+ "",
28301
+ "This skill searches KakaoTalk chats that are indexed through the external `lazykatok` CLI. AutoRAG never reads KakaoTalk databases directly.",
28302
+ "",
28303
+ "## When to use",
28304
+ "Use this skill when the question is about KakaoTalk conversations, chat participants, or content shared inside chats.",
28305
+ "",
28306
+ "## Indexing",
28307
+ `Indexing is server-managed and refreshed ${cadence}. You do not trigger indexing; just search.`,
28308
+ "",
28309
+ "## How to search",
28310
+ `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:`,
28311
+ instanceSources.length > 0 ? instanceSources : "- (no configured instances)",
28312
+ "",
28313
+ "Chat/channel filtering is owned by lazykatok.",
28314
+ "",
28315
+ "## Native CLI",
28316
+ "The external `lazykatok` CLI owns the archive, index, and credentials. When the dedicated tool cannot express what you need, call lazykatok directly through `bash`:",
28317
+ '- `lazykatok search bm25 "<query>" --json --limit 20` — lexical search (`semantic` mode also available)',
28318
+ "- `lazykatok chunk get <chunkId> --json` — fetch one chunk by id (chunk ids appear in result metadata)",
28319
+ "- `lazykatok chunk context <chunkId> --json` — surrounding messages of a chunk",
28320
+ "Never pass datasource virtual paths (`/kakao/...`) to bash; they are not OS paths.",
28321
+ "",
28322
+ "## Output rules",
28323
+ "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."
28324
+ ].join(`
28325
+ `)
28215
28326
  };
28216
28327
  }
28217
- fail(code, message) {
28328
+ fail(code, result) {
28329
+ const base = result.stderr.length > 0 ? `${result.reason}: ${result.stderr.trim().slice(0, 4000)}` : result.reason;
28330
+ const message = code === "datasource-index-failed" && result.reason === "timeout" ? `${base} — ${connectorSyncTimeoutHint()}` : base;
28331
+ const diagnostic = {
28332
+ code,
28333
+ severity: code === "datasource-unavailable" ? "warning" : "error",
28334
+ message,
28335
+ instanceId: this.instanceId,
28336
+ source: KAKAO_DATASOURCE_ID2
28337
+ };
28218
28338
  return {
28219
28339
  ok: false,
28220
28340
  instanceId: this.instanceId,
28221
- skill: "mailcrawl",
28341
+ skill: KAKAO_DATASOURCE_ID2,
28222
28342
  indexedAt: Date.now(),
28223
- diagnostics: [
28224
- {
28225
- code,
28226
- severity: code === "datasource-unavailable" ? "warning" : "error",
28227
- message: `mailcrawl ${message}`,
28228
- instanceId: this.instanceId,
28229
- source: "mailcrawl"
28230
- }
28231
- ],
28232
- error: message,
28343
+ diagnostics: [diagnostic],
28344
+ error: result.reason,
28233
28345
  code,
28234
28346
  message
28235
28347
  };
28236
28348
  }
28237
28349
  }
28238
- var TAGS;
28239
- var init_skill9 = __esm(() => {
28350
+ function syncChunkCount(sync) {
28351
+ if (!("data" in sync) || typeof sync.data !== "object" || sync.data === null)
28352
+ return 0;
28353
+ const meta = "metadata" in sync.data && typeof sync.data.metadata === "object" && sync.data.metadata !== null ? sync.data.metadata : undefined;
28354
+ if (typeof meta?.chunks === "number")
28355
+ return meta.chunks;
28356
+ if ("messageCount" in sync.data && typeof sync.data.messageCount === "number")
28357
+ return sync.data.messageCount;
28358
+ return 0;
28359
+ }
28360
+ var KAKAO_DATASOURCE_ID2 = "kakao", KAKAO_SKILL_TYPE = "kakaotalk", DEFAULT_INSTANCE_ID4 = "default", DEFAULT_POLLING_INTERVAL_MS3, DEFAULT_KAKAO_TAGS2;
28361
+ var init_skill7 = __esm(() => {
28362
+ init_skip();
28240
28363
  init_scope2();
28241
- init_client9();
28242
- init_config3();
28364
+ init_sidecar();
28243
28365
  init_methods4();
28244
- TAGS = ["mailcrawl", "email", "pii"];
28366
+ init_methods3();
28367
+ DEFAULT_POLLING_INTERVAL_MS3 = 15 * 60 * 1000;
28368
+ DEFAULT_KAKAO_TAGS2 = ["kakaotalk", "personal", "pii"];
28245
28369
  });
28246
28370
 
28247
- // src/datasource/skills/mailcrawl/index.ts
28248
- var init_mailcrawl = __esm(() => {
28249
- init_client9();
28250
- init_methods4();
28251
- init_skill9();
28371
+ // src/datasource/skills/lazykatok/index.ts
28372
+ var init_lazykatok = __esm(() => {
28373
+ init_client8();
28374
+ init_methods3();
28375
+ init_skill7();
28376
+ init_types5();
28252
28377
  });
28253
28378
 
28254
- // src/datasource/sidecar/fusion.ts
28255
- function encodeVector(vector) {
28256
- const floats = Float32Array.from(vector);
28257
- return new Uint8Array(floats.buffer, floats.byteOffset, floats.byteLength);
28258
- }
28259
- function decodeVector(blob) {
28260
- if (blob.byteLength % Float32Array.BYTES_PER_ELEMENT !== 0)
28261
- return;
28262
- return new Float32Array(new Uint8Array(blob).buffer);
28263
- }
28264
- function cosine3(a, b) {
28265
- if (a.length === 0 || a.length !== b.length)
28266
- return 0;
28267
- let dot = 0;
28268
- let normA = 0;
28269
- let normB = 0;
28270
- for (let index = 0;index < a.length; index += 1) {
28271
- const x = a[index] ?? 0;
28272
- const y = b[index] ?? 0;
28273
- dot += x * y;
28274
- normA += x * x;
28275
- normB += y * y;
28379
+ // src/datasource/skills/mail-export/connector.ts
28380
+ import { readdir as readdir4, readFile as readFile5, stat as stat6 } from "node:fs/promises";
28381
+ import { basename as basename12, dirname as dirname25, extname as extname6, join as join37 } from "node:path";
28382
+ import { simpleParser as simpleParser2 } from "mailparser";
28383
+
28384
+ class MailExportConnector {
28385
+ options;
28386
+ constructor(options = {}) {
28387
+ this.options = options;
28276
28388
  }
28277
- if (normA === 0 || normB === 0)
28278
- return 0;
28279
- return dot / Math.sqrt(normA * normB);
28280
- }
28281
- function topKByCosine(query, rows, k) {
28282
- if (k <= 0)
28283
- return [];
28284
- const scored = [];
28285
- for (const row of rows) {
28286
- const score = cosine3(query, row.vector);
28287
- if (score > 0)
28288
- scored.push({ key: row.key, score });
28389
+ async fetch() {
28390
+ const paths = this.options.paths ?? [];
28391
+ if (paths.length === 0)
28392
+ return { ok: false, reason: "not-configured", message: "no mail export paths configured" };
28393
+ const maxDocuments = this.options.maxDocuments ?? DEFAULT_MAX_DOCUMENTS3;
28394
+ const maxBytes = this.options.maxBytesPerFile ?? DEFAULT_MAX_BYTES_PER_FILE2;
28395
+ const files = [];
28396
+ let missingPaths = 0;
28397
+ for (const path of paths) {
28398
+ try {
28399
+ const info = await stat6(path);
28400
+ if (info.isDirectory()) {
28401
+ const entries = await readdir4(path, { recursive: true, withFileTypes: true });
28402
+ for (const entry of entries) {
28403
+ if (!entry.isFile())
28404
+ continue;
28405
+ const extension = extname6(entry.name).toLowerCase();
28406
+ if (extension === ".eml" || extension === ".mbox") {
28407
+ files.push(join37(entry.parentPath, entry.name));
28408
+ }
28409
+ }
28410
+ } else if (info.isFile()) {
28411
+ files.push(path);
28412
+ }
28413
+ } catch {
28414
+ missingPaths += 1;
28415
+ }
28416
+ }
28417
+ if (missingPaths === paths.length) {
28418
+ return { ok: false, reason: "unavailable", message: "no configured mail export paths were readable" };
28419
+ }
28420
+ const documents = [];
28421
+ const warnings = [];
28422
+ if (missingPaths > 0)
28423
+ warnings.push(`${missingPaths} configured path(s) were unreadable`);
28424
+ let parseFailures = 0;
28425
+ let skippedLarge = 0;
28426
+ for (const file of files.sort()) {
28427
+ if (documents.length >= maxDocuments)
28428
+ break;
28429
+ let bytes;
28430
+ try {
28431
+ const info = await stat6(file);
28432
+ if (info.size > maxBytes) {
28433
+ skippedLarge += 1;
28434
+ continue;
28435
+ }
28436
+ bytes = await readFile5(file);
28437
+ } catch {
28438
+ parseFailures += 1;
28439
+ continue;
28440
+ }
28441
+ const stem = basename12(file, extname6(file));
28442
+ const mailboxDir = basename12(dirname25(file));
28443
+ const hierarchy = ["mailboxes", mailboxDir, stem];
28444
+ const extension = extname6(file).toLowerCase();
28445
+ const rawMessages = extension === ".mbox" ? splitMbox(bytes.toString("utf8")) : [bytes.toString("utf8")];
28446
+ for (const [index, raw] of rawMessages.entries()) {
28447
+ if (documents.length >= maxDocuments)
28448
+ break;
28449
+ try {
28450
+ const parsed = await simpleParser2(raw);
28451
+ const subject = parsed.subject ?? "(no subject)";
28452
+ const from = Array.isArray(parsed.from) ? parsed.from[0]?.text : parsed.from?.text;
28453
+ const to = Array.isArray(parsed.to) ? parsed.to[0]?.text : parsed.to?.text;
28454
+ const body = (parsed.text ?? "").trim().slice(0, MAX_BODY_CHARS);
28455
+ const headerBlock = [
28456
+ `Subject: ${subject}`,
28457
+ ...from !== undefined ? [`From: ${from}`] : [],
28458
+ ...to !== undefined ? [`To: ${to}`] : [],
28459
+ ...parsed.date !== undefined ? [`Date: ${parsed.date.toISOString()}`] : []
28460
+ ].join(`
28461
+ `);
28462
+ const content = `${headerBlock}
28463
+
28464
+ ${body}`.trim();
28465
+ if (content.length === 0)
28466
+ continue;
28467
+ documents.push({
28468
+ docId: extension === ".mbox" ? `${stem}-${index + 1}` : stem,
28469
+ hierarchy,
28470
+ title: subject,
28471
+ content,
28472
+ ...parsed.date !== undefined ? { publishedAt: parsed.date.getTime() } : {},
28473
+ metadata: {
28474
+ path: file,
28475
+ subject,
28476
+ ...from !== undefined ? { from } : {},
28477
+ messageIndex: index
28478
+ }
28479
+ });
28480
+ } catch {
28481
+ parseFailures += 1;
28482
+ }
28483
+ }
28484
+ }
28485
+ if (skippedLarge > 0)
28486
+ warnings.push(`${skippedLarge} file(s) exceeded the size limit and were skipped`);
28487
+ if (parseFailures > 0)
28488
+ warnings.push(`${parseFailures} message(s) failed to parse`);
28489
+ return { ok: true, documents, ...warnings.length > 0 ? { warnings } : {} };
28289
28490
  }
28290
- scored.sort((a, b) => b.score - a.score || a.key.localeCompare(b.key));
28291
- return scored.slice(0, k);
28292
28491
  }
28293
- function reciprocalRankFusion(lists) {
28294
- const scores = new Map;
28295
- for (const list of lists) {
28296
- const seen = new Set;
28297
- for (const [index, key] of list.entries()) {
28298
- if (seen.has(key))
28299
- continue;
28300
- seen.add(key);
28301
- scores.set(key, (scores.get(key) ?? 0) + 1 / (RRF_K2 + index + 1));
28492
+ function splitMbox(text) {
28493
+ const lines = text.split(/\r?\n/u);
28494
+ const messages = [];
28495
+ let current = [];
28496
+ for (const line of lines) {
28497
+ if (MBOX_SEPARATOR.test(line) && line.startsWith("From ")) {
28498
+ if (current.length > 0)
28499
+ messages.push(current.join(`
28500
+ `));
28501
+ current = [];
28502
+ continue;
28302
28503
  }
28504
+ current.push(line);
28303
28505
  }
28304
- return [...scores.entries()].map(([key, score]) => ({ key, score })).sort((a, b) => b.score - a.score);
28506
+ if (current.length > 0)
28507
+ messages.push(current.join(`
28508
+ `));
28509
+ return messages.filter((message) => message.trim().length > 0);
28305
28510
  }
28306
- var RRF_K2 = 60;
28307
-
28308
- // src/datasource/sidecar/index.ts
28309
- import { createHash as createHash14 } from "node:crypto";
28310
- import { mkdirSync as mkdirSync23, statSync as statSync3 } from "node:fs";
28311
- import { dirname as dirname25 } from "node:path";
28312
- import { DatabaseSync as DatabaseSync2 } from "node:sqlite";
28313
- import { pathToFileURL as pathToFileURL2 } from "node:url";
28511
+ var DEFAULT_MAX_DOCUMENTS3 = 500, DEFAULT_MAX_BYTES_PER_FILE2, MAX_BODY_CHARS = 50000, MBOX_SEPARATOR;
28512
+ var init_connector4 = __esm(() => {
28513
+ DEFAULT_MAX_BYTES_PER_FILE2 = 50 * 1024 * 1024;
28514
+ MBOX_SEPARATOR = /^From .*\d{4}$/mu;
28515
+ });
28314
28516
 
28315
- class CrawlerSidecar {
28316
- descriptor;
28317
- options;
28318
- select;
28319
- where;
28320
- opened;
28321
- attached;
28322
- lastSemantic;
28323
- constructor(options) {
28324
- this.options = options;
28325
- const { descriptor } = options;
28326
- this.descriptor = descriptor;
28327
- const metadataColumns = descriptor.metadata.map((column, index) => `${column.sql} as m${index}`);
28328
- this.select = [
28329
- `${descriptor.keyExpression} as k`,
28330
- `${descriptor.textExpression} as content`,
28331
- `${descriptor.titleExpression ?? "null"} as title`,
28332
- `${descriptor.epochSecondsExpression ?? "null"} as epoch`,
28333
- ...metadataColumns
28334
- ].join(", ");
28335
- const scope = descriptor.scopeExpression !== undefined && options.workspaceId !== undefined;
28336
- const allowlist = [
28337
- ...(options.channelIds?.length ?? 0) > 0 ? [`${descriptor.channelIdExpression} in (${options.channelIds?.map(() => "?").join(", ")})`] : [],
28338
- ...(options.channelNames?.length ?? 0) > 0 ? [`${descriptor.channelNameExpression} in (${options.channelNames?.map(() => "?").join(", ")})`] : []
28339
- ];
28340
- this.where = [
28341
- descriptor.liveWhere,
28342
- ...scope ? [`${descriptor.scopeExpression} = ?`] : [],
28343
- ...allowlist.length > 0 ? [`(${allowlist.join(" or ")})`] : []
28344
- ].join(" and ");
28345
- }
28346
- get db() {
28347
- if (this.opened !== undefined)
28348
- return this.opened;
28349
- mkdirSync23(dirname25(this.options.sidecarDbPath), { recursive: true });
28350
- const db = new DatabaseSync2(this.options.sidecarDbPath);
28351
- db.exec(`pragma busy_timeout = ${BUSY_TIMEOUT_MS}`);
28352
- db.exec("pragma journal_mode = wal");
28353
- db.exec(`
28354
- create table if not exists sidecar_meta (k text primary key, v text not null);
28355
- create table if not exists sidecar_vec (message_key text primary key, hash text not null, vec blob not null);
28356
- `);
28357
- this.opened = db;
28358
- return db;
28359
- }
28360
- attachCrawler() {
28361
- let file;
28362
- try {
28363
- const { dev, ino } = statSync3(this.options.crawlerDbPath);
28364
- file = { dev, ino };
28365
- } catch {
28366
- this.detachCrawler();
28367
- return false;
28368
- }
28369
- if (this.attached !== undefined) {
28370
- if (this.attached.dev === file.dev && this.attached.ino === file.ino)
28371
- return true;
28372
- this.detachCrawler();
28373
- }
28374
- const crawlerUri = `${pathToFileURL2(this.options.crawlerDbPath).href}?mode=ro`.replaceAll("'", "''");
28375
- try {
28376
- this.db.exec(`attach database '${crawlerUri}' as crawler`);
28377
- } catch {
28378
- return false;
28379
- }
28380
- const { descriptor } = this.options;
28381
- try {
28382
- this.db.prepare(`select ${this.select} from ${descriptor.fromClause} where ${this.where} limit 0`).all(...this.whereParams());
28383
- this.db.prepare(`select 1 from ${descriptor.fts.table} limit 0`).all();
28384
- } catch {
28385
- this.db.exec("detach database crawler");
28386
- return false;
28517
+ // src/datasource/skills/mail-export/skill.ts
28518
+ var MAIL_EXPORT_SKILL_DEFINITION, MailExportSkill;
28519
+ var init_skill8 = __esm(() => {
28520
+ init_connector_skill();
28521
+ init_connector4();
28522
+ MAIL_EXPORT_SKILL_DEFINITION = {
28523
+ skillName: "mail-export",
28524
+ skillType: "mail-export",
28525
+ description: "Local mail export datasource",
28526
+ capabilities: ["email", "filesystem"],
28527
+ defaultTags: ["mail-export", "email", "pii"],
28528
+ contentType: "email",
28529
+ manifestDescription: "Search indexed local mail exports (mbox/eml archives). Use for questions about archived email conversations.",
28530
+ manifestNotes: [
28531
+ "Only the server-configured export paths are indexed; archives are read locally and never uploaded."
28532
+ ]
28533
+ };
28534
+ MailExportSkill = class MailExportSkill extends ConnectorDatasourceSkill {
28535
+ constructor(options = {}) {
28536
+ const { connector, connectorOptions, ...rest } = options;
28537
+ super(MAIL_EXPORT_SKILL_DEFINITION, {
28538
+ ...rest,
28539
+ connector: connector ?? new MailExportConnector(connectorOptions ?? {})
28540
+ });
28387
28541
  }
28388
- this.attached = file;
28389
- this.lastSemantic = undefined;
28390
- return true;
28391
- }
28392
- detachCrawler() {
28393
- if (this.attached === undefined)
28394
- return;
28395
- this.attached = undefined;
28396
- this.lastSemantic = undefined;
28397
- try {
28398
- this.opened?.exec("detach database crawler");
28399
- } catch {}
28400
- }
28401
- get semanticEnabled() {
28402
- return this.options.embedder !== undefined;
28542
+ };
28543
+ });
28544
+
28545
+ // src/datasource/skills/mail-export/index.ts
28546
+ var init_mail_export = __esm(() => {
28547
+ init_connector4();
28548
+ init_skill8();
28549
+ });
28550
+
28551
+ // src/datasource/skills/mailcrawl/config.ts
28552
+ import { chmodSync, mkdirSync as mkdirSync23 } from "node:fs";
28553
+ function validateMailcrawlInstanceId(instanceId) {
28554
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/u.test(instanceId)) {
28555
+ throw new Error("mailcrawl instanceId must be a safe single path segment");
28403
28556
  }
28404
- close() {
28405
- this.opened?.close();
28406
- this.opened = undefined;
28407
- this.attached = undefined;
28408
- this.lastSemantic = undefined;
28557
+ }
28558
+ function ensurePrivateMailcrawlDataDir(path) {
28559
+ mkdirSync23(path, { recursive: true, mode: 448 });
28560
+ if (process.platform !== "win32")
28561
+ chmodSync(path, 448);
28562
+ }
28563
+ var init_config3 = () => {};
28564
+
28565
+ // src/datasource/skills/mailcrawl/client.ts
28566
+ import { spawn as spawn15 } from "node:child_process";
28567
+
28568
+ class MailcrawlClient {
28569
+ options;
28570
+ constructor(options = {}) {
28571
+ this.options = options;
28572
+ if (options.instanceId !== undefined)
28573
+ validateMailcrawlInstanceId(options.instanceId);
28409
28574
  }
28410
- whereParams() {
28411
- const { descriptor, workspaceId, channelIds = [], channelNames = [] } = this.options;
28412
- return [
28413
- ...descriptor.scopeExpression !== undefined && workspaceId !== undefined ? [workspaceId] : [],
28414
- ...channelIds,
28415
- ...channelNames
28575
+ async sync(signal) {
28576
+ const args = [
28577
+ "sync",
28578
+ "--json",
28579
+ ...this.options.account === undefined ? [] : ["--account", this.options.account],
28580
+ ...this.options.mailbox === undefined ? [] : ["--mailbox", this.options.mailbox],
28581
+ ...this.options.backend === undefined ? [] : ["--backend", this.options.backend],
28582
+ ...this.options.source === undefined ? [] : ["--source", this.options.source],
28583
+ ...this.options.fixture === undefined ? [] : ["--fixture", this.options.fixture],
28584
+ ...this.options.himalayaConfig === undefined ? [] : ["--himalaya-config", this.options.himalayaConfig]
28416
28585
  ];
28586
+ const result = await this.run(args, signal, this.indexTimeoutMs());
28587
+ if (!result.ok)
28588
+ return failure3(result);
28589
+ const parsed = parseObject2(result.stdout);
28590
+ if (parsed === undefined || !hasAnyNumber(parsed, ["added", "updated", "deleted", "unchanged", "chunksAdded"])) {
28591
+ return failure3({ ...result, ok: false, reason: "invalid-output" });
28592
+ }
28593
+ const messages = number(parsed, "added") ?? number(parsed, "updated") ?? number(parsed, "unchanged") ?? 0;
28594
+ return ok4({
28595
+ messages,
28596
+ added: number(parsed, "added"),
28597
+ updated: number(parsed, "updated"),
28598
+ deleted: number(parsed, "deleted"),
28599
+ unchanged: number(parsed, "unchanged"),
28600
+ chunksAdded: number(parsed, "chunksAdded"),
28601
+ archiveRevision: string(parsed, "archiveRevision")
28602
+ }, result);
28417
28603
  }
28418
- searchLexical(query, topK) {
28419
- const match = toMatchQuery(query);
28420
- if (match === undefined || topK <= 0 || !this.attachCrawler())
28421
- return [];
28422
- const { descriptor } = this.options;
28423
- const rows = this.db.prepare(`select ${this.select}, bm25(${descriptor.fts.table}) as rank
28424
- from ${descriptor.fromClause} join ${descriptor.fts.table} on ${descriptor.fts.joinOn}
28425
- where ${descriptor.fts.table} match ? and ${this.where}
28426
- order by rank limit ?`).all(match, ...this.whereParams(), topK);
28427
- return rows.map((row) => this.toHit(this.toRow(row), -Number(row.rank)));
28428
- }
28429
- async searchSemantic(query, topK) {
28430
- const trimmed = query.trim();
28431
- if (this.options.embedder === undefined || trimmed.length === 0 || topK <= 0 || !this.attachCrawler())
28432
- return [];
28433
- const ranked = await this.rankSemantic(trimmed, topK * POOL_FACTOR);
28434
- if (ranked.length === 0)
28435
- return [];
28436
- const live = this.hydrate(ranked.map((entry) => entry.key));
28437
- const hits = [];
28438
- for (const entry of ranked) {
28439
- const row = live.get(entry.key);
28440
- if (row !== undefined)
28441
- hits.push(this.toHit(row, entry.score));
28442
- if (hits.length >= topK)
28443
- break;
28604
+ async search(mode, query, options) {
28605
+ if (query.trim().length === 0)
28606
+ return { ok: true, hits: [], stdout: "", stderr: "", code: 0 };
28607
+ const args = [
28608
+ "search",
28609
+ "--mode",
28610
+ mode === "keyword" ? "bm25" : mode,
28611
+ ...this.options.account === undefined ? [] : ["--account", this.options.account],
28612
+ ...this.options.mailbox === undefined ? [] : ["--mailbox", this.options.mailbox],
28613
+ "--limit",
28614
+ String(options?.topK ?? 20),
28615
+ "--json",
28616
+ query.trim()
28617
+ ];
28618
+ const result = await this.run(args, options?.signal, this.options.timeoutMs ?? DEFAULT_SEARCH_TIMEOUT_MS2);
28619
+ if (!result.ok)
28620
+ return failure3(result);
28621
+ const parsed = parseJson3(result.stdout);
28622
+ if (parsed === undefined)
28623
+ return failure3({ ...result, ok: false, reason: "invalid-output" });
28624
+ const rows = Array.isArray(parsed) ? parsed : objectArray(parsed, ["hits", "results", "items"]);
28625
+ if (rows === undefined)
28626
+ return failure3({ ...result, ok: false, reason: "invalid-output" });
28627
+ const hits = rows.map((row, index) => normalizeHit2(row, mode, index));
28628
+ if (hits.some((hit) => hit === undefined)) {
28629
+ return failure3({ ...result, ok: false, reason: "invalid-output" });
28444
28630
  }
28445
- return hits;
28631
+ return {
28632
+ ok: true,
28633
+ hits,
28634
+ stdout: result.stdout,
28635
+ stderr: result.stderr,
28636
+ code: result.code ?? 0
28637
+ };
28446
28638
  }
28447
- async rankSemantic(query, pool) {
28448
- const now = Date.now();
28449
- let cached = this.lastSemantic;
28450
- if (cached === undefined || cached.query !== query || now - cached.at > SEMANTIC_REUSE_MS) {
28451
- const embedder = this.options.embedder;
28452
- if (embedder === undefined)
28453
- return [];
28454
- cached = {
28455
- query,
28456
- at: now,
28457
- vector: embedder.embed([query]).then((vectors) => vectors[0], () => {
28458
- return;
28459
- }),
28460
- ranked: undefined
28461
- };
28462
- this.lastSemantic = cached;
28639
+ indexTimeoutMs() {
28640
+ return this.options.indexTimeoutMs ?? this.options.timeoutMs ?? DEFAULT_INDEX_TIMEOUT_MS3;
28641
+ }
28642
+ async run(args, signal, timeoutMs) {
28643
+ const violation = findRemoteEmbeddingViolation({ ...process.env, ...this.options.env ?? {} });
28644
+ if (violation !== undefined) {
28645
+ return { ok: false, reason: "remote-embedding-rejected", stdout: "", stderr: "", code: null };
28463
28646
  }
28464
- const vector = await cached.vector;
28465
- if (vector === undefined) {
28466
- if (this.lastSemantic === cached)
28467
- this.lastSemantic = undefined;
28468
- return [];
28647
+ const dataDir = this.options.dataDir;
28648
+ if (dataDir !== undefined)
28649
+ ensurePrivateMailcrawlDataDir(dataDir);
28650
+ const env = {};
28651
+ for (const [key, value] of Object.entries(process.env)) {
28652
+ if (value !== undefined && (SAFE_ENV_KEYS2.has(key) || SAFE_MAILCRAWL_ENV_KEYS.has(key)))
28653
+ env[key] = value;
28469
28654
  }
28470
- if (cached.ranked === undefined || cached.ranked.pool < pool) {
28471
- const wide = pool * POOL_FACTOR;
28472
- cached.ranked = { pool: wide, keys: topKByCosine(vector, this.storedVectors(), wide) };
28655
+ for (const [key, value] of Object.entries(this.options.env ?? {})) {
28656
+ if (value === undefined)
28657
+ delete env[key];
28658
+ else if (SAFE_ENV_KEYS2.has(key) || SAFE_MAILCRAWL_ENV_KEYS.has(key))
28659
+ env[key] = value;
28473
28660
  }
28474
- return cached.ranked.keys.slice(0, pool);
28661
+ if (dataDir !== undefined)
28662
+ env.MAILCRAWL_DATA_DIR = dataDir;
28663
+ return spawnMailcrawl(this.options.binaryPath ?? DEFAULT_BINARY4, args, env, this.options, signal, timeoutMs);
28475
28664
  }
28476
- async searchHybrid(query, topK) {
28477
- if (topK <= 0)
28478
- return [];
28479
- const pool = topK * POOL_FACTOR;
28480
- const lexical = this.searchLexical(query, pool);
28481
- const semantic = await this.searchSemantic(query, pool);
28482
- if (semantic.length === 0)
28483
- return lexical.slice(0, topK);
28484
- const byKey = new Map;
28485
- for (const hit of [...semantic, ...lexical])
28486
- byKey.set(hit.key, hit);
28487
- const fused = [];
28488
- for (const entry of reciprocalRankFusion([lexical.map((hit) => hit.key), semantic.map((hit) => hit.key)])) {
28489
- const hit = byKey.get(entry.key);
28490
- if (hit !== undefined)
28491
- fused.push({ ...hit, score: entry.score });
28492
- if (fused.length >= topK)
28493
- break;
28665
+ async index(signal) {
28666
+ const result = await this.run(["index", "--json"], signal, this.indexTimeoutMs());
28667
+ if (!result.ok)
28668
+ return failure3(result);
28669
+ const parsed = parseObject2(result.stdout);
28670
+ if (parsed === undefined || number(parsed, "embedded") === undefined && number(parsed, "reused") === undefined && string(parsed, "generation") === undefined) {
28671
+ return failure3({ ...result, ok: false, reason: "invalid-output" });
28494
28672
  }
28495
- return fused;
28673
+ return ok4({
28674
+ embedded: number(parsed, "embedded"),
28675
+ reused: number(parsed, "reused"),
28676
+ generation: string(parsed, "generation"),
28677
+ archiveRevision: string(parsed, "archiveRevision"),
28678
+ rebuilt: typeof parsed.rebuilt === "boolean" ? parsed.rebuilt : undefined,
28679
+ embedder: string(parsed, "embedder")
28680
+ }, result);
28496
28681
  }
28497
- async sync() {
28498
- const { embedder } = this.options;
28499
- if (embedder === undefined)
28500
- return { ok: false, message: "no embedder configured" };
28501
- if (!this.attachCrawler())
28502
- return { ok: false, message: "crawler archive not available yet" };
28503
- let identity;
28504
- try {
28505
- identity = await embedder.identity();
28506
- } catch (error) {
28507
- return { ok: false, message: errorMessage2(error) };
28508
- }
28509
- this.adoptIdentity(identity);
28510
- const pruned = this.prune();
28511
- const pending = this.pendingKeys();
28512
- const batchSize = Math.max(1, this.options.batchSize ?? DEFAULT_BATCH_SIZE);
28513
- const budget = Math.max(1, this.options.maxEmbedPerSync ?? DEFAULT_MAX_EMBED_PER_SYNC);
28514
- const target = Math.min(pending.length, budget);
28515
- const embed = async (rows) => {
28516
- const vectors = await embedder.embed(rows.map((row) => row.input));
28517
- if (vectors.length !== rows.length) {
28518
- throw new Error(`embedder returned ${vectors.length} vectors for ${rows.length} messages`);
28519
- }
28520
- const wrong = vectors.find((vector) => vector.length !== identity.dimension);
28521
- if (wrong !== undefined) {
28522
- throw new Error(`embedder returned a ${wrong.length}-dimension vector, expected ${identity.dimension}`);
28523
- }
28524
- return vectors;
28682
+ }
28683
+ function spawnMailcrawl(binary, args, env, options, signal, timeoutMs) {
28684
+ return new Promise((resolveResult) => {
28685
+ const portable = portableSpawnCommand(binary, args);
28686
+ const child = spawn15(portable.command, portable.args, {
28687
+ env,
28688
+ detached: process.platform !== "win32",
28689
+ stdio: ["ignore", "pipe", "pipe"]
28690
+ });
28691
+ let stdout = { text: "", bytes: 0, exceeded: false }, stderr = { text: "", bytes: 0, exceeded: false }, settled = false, reason;
28692
+ const max = options.maxBufferBytes ?? DEFAULT_MAX_BUFFER_BYTES4;
28693
+ const timer = setTimeout(() => {
28694
+ reason = "timeout";
28695
+ terminate4(child);
28696
+ }, timeoutMs);
28697
+ const abort = () => {
28698
+ reason = "aborted";
28699
+ terminate4(child);
28525
28700
  };
28526
- let embedded = 0;
28527
- let skipped = 0;
28528
- for (let offset = 0;offset < target; offset += batchSize) {
28529
- const rows = [...this.hydrate(pending.slice(offset, Math.min(offset + batchSize, target))).values()];
28530
- if (rows.length === 0)
28531
- continue;
28532
- try {
28533
- this.store(rows, await embed(rows));
28534
- embedded += rows.length;
28535
- } catch (batchError) {
28536
- const rejected = [];
28537
- let lastError = batchError;
28538
- for (const row of rows) {
28539
- try {
28540
- this.store([row], await embed([row]));
28541
- embedded += 1;
28542
- } catch (error) {
28543
- rejected.push(row);
28544
- lastError = error;
28545
- }
28546
- }
28547
- if (rejected.length === 0)
28548
- continue;
28549
- if (embedded === 0)
28550
- return { ok: false, message: errorMessage2(lastError) };
28551
- this.store(rejected, undefined);
28552
- skipped += rejected.length;
28701
+ if (signal?.aborted)
28702
+ abort();
28703
+ signal?.addEventListener("abort", abort, { once: true });
28704
+ child.stdout.setEncoding("utf8");
28705
+ child.stderr.setEncoding("utf8");
28706
+ child.stdout.on("data", (chunk) => {
28707
+ stdout = appendOutput(stdout, chunk, max);
28708
+ if (stdout.exceeded && reason === undefined) {
28709
+ reason = "stdout-too-large";
28710
+ terminate4(child);
28553
28711
  }
28554
- }
28555
- return { ok: true, embedded, skipped, pruned, remaining: pending.length - embedded - skipped };
28556
- }
28557
- adoptIdentity(identity) {
28558
- const serialized = JSON.stringify(identity);
28559
- const current = this.db.prepare("select v from sidecar_meta where k = ?").get(IDENTITY_KEY);
28560
- if (current?.v === serialized)
28561
- return;
28562
- this.transaction(() => {
28563
- this.db.exec("delete from sidecar_vec");
28564
- this.db.prepare("insert or replace into sidecar_meta (k, v) values (?, ?)").run(IDENTITY_KEY, serialized);
28565
28712
  });
28566
- this.lastSemantic = undefined;
28567
- }
28568
- prune() {
28569
- const { descriptor } = this.options;
28570
- const result = this.db.prepare(`delete from sidecar_vec where message_key not in (
28571
- select ${descriptor.keyExpression} from ${descriptor.fromClause} where ${this.where}
28572
- )`).run(...this.whereParams());
28573
- if (Number(result.changes) > 0)
28574
- this.lastSemantic = undefined;
28575
- return Number(result.changes);
28576
- }
28577
- pendingKeys() {
28578
- const { descriptor } = this.options;
28579
- const rows = this.db.prepare(`select ${this.select}, v.hash as stored_hash
28580
- from ${descriptor.fromClause}
28581
- left join sidecar_vec v on v.message_key = ${descriptor.keyExpression}
28582
- where ${this.where}`).iterate(...this.whereParams());
28583
- const pending = [];
28584
- for (const row of rows) {
28585
- if (row.stored_hash !== this.toRow(row).hash)
28586
- pending.push(String(row.k));
28587
- }
28588
- return pending;
28589
- }
28590
- hydrate(keys) {
28591
- const { descriptor } = this.options;
28592
- const out = new Map;
28593
- if (keys.length === 0)
28594
- return out;
28595
- const rows = this.db.prepare(`select ${this.select} from ${descriptor.fromClause}
28596
- where ${descriptor.keyExpression} in (${keys.map(() => "?").join(", ")}) and ${this.where}`).all(...keys, ...this.whereParams());
28597
- const byKey = new Map(rows.map((row) => [String(row.k), this.toRow(row)]));
28598
- for (const key of keys) {
28599
- const row = byKey.get(key);
28600
- if (row !== undefined)
28601
- out.set(key, row);
28602
- }
28603
- return out;
28604
- }
28605
- store(rows, vectors) {
28606
- const upsert = this.db.prepare("insert or replace into sidecar_vec (message_key, hash, vec) values (?, ?, ?)");
28607
- this.transaction(() => {
28608
- for (const [index, row] of rows.entries()) {
28609
- upsert.run(row.key, row.hash, encodeVector(vectors?.[index] ?? []));
28713
+ child.stderr.on("data", (chunk) => {
28714
+ stderr = appendOutput(stderr, chunk, max);
28715
+ if (stderr.exceeded && reason === undefined) {
28716
+ reason = "stderr-too-large";
28717
+ terminate4(child);
28610
28718
  }
28611
28719
  });
28612
- this.lastSemantic = undefined;
28720
+ child.on("error", (error) => {
28721
+ if (settled)
28722
+ return;
28723
+ settled = true;
28724
+ clearTimeout(timer);
28725
+ signal?.removeEventListener("abort", abort);
28726
+ resolveResult({
28727
+ ok: false,
28728
+ reason: error.code === "ENOENT" ? "binary-missing" : "spawn-error",
28729
+ stdout: stdout.text,
28730
+ stderr: "",
28731
+ code: null
28732
+ });
28733
+ });
28734
+ child.on("close", (code) => {
28735
+ if (settled)
28736
+ return;
28737
+ settled = true;
28738
+ clearTimeout(timer);
28739
+ signal?.removeEventListener("abort", abort);
28740
+ resolveResult({
28741
+ ok: code === 0 && reason === undefined,
28742
+ ...reason === undefined ? code === 0 ? {} : { reason: "nonzero-exit" } : { reason },
28743
+ stdout: stdout.text,
28744
+ stderr: stderr.text,
28745
+ code
28746
+ });
28747
+ });
28748
+ });
28749
+ }
28750
+ function terminate4(child) {
28751
+ if (child.killed)
28752
+ return;
28753
+ if (process.platform !== "win32" && child.pid !== undefined) {
28754
+ try {
28755
+ process.kill(-child.pid, "SIGTERM");
28756
+ return;
28757
+ } catch {}
28613
28758
  }
28614
- *storedVectors() {
28615
- const rows = this.db.prepare("select message_key, vec from sidecar_vec where length(vec) > 0").iterate();
28616
- for (const row of rows) {
28617
- const vector = row.vec instanceof Uint8Array ? decodeVector(row.vec) : undefined;
28618
- if (vector !== undefined)
28619
- yield { key: String(row.message_key), vector };
28759
+ child.kill("SIGTERM");
28760
+ }
28761
+ function parseJson3(text) {
28762
+ try {
28763
+ return JSON.parse(text.trim());
28764
+ } catch {
28765
+ return;
28766
+ }
28767
+ }
28768
+ function parseObject2(text) {
28769
+ const value = parseJson3(text);
28770
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : undefined;
28771
+ }
28772
+ function objectArray(value, keys) {
28773
+ if (value === null || typeof value !== "object" || Array.isArray(value))
28774
+ return;
28775
+ for (const key of keys)
28776
+ if (Array.isArray(value[key]))
28777
+ return value[key];
28778
+ return;
28779
+ }
28780
+ function hasAnyNumber(value, keys) {
28781
+ return keys.some((key) => number(value, key) !== undefined);
28782
+ }
28783
+ function normalizeHit2(value, mode, index) {
28784
+ if (value === null || typeof value !== "object" || Array.isArray(value))
28785
+ return;
28786
+ const row = value;
28787
+ const text = (key) => typeof row[key] === "string" ? row[key] : "";
28788
+ const chunkId = text("chunkId") || text("chunk_id");
28789
+ const messageId = text("messageId") || text("message_id");
28790
+ const accountId = text("accountId") || text("account_id");
28791
+ const mailbox = text("mailbox");
28792
+ const snippet = text("snippet") || text("content") || text("text");
28793
+ if (!isSafeOpaqueId(chunkId) || !isSafeOpaqueId(messageId) || !snippet)
28794
+ return;
28795
+ const to = Array.isArray(row.to) ? row.to.filter((item) => typeof item === "string") : [];
28796
+ return {
28797
+ chunkId,
28798
+ messageId,
28799
+ threadId: text("threadId") || text("thread_id"),
28800
+ accountId,
28801
+ mailbox,
28802
+ subject: text("subject"),
28803
+ from: text("from"),
28804
+ to,
28805
+ date: text("date"),
28806
+ snippet,
28807
+ score: typeof row.score === "number" ? row.score : 1 / (index + 1),
28808
+ mode
28809
+ };
28810
+ }
28811
+ function number(value, key) {
28812
+ return typeof value[key] === "number" && Number.isFinite(value[key]) ? value[key] : undefined;
28813
+ }
28814
+ function string(value, key) {
28815
+ return typeof value[key] === "string" ? value[key] : undefined;
28816
+ }
28817
+ function ok4(data, result) {
28818
+ return { ok: true, data, stdout: result.stdout, stderr: result.stderr, code: result.code ?? 0 };
28819
+ }
28820
+ function failure3(result) {
28821
+ return {
28822
+ ok: false,
28823
+ reason: result.reason ?? "nonzero-exit",
28824
+ stdout: result.stdout,
28825
+ stderr: boundDiagnosticText6(result.stderr),
28826
+ code: result.code
28827
+ };
28828
+ }
28829
+ function boundDiagnosticText6(value) {
28830
+ if (value.length === 0)
28831
+ return "";
28832
+ return value.trim().slice(0, 4000);
28833
+ }
28834
+ function appendOutput(current, chunk, maxBytes) {
28835
+ if (current.exceeded)
28836
+ return current;
28837
+ const bytes = Buffer.from(chunk, "utf8");
28838
+ const remaining = Math.max(0, maxBytes - current.bytes);
28839
+ const prefix = truncateUtf8(chunk, remaining);
28840
+ const total = current.bytes + bytes.byteLength;
28841
+ return {
28842
+ text: current.text + prefix,
28843
+ bytes: Math.min(total, maxBytes),
28844
+ exceeded: total > maxBytes
28845
+ };
28846
+ }
28847
+ function isSafeOpaqueId(value) {
28848
+ return value.length > 0 && value !== "." && value !== ".." && !/[\\/#\u0000-\u001f\u007f]/u.test(value);
28849
+ }
28850
+ function findRemoteEmbeddingViolation(env) {
28851
+ for (const [key, value] of Object.entries(env)) {
28852
+ if (value === undefined)
28853
+ continue;
28854
+ const lower = key.toLowerCase();
28855
+ if (REMOTE_EMBEDDING_ENV_KEYS.has(lower))
28856
+ return key;
28857
+ if (lower === "mailcrawl_embedder" || lower === "mailcrawl_embed_url") {
28858
+ if (isRemoteHttpEndpoint(value))
28859
+ return key;
28620
28860
  }
28621
28861
  }
28622
- transaction(work) {
28623
- this.db.exec("begin immediate");
28862
+ return;
28863
+ }
28864
+ function isRemoteHttpEndpoint(value) {
28865
+ const trimmed = value.trim();
28866
+ if (!/^https?:\/\//iu.test(trimmed))
28867
+ return false;
28868
+ try {
28869
+ return !LOOPBACK_HOSTS2.has(new URL(trimmed).hostname);
28870
+ } catch {
28871
+ return true;
28872
+ }
28873
+ }
28874
+ function truncateUtf8(value, maxBytes) {
28875
+ if (Buffer.byteLength(value) <= maxBytes)
28876
+ return value;
28877
+ const decoder = new TextDecoder("utf-8", { fatal: true });
28878
+ const bytes = Buffer.from(value, "utf8");
28879
+ for (let end = Math.min(maxBytes, bytes.length);end >= 0; end -= 1) {
28624
28880
  try {
28625
- work();
28626
- this.db.exec("commit");
28627
- } catch (error) {
28628
- this.db.exec("rollback");
28629
- throw error;
28630
- }
28881
+ return decoder.decode(bytes.subarray(0, end));
28882
+ } catch {}
28631
28883
  }
28632
- toRow(row) {
28633
- const metadata = {};
28634
- for (const [index, column] of this.options.descriptor.metadata.entries()) {
28635
- const value = row[`m${index}`];
28636
- if (typeof value === "string" && value.length > 0 || typeof value === "number")
28637
- metadata[column.key] = String(value);
28638
- }
28639
- const title = typeof row.title === "string" && row.title.length > 0 ? row.title : undefined;
28640
- const epoch = typeof row.epoch === "number" && Number.isFinite(row.epoch) ? row.epoch : undefined;
28641
- const content = String(row.content ?? "");
28642
- const input = `${title ?? ""}
28643
- ${content}`.slice(0, MAX_EMBED_INPUT_CHARS);
28644
- return {
28645
- key: String(row.k),
28646
- content,
28647
- title,
28648
- epoch,
28649
- metadata,
28650
- input,
28651
- hash: createHash14("sha1").update(input).digest("hex")
28652
- };
28884
+ return "";
28885
+ }
28886
+ 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;
28887
+ var init_client9 = __esm(() => {
28888
+ init_portable_spawn();
28889
+ init_config3();
28890
+ LOOPBACK_HOSTS2 = new Set(["127.0.0.1", "localhost", "[::1]", "::1"]);
28891
+ SAFE_ENV_KEYS2 = new Set(["HOME", "LANG", "LC_ALL", "PATH", "TMPDIR", "TMP", "TEMP", "NO_COLOR"]);
28892
+ SAFE_MAILCRAWL_ENV_KEYS = new Set([
28893
+ "MAILCRAWL_DATA_DIR",
28894
+ "MAILCRAWL_EMBEDDER",
28895
+ "MAILCRAWL_EMBEDDER_PROVIDER",
28896
+ "MAILCRAWL_EMBED_URL",
28897
+ "MAILCRAWL_EMBED_MODEL",
28898
+ "MAILCRAWL_EMBED_DIM",
28899
+ "MAILCRAWL_EMBED_TIMEOUT",
28900
+ "MAILCRAWL_EMBED_BATCH_SIZE",
28901
+ "MAILCRAWL_QUERY_PREFIX",
28902
+ "MAILCRAWL_PASSAGE_PREFIX",
28903
+ "MAILCRAWL_NATIVE_MODEL",
28904
+ "MAILCRAWL_NATIVE_DEVICE",
28905
+ "MAILCRAWL_NATIVE_DTYPE",
28906
+ "MAILCRAWL_KIWI_MODEL",
28907
+ "MAILCRAWL_JA_HELPER",
28908
+ "MAILCRAWL_ZH_HELPER"
28909
+ ]);
28910
+ REMOTE_EMBEDDING_ENV_KEYS = new Set([
28911
+ "mailcrawl_embedder_base_url",
28912
+ "mailcrawl_embedding_base_url",
28913
+ "mailcrawl_allow_remote_embeddings"
28914
+ ]);
28915
+ });
28916
+
28917
+ // src/datasource/skills/mailcrawl/methods.ts
28918
+ class MailcrawlMethod {
28919
+ client;
28920
+ mode;
28921
+ instanceId;
28922
+ tags;
28923
+ account;
28924
+ mailbox;
28925
+ constructor(client, mode, instanceId, tags, account, mailbox) {
28926
+ this.client = client;
28927
+ this.mode = mode;
28928
+ this.instanceId = instanceId;
28929
+ this.tags = tags;
28930
+ this.account = account;
28931
+ this.mailbox = mailbox;
28653
28932
  }
28654
- toHit(row, score) {
28933
+ describe() {
28655
28934
  return {
28656
- key: row.key,
28657
- content: row.content,
28658
- score,
28659
- ...row.title !== undefined ? { title: row.title } : {},
28660
- ...row.epoch !== undefined ? { publishedAt: Math.round(row.epoch * 1000) } : {},
28661
- metadata: row.metadata
28935
+ name: `mailcrawl-${this.mode}`,
28936
+ type: this.mode === "bm25" ? "bm25" : this.mode === "semantic" ? "vector" : "hybrid",
28937
+ description: `${this.mode} retrieval over archived email via the external mailcrawl CLI`,
28938
+ status: "active",
28939
+ capabilities: ["email", this.mode, "scoped", "external-cli", "path-opaque-sources"],
28940
+ datasourceId: "mailcrawl",
28941
+ tags: this.tags
28662
28942
  };
28663
28943
  }
28944
+ async retrieve(query, options) {
28945
+ if (!query.trim())
28946
+ return [];
28947
+ const result = await this.client.search(this.mode, query, {
28948
+ topK: options.topK,
28949
+ signal: options.signal
28950
+ });
28951
+ if (!result.ok)
28952
+ throw datasourceCliError("mailcrawl", `search --mode ${this.mode}`, result);
28953
+ return result.hits.filter((hit) => {
28954
+ if (this.account !== undefined && hit.accountId !== this.account)
28955
+ return false;
28956
+ if (this.mailbox !== undefined && hit.mailbox !== this.mailbox)
28957
+ return false;
28958
+ const source = datasourceSourcePath("mailcrawl", this.instanceId, hit.chunkId);
28959
+ return matchesDatasourceScope(source, options.scope);
28960
+ }).map((hit) => {
28961
+ const source = datasourceSourcePath("mailcrawl", this.instanceId, hit.chunkId);
28962
+ return {
28963
+ id: `mailcrawl:${this.instanceId}:${hit.chunkId}`,
28964
+ content: hit.snippet,
28965
+ source,
28966
+ score: hit.score,
28967
+ metadata: {
28968
+ method: `mailcrawl-${this.mode}`,
28969
+ datasourceId: "mailcrawl",
28970
+ instanceId: this.instanceId,
28971
+ mode: this.mode,
28972
+ chunkId: hit.chunkId,
28973
+ messageId: hit.messageId,
28974
+ threadId: hit.threadId,
28975
+ accountId: hit.accountId,
28976
+ mailbox: hit.mailbox,
28977
+ subject: hit.subject,
28978
+ from: hit.from,
28979
+ to: hit.to,
28980
+ date: hit.date
28981
+ }
28982
+ };
28983
+ }).slice(0, options.topK ?? 20);
28984
+ }
28664
28985
  }
28665
- function toMatchQuery(text) {
28666
- const terms = [...new Set(text.match(/[\p{L}\p{N}_]+/gu) ?? [])];
28667
- return terms.length === 0 ? undefined : terms.map((term) => `"${term}"`).join(" OR ");
28668
- }
28669
- function errorMessage2(error) {
28670
- return error instanceof Error ? error.message : String(error);
28671
- }
28672
- var DEFAULT_BATCH_SIZE = 256, DEFAULT_MAX_EMBED_PER_SYNC = 2048, MAX_EMBED_INPUT_CHARS = 8192, BUSY_TIMEOUT_MS = 5000, POOL_FACTOR = 3, SEMANTIC_REUSE_MS = 1e4, IDENTITY_KEY = "identity";
28673
- var init_sidecar = () => {};
28674
-
28675
- // src/datasource/sidecar/methods.ts
28676
- function sidecarMethodName(datasourceId, mode) {
28677
- return mode === "bm25" ? `${datasourceId}-sidecar-bm25` : `${datasourceId}-${mode}`;
28678
- }
28986
+ var modes, MAILCRAWL_MODES;
28987
+ var init_methods5 = __esm(() => {
28988
+ init_errors4();
28989
+ init_scope2();
28990
+ modes = ["bm25", "semantic", "hybrid"];
28991
+ MAILCRAWL_MODES = modes;
28992
+ });
28679
28993
 
28680
- class SidecarRetrievalMethod {
28681
- options;
28682
- constructor(options) {
28683
- this.options = options;
28994
+ // src/datasource/skills/mailcrawl/skill.ts
28995
+ class MailcrawlSkill {
28996
+ client;
28997
+ instanceId;
28998
+ instances;
28999
+ interval;
29000
+ tags;
29001
+ account;
29002
+ mailbox;
29003
+ lastIndexedAt;
29004
+ constructor(options = {}) {
29005
+ const connectorOptions = options.connectorOptions ?? options;
29006
+ this.instanceId = options.instanceId ?? "default";
29007
+ this.instances = options.instances?.length ? options.instances : [this.instanceId];
29008
+ validateMailcrawlInstanceId(this.instanceId);
29009
+ for (const instance of this.instances)
29010
+ validateMailcrawlInstanceId(instance);
29011
+ this.interval = options.pollingIntervalMs ?? 15 * 60 * 1000;
29012
+ this.tags = options.tags ?? TAGS;
29013
+ this.account = connectorOptions.account;
29014
+ this.mailbox = connectorOptions.mailbox;
29015
+ this.client = options.client ?? new MailcrawlClient({ ...connectorOptions, instanceId: this.instanceId });
28684
29016
  }
28685
29017
  describe() {
28686
- const { datasourceId, skillType, mode, tags } = this.options;
28687
- const { type, text, capabilities } = DESCRIPTIONS[mode];
28688
29018
  return {
28689
- name: sidecarMethodName(datasourceId, mode),
28690
- type,
28691
- description: `${text} over ${skillType} via a read-only sidecar index`,
29019
+ name: "mailcrawl",
29020
+ id: "mailcrawl",
29021
+ type: "mailcrawl-archive",
29022
+ description: "Local email archive datasource via the external mailcrawl CLI",
29023
+ capabilities: ["email", "external-cli", "polling", "incremental", "bm25", "semantic", "hybrid"],
29024
+ tags: this.tags,
28692
29025
  status: "active",
28693
- capabilities: [...capabilities, "scoped", "path-opaque-sources"],
28694
- datasourceId,
28695
- tags
29026
+ requiresExternalCli: true,
29027
+ datasourceId: "mailcrawl",
29028
+ instanceId: this.instanceId,
29029
+ instances: this.instances
28696
29030
  };
28697
29031
  }
28698
- async retrieve(query, options) {
28699
- const trimmed = query.trim();
28700
- if (trimmed.length === 0)
28701
- return [];
28702
- const topK = options.topK ?? DEFAULT_TOP_K5;
28703
- const { sidecar, mode } = this.options;
28704
- const hits = mode === "bm25" ? sidecar.searchLexical(trimmed, topK) : mode === "semantic" ? await sidecar.searchSemantic(trimmed, topK) : await sidecar.searchHybrid(trimmed, topK);
28705
- const results = [];
28706
- for (const hit of hits) {
28707
- const result = this.toResult(hit, options);
28708
- if (result !== undefined)
28709
- results.push(result);
29032
+ polling() {
29033
+ return {
29034
+ mode: this.interval > 0 ? "poll" : "none",
29035
+ ...this.interval > 0 ? { intervalMs: this.interval } : {},
29036
+ lastIndexedAt: this.lastIndexedAt
29037
+ };
29038
+ }
29039
+ async index(options) {
29040
+ try {
29041
+ const result = await this.client.sync(options?.signal);
29042
+ if (!result.ok) {
29043
+ 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);
29044
+ }
29045
+ const indexed = await this.client.index(options?.signal);
29046
+ const diagnostics = [];
29047
+ if (!indexed.ok) {
29048
+ diagnostics.push({
29049
+ code: indexed.reason === "remote-embedding-rejected" ? "datasource-embedding-egress-rejected" : "datasource-index-failed",
29050
+ severity: "warning",
29051
+ message: `mailcrawl semantic index unavailable (${indexed.reason}); BM25 remains available`,
29052
+ instanceId: this.instanceId,
29053
+ source: "mailcrawl"
29054
+ });
29055
+ }
29056
+ this.lastIndexedAt = Date.now();
29057
+ return {
29058
+ ok: true,
29059
+ instanceId: this.instanceId,
29060
+ skill: "mailcrawl",
29061
+ chunkCount: result.data.chunksAdded ?? result.data.messages,
29062
+ indexedAt: this.lastIndexedAt,
29063
+ diagnostics
29064
+ };
29065
+ } catch {
29066
+ return this.fail("datasource-unavailable", "mailcrawl sync failed unexpectedly");
28710
29067
  }
28711
- return results;
28712
29068
  }
28713
- toResult(hit, options) {
28714
- const { descriptor, datasourceId, instanceId, mode } = this.options;
28715
- const chunkId = descriptor.chunkId(hit.key, hit.metadata);
28716
- const source = datasourceSourcePath(datasourceId, instanceId, chunkId);
28717
- if (!matchesDatasourceScope(source, options.scope))
28718
- return;
28719
- const method = sidecarMethodName(datasourceId, mode);
29069
+ retrievalMethods() {
29070
+ return [
29071
+ new MailcrawlMethod(this.client, "bm25", this.instanceId, this.tags, this.account, this.mailbox),
29072
+ new MailcrawlMethod(this.client, "semantic", this.instanceId, this.tags, this.account, this.mailbox),
29073
+ new MailcrawlMethod(this.client, "hybrid", this.instanceId, this.tags, this.account, this.mailbox)
29074
+ ];
29075
+ }
29076
+ describeSources() {
29077
+ return this.instances.map((instanceId) => ({
29078
+ source: datasourceSourcePath("mailcrawl", instanceId),
29079
+ datasourceId: "mailcrawl",
29080
+ skill: "mailcrawl",
29081
+ instanceId,
29082
+ contentType: "email",
29083
+ metadata: { datasourceId: "mailcrawl", instanceId, tags: this.tags }
29084
+ }));
29085
+ }
29086
+ skillManifest() {
29087
+ const scopes = this.instances.map((id) => `- \`${datasourceSourcePath("mailcrawl", id)}\``).join(`
29088
+ `);
28720
29089
  return {
28721
- id: `${datasourceId}:${instanceId}:${chunkId}`,
28722
- content: hit.content,
28723
- source,
28724
- score: hit.score,
28725
- metadata: {
28726
- ...hit.metadata,
28727
- method,
28728
- datasourceId,
28729
- instanceId,
28730
- backend: "sidecar",
28731
- ...hit.title !== undefined ? { title: hit.title } : {},
28732
- hierarchy: descriptor.hierarchy(hit.metadata).join("/"),
28733
- ...hit.publishedAt !== undefined ? { publishedAt: hit.publishedAt } : {}
28734
- }
29090
+ name: "datasource-mailcrawl",
29091
+ description: "Search local email synchronized and indexed by mailcrawl.",
29092
+ content: `# mailcrawl email datasource
29093
+
29094
+ Use this skill for authorized local email retrieval. AutoRAG invokes the external \`mailcrawl\` CLI and never opens its SQLite archive directly.
29095
+
29096
+ ## Indexing
29097
+ Indexing is server-managed and incremental; do not trigger sync yourself.
29098
+
29099
+ ## Search
29100
+ Call the dedicated \`${datasourceSearchToolName("mailcrawl")}\` tool with a natural-language query. Available scopes:
29101
+ ${scopes}
29102
+
29103
+ The selectable retrieval modes are BM25, semantic, and hybrid. Scope arguments can only narrow trusted access. Source identifiers remain opaque and hierarchical.
29104
+
29105
+ ## Native CLI
29106
+ The external \`mailcrawl\` CLI owns the archive and index. When the dedicated tool cannot express what you need, call mailcrawl directly through \`bash\`:
29107
+ - \`mailcrawl search --mode hybrid --limit 20 --json "<query>"\` (\`bm25\` and \`semantic\` modes also available; \`--account\` / \`--mailbox\` narrow the archive)
29108
+ Never pass datasource virtual paths (\`/mailcrawl/...\`) to bash; they are not OS paths.`
29109
+ };
29110
+ }
29111
+ fail(code, message) {
29112
+ return {
29113
+ ok: false,
29114
+ instanceId: this.instanceId,
29115
+ skill: "mailcrawl",
29116
+ indexedAt: Date.now(),
29117
+ diagnostics: [
29118
+ {
29119
+ code,
29120
+ severity: code === "datasource-unavailable" ? "warning" : "error",
29121
+ message: `mailcrawl ${message}`,
29122
+ instanceId: this.instanceId,
29123
+ source: "mailcrawl"
29124
+ }
29125
+ ],
29126
+ error: message,
29127
+ code,
29128
+ message
28735
29129
  };
28736
29130
  }
28737
29131
  }
28738
- var DEFAULT_TOP_K5 = 20, DESCRIPTIONS;
28739
- var init_methods5 = __esm(() => {
29132
+ var TAGS;
29133
+ var init_skill9 = __esm(() => {
28740
29134
  init_scope2();
28741
- DESCRIPTIONS = {
28742
- bm25: { type: "bm25", text: "BM25-ranked FTS5 retrieval", capabilities: ["lexical", "fts5", "bm25"] },
28743
- semantic: { type: "vector", text: "Semantic (embedding) retrieval", capabilities: ["semantic"] },
28744
- hybrid: {
28745
- type: "hybrid",
28746
- text: "Hybrid BM25 + semantic retrieval (reciprocal rank fusion)",
28747
- capabilities: ["lexical", "semantic", "hybrid"]
28748
- }
28749
- };
29135
+ init_client9();
29136
+ init_config3();
29137
+ init_methods5();
29138
+ TAGS = ["mailcrawl", "email", "pii"];
29139
+ });
29140
+
29141
+ // src/datasource/skills/mailcrawl/index.ts
29142
+ var init_mailcrawl = __esm(() => {
29143
+ init_client9();
29144
+ init_methods5();
29145
+ init_skill9();
28750
29146
  });
28751
29147
 
28752
29148
  // src/datasource/crawler-skill.ts
@@ -29029,7 +29425,7 @@ var init_crawler_skill = __esm(() => {
29029
29425
  init_errors4();
29030
29426
  init_scope2();
29031
29427
  init_sidecar();
29032
- init_methods5();
29428
+ init_methods4();
29033
29429
  DEFAULT_POLLING_INTERVAL_MS4 = 15 * 60 * 1000;
29034
29430
  });
29035
29431
 
@@ -30656,7 +31052,7 @@ var init_wacrawl = __esm(() => {
30656
31052
  });
30657
31053
 
30658
31054
  // src/datasource/skills/factory.ts
30659
- import { homedir as homedir4 } from "node:os";
31055
+ import { homedir as homedir5 } from "node:os";
30660
31056
  import { join as join39 } from "node:path";
30661
31057
  function chatCrawlerArgs(descriptor, defaultCrawlerDb, config, workspaceRoot, registrationName) {
30662
31058
  const sidecar = resolveSidecarOptions({
@@ -30760,9 +31156,9 @@ var init_factory = __esm(() => {
30760
31156
  init_telecrawl();
30761
31157
  init_wacrawl();
30762
31158
  BUILDERS = {
30763
- telegram: (config, workspaceRoot, registrationName) => new TelecrawlSkill(chatCrawlerArgs(CRAWLER_DESCRIPTORS.telegram, join39(homedir4(), ".telecrawl", "telecrawl.db"), config, workspaceRoot, registrationName)),
30764
- whatsapp: (config, workspaceRoot, registrationName) => new WacrawlSkill(chatCrawlerArgs(CRAWLER_DESCRIPTORS.whatsapp, join39(homedir4(), ".wacrawl", "wacrawl.db"), config, workspaceRoot, registrationName)),
30765
- slack: (config, workspaceRoot, registrationName) => new SlackSkill(chatCrawlerArgs(CRAWLER_DESCRIPTORS.slack, join39(homedir4(), ".slacrawl", "slacrawl.db"), config, workspaceRoot, registrationName)),
31159
+ telegram: (config, workspaceRoot, registrationName) => new TelecrawlSkill(chatCrawlerArgs(CRAWLER_DESCRIPTORS.telegram, join39(homedir5(), ".telecrawl", "telecrawl.db"), config, workspaceRoot, registrationName)),
31160
+ whatsapp: (config, workspaceRoot, registrationName) => new WacrawlSkill(chatCrawlerArgs(CRAWLER_DESCRIPTORS.whatsapp, join39(homedir5(), ".wacrawl", "wacrawl.db"), config, workspaceRoot, registrationName)),
31161
+ slack: (config, workspaceRoot, registrationName) => new SlackSkill(chatCrawlerArgs(CRAWLER_DESCRIPTORS.slack, join39(homedir5(), ".slacrawl", "slacrawl.db"), config, workspaceRoot, registrationName)),
30766
31162
  discord: (config, workspaceRoot, registrationName, embeddingRuntime) => {
30767
31163
  const connector = config.connector ?? {};
30768
31164
  const clientOptions = {
@@ -30818,24 +31214,42 @@ var init_factory = __esm(() => {
30818
31214
  ...config.channels?.ids !== undefined ? { chatIds: config.channels.ids } : {},
30819
31215
  ...larkConnectorOptions(config.connector)
30820
31216
  }),
30821
- kakao: (config, _workspaceRoot, _registrationName) => new LazykatokSkill({
30822
- client: new LazykatokClient({
30823
- ...config.connector
30824
- }),
30825
- ...config.instanceId !== undefined ? { instanceId: config.instanceId } : {},
30826
- ...config.pollingIntervalMs !== undefined ? { pollingIntervalMs: config.pollingIntervalMs } : {},
30827
- ...config.tags !== undefined ? { tags: config.tags } : {}
30828
- }),
31217
+ kakao: (config, workspaceRoot, registrationName) => {
31218
+ const connector = config.connector ?? {};
31219
+ const dataDir = typeof connector.workspacePath === "string" && connector.workspacePath.length > 0 ? connector.workspacePath : join39(homedir5(), "Library", "Application Support", "katok");
31220
+ const sidecar = resolveSidecarOptions({
31221
+ descriptor: CRAWLER_DESCRIPTORS.kakao,
31222
+ connector: config.connector,
31223
+ workspaceRoot,
31224
+ registrationName,
31225
+ instanceId: config.instanceId ?? "default",
31226
+ defaultCrawlerDb: join39(dataDir, "archive.sqlite3")
31227
+ });
31228
+ return new LazykatokSkill({
31229
+ client: new LazykatokClient(connector),
31230
+ ...config.instanceId !== undefined ? { instanceId: config.instanceId } : {},
31231
+ ...config.pollingIntervalMs !== undefined ? { pollingIntervalMs: config.pollingIntervalMs } : {},
31232
+ ...config.tags !== undefined ? { tags: config.tags } : {},
31233
+ channelIds: config.channels?.ids,
31234
+ channelNames: config.channels?.names,
31235
+ ...sidecar === undefined ? {} : { sidecar }
31236
+ });
31237
+ },
30829
31238
  github: (config, workspaceRoot, registrationName) => new GitHubSkill({
30830
31239
  ...common(config, workspaceRoot),
30831
31240
  skillName: registrationName,
30832
31241
  connectorOptions: config.connector
30833
31242
  }),
30834
- "github-gist": (config, workspaceRoot, registrationName) => new GitHubGistSkill({
30835
- ...common(config, workspaceRoot),
30836
- skillName: registrationName,
30837
- connectorOptions: config.connector
30838
- }),
31243
+ "github-gist": (config, workspaceRoot, registrationName) => {
31244
+ const { semantic, ...connectorOptions } = config.connector ?? {};
31245
+ const semanticOff = semantic === false || typeof semantic === "object" && semantic !== null && semantic.enabled === false;
31246
+ return new GitHubGistSkill({
31247
+ ...common(config, workspaceRoot),
31248
+ skillName: registrationName,
31249
+ connectorOptions,
31250
+ ...semanticOff ? { semantic: { enabled: false } } : {}
31251
+ });
31252
+ },
30839
31253
  "cloud-drive": (config, workspaceRoot, registrationName) => new CloudDriveSkill({
30840
31254
  ...common(config, workspaceRoot),
30841
31255
  skillName: registrationName,
@@ -30881,7 +31295,7 @@ var init_factory = __esm(() => {
30881
31295
 
30882
31296
  // src/cli/config.ts
30883
31297
  import { randomUUID as randomUUID8 } from "node:crypto";
30884
- import { existsSync as existsSync28, mkdirSync as mkdirSync25, readFileSync as readFileSync25, renameSync as renameSync10, unlinkSync as unlinkSync5, writeFileSync as writeFileSync22 } from "node:fs";
31298
+ import { existsSync as existsSync29, mkdirSync as mkdirSync25, readFileSync as readFileSync25, renameSync as renameSync10, unlinkSync as unlinkSync5, writeFileSync as writeFileSync22 } from "node:fs";
30885
31299
  import { basename as basename14, dirname as dirname26, isAbsolute as isAbsolute10, join as join40, resolve as resolve16 } from "node:path";
30886
31300
  import { findEnvKeys, getEnvApiKey } from "@earendil-works/pi-ai/compat";
30887
31301
  import { getAgentDir as getAgentDir4, ModelRuntime as ModelRuntime2, SettingsManager as SettingsManager2 } from "@earendil-works/pi-coding-agent";
@@ -30906,7 +31320,7 @@ function resolveConfigPath(input) {
30906
31320
  function readConfigFile(configPath, explicit) {
30907
31321
  let exists;
30908
31322
  try {
30909
- exists = existsSync28(configPath);
31323
+ exists = existsSync29(configPath);
30910
31324
  } catch {
30911
31325
  exists = false;
30912
31326
  }
@@ -30966,12 +31380,12 @@ function replaceFileAtomically(path, contents, assertCommitAllowed) {
30966
31380
  }
30967
31381
  }
30968
31382
  function migrateLegacyConfig(configPath, legacyPath) {
30969
- if (existsSync28(configPath) || !existsSync28(legacyPath))
31383
+ if (existsSync29(configPath) || !existsSync29(legacyPath))
30970
31384
  return;
30971
31385
  const legacy = readConfigFile(legacyPath, true);
30972
31386
  const legacyBytes = readFileSync25(legacyPath);
30973
31387
  const migrated = normalizeLegacyConfigPaths(legacy ?? {}, dirname26(legacyPath));
30974
- const migratedPathsUnchanged = legacy?.workspacePath === migrated.workspacePath && legacy?.memoryPath === migrated.memoryPath && JSON.stringify(legacy?.searchPaths) === JSON.stringify(migrated.searchPaths);
31388
+ const migratedPathsUnchanged = legacy?.workspacePath === migrated.workspacePath && legacy?.memoryPath === migrated.memoryPath && JSON.stringify(legacy?.searchPaths) === JSON.stringify(migrated.searchPaths) && JSON.stringify(legacy?.excludePaths) === JSON.stringify(migrated.excludePaths);
30975
31389
  const migratedBytes = migratedPathsUnchanged ? legacyBytes : `${JSON.stringify(migrated, null, 2)}
30976
31390
  `;
30977
31391
  mkdirSync25(dirname26(configPath), { recursive: true });
@@ -31009,15 +31423,25 @@ function resolveConfigFileReadOnly(configPath, explicit, legacyPath) {
31009
31423
  function resolveSearchPaths(searchPaths, origin) {
31010
31424
  return searchPaths.map((searchPath) => resolvePersistedPath(searchPath, origin));
31011
31425
  }
31426
+ function resolveExcludePaths(value, origin) {
31427
+ if (value === undefined)
31428
+ return;
31429
+ if (!Array.isArray(value) || value.some((path) => typeof path !== "string" || path.length === 0)) {
31430
+ throw new ConfigError("Config field 'excludePaths' must be a non-empty string array");
31431
+ }
31432
+ return resolveSearchPaths(value, origin);
31433
+ }
31012
31434
  function resolvePersistedPath(path, origin) {
31013
31435
  return isAbsolute10(path) ? path : resolve16(origin, path);
31014
31436
  }
31015
31437
  function normalizeLegacyConfigPaths(partial, origin) {
31016
31438
  const workspacePath = resolvePersistedPath(partial.workspacePath ?? ".", origin);
31439
+ const excludePaths = resolveExcludePaths(partial.excludePaths, workspacePath);
31017
31440
  return {
31018
31441
  ...partial,
31019
31442
  workspacePath,
31020
31443
  ...partial.searchPaths === undefined ? {} : { searchPaths: resolveSearchPaths(partial.searchPaths, workspacePath) },
31444
+ ...excludePaths === undefined ? {} : { excludePaths },
31021
31445
  ...partial.memoryPath === undefined ? {} : { memoryPath: resolvePersistedPath(partial.memoryPath, workspacePath) }
31022
31446
  };
31023
31447
  }
@@ -31472,10 +31896,7 @@ function resolveConfig(input) {
31472
31896
  const configOrigin = dirname26(resolve16(configPath));
31473
31897
  const fileWorkspacePath = typeof file.workspacePath === "string" ? resolvePersistedPath(file.workspacePath, configOrigin) : undefined;
31474
31898
  const fileSearchPaths = file.searchPaths ? resolveSearchPaths(file.searchPaths, fileWorkspacePath ?? configOrigin) : undefined;
31475
- if (file.excludePaths !== undefined && (!Array.isArray(file.excludePaths) || file.excludePaths.some((path) => typeof path !== "string" || path.length === 0))) {
31476
- throw new ConfigError("Config field 'excludePaths' must be a non-empty string array");
31477
- }
31478
- const fileExcludePaths = file.excludePaths ? resolveSearchPaths(file.excludePaths, fileWorkspacePath ?? configOrigin) : undefined;
31899
+ const fileExcludePaths = resolveExcludePaths(file.excludePaths, fileWorkspacePath ?? configOrigin);
31479
31900
  const searchPaths = flagSearchPaths ?? envSearchPaths ?? fileSearchPaths ?? defaultSearchPaths;
31480
31901
  const flagWorkspacePath = flagString(flags, "workspace");
31481
31902
  const envWorkspacePath = envString(env, "AUTORAG_WORKSPACE");
@@ -32211,7 +32632,7 @@ function writeDefaultConfig(path, partial, opts = {}) {
32211
32632
  full.dupey = partial.dupey ?? { enabled: true };
32212
32633
  full.excludeExactDuplicates = partial.excludeExactDuplicates ?? true;
32213
32634
  if (partial.excludePaths !== undefined)
32214
- full.excludePaths = resolveSearchPaths(partial.excludePaths, workspacePath);
32635
+ full.excludePaths = resolveExcludePaths(partial.excludePaths, workspacePath);
32215
32636
  if (partial.limits !== undefined)
32216
32637
  full.limits = normalizeLimitsConfig(partial.limits);
32217
32638
  if (partial.refresh !== undefined)
@@ -32230,7 +32651,7 @@ function writeDefaultConfig(path, partial, opts = {}) {
32230
32651
  `;
32231
32652
  const lock = acquireConfigWriteLock(path);
32232
32653
  try {
32233
- const exists = existsSync28(path);
32654
+ const exists = existsSync29(path);
32234
32655
  if (!opts.force && exists) {
32235
32656
  throw new ConfigError(`Config file already exists: ${path}`);
32236
32657
  }