@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/core.js CHANGED
@@ -2109,6 +2109,25 @@ function errorMessage(error) {
2109
2109
  var MANIFEST_FILENAME = "manifest.json";
2110
2110
  var init_bundle = () => {};
2111
2111
 
2112
+ // src/filesystem/path-exclusions.ts
2113
+ import { posix as posix2, win32 as win322 } from "node:path";
2114
+ function createPathExclusionMatcher(excludedPaths, platform = process.platform) {
2115
+ const paths = platform === "win32" ? win322 : posix2;
2116
+ const normalize = (path) => {
2117
+ const absolute = paths.resolve(path);
2118
+ return platform === "win32" ? absolute.toLowerCase() : absolute;
2119
+ };
2120
+ const excluded = excludedPaths.map((path) => {
2121
+ const absolute = normalize(path);
2122
+ return { absolute, prefix: absolute.endsWith(paths.sep) ? absolute : `${absolute}${paths.sep}` };
2123
+ });
2124
+ return (path) => {
2125
+ const absolute = normalize(path);
2126
+ return excluded.some((entry) => absolute === entry.absolute || absolute.startsWith(entry.prefix));
2127
+ };
2128
+ }
2129
+ var init_path_exclusions = () => {};
2130
+
2112
2131
  // src/everything/client.ts
2113
2132
  import { spawn as spawn2 } from "node:child_process";
2114
2133
  import { createHash as createHash4 } from "node:crypto";
@@ -2226,6 +2245,7 @@ class EverythingClient {
2226
2245
  run;
2227
2246
  launch;
2228
2247
  stateDir;
2248
+ isExcludedPath;
2229
2249
  binaries;
2230
2250
  operation = Promise.resolve();
2231
2251
  constructor(options) {
@@ -2235,6 +2255,7 @@ class EverythingClient {
2235
2255
  this.run = options.run ?? runProcess;
2236
2256
  this.launch = options.launch ?? launchDetached;
2237
2257
  this.stateDir = join6(options.root, ".autorag", "everything");
2258
+ this.isExcludedPath = createPathExclusionMatcher(options.excludeFolders ?? [], "win32");
2238
2259
  }
2239
2260
  isSupported() {
2240
2261
  return this.platform === "win32";
@@ -2254,6 +2275,9 @@ class EverythingClient {
2254
2275
  }
2255
2276
  const es = { ok: true, esPath: binaries.esPath };
2256
2277
  const timeoutMs = this.options.timeoutMs ?? DEFAULT_TIMEOUT_MS2;
2278
+ if ((this.options.excludeFolders?.length ?? 0) > 0) {
2279
+ return this.searchWithExclusions(es.esPath, request, timeoutMs);
2280
+ }
2257
2281
  const args = buildEverythingSearchArgs(this.instanceName, request, timeoutMs);
2258
2282
  const result = await this.run(es.esPath, args, timeoutMs + 5000);
2259
2283
  if (result.code !== 0)
@@ -2265,6 +2289,50 @@ class EverythingClient {
2265
2289
  }
2266
2290
  });
2267
2291
  }
2292
+ async searchWithExclusions(esPath, request, timeoutMs) {
2293
+ const deadline = Date.now() + timeoutMs;
2294
+ const offset = request.offset ?? 0;
2295
+ const maxResults = request.maxResults ?? DEFAULT_MAX_RESULTS;
2296
+ const results = [];
2297
+ let skipped = 0;
2298
+ let nativeOffset = 0;
2299
+ const timedOut = () => ({
2300
+ ok: false,
2301
+ reason: "search-failed",
2302
+ message: `Everything search exceeded ${timeoutMs}ms while filtering excluded paths; narrow the query or increase the Everything timeout.`
2303
+ });
2304
+ for (;; ) {
2305
+ const remainingMs = deadline - Date.now();
2306
+ if (remainingMs <= 0)
2307
+ return timedOut();
2308
+ const args = buildEverythingSearchArgs(this.instanceName, { ...request, offset: nativeOffset, maxResults: MAX_FILTER_CANDIDATES }, remainingMs);
2309
+ const response = await this.run(esPath, args, remainingMs + 5000);
2310
+ if (response.code !== 0)
2311
+ return this.failure("search-failed", esPath, args, response);
2312
+ if (Date.now() >= deadline)
2313
+ return timedOut();
2314
+ let entries;
2315
+ try {
2316
+ entries = parseEverythingJson(response.stdout);
2317
+ } catch (error) {
2318
+ return { ok: false, reason: "search-failed", message: error.message };
2319
+ }
2320
+ for (const entry of entries) {
2321
+ if (this.isExcludedPath(entry.path))
2322
+ continue;
2323
+ if (skipped < offset) {
2324
+ skipped += 1;
2325
+ continue;
2326
+ }
2327
+ results.push(entry);
2328
+ if (results.length >= maxResults)
2329
+ return { ok: true, results };
2330
+ }
2331
+ if (entries.length < MAX_FILTER_CANDIDATES)
2332
+ return { ok: true, results };
2333
+ nativeOffset += entries.length;
2334
+ }
2335
+ }
2268
2336
  index() {
2269
2337
  return this.serialize(async () => {
2270
2338
  const es = await this.ensureRunning(true);
@@ -2338,7 +2406,8 @@ class EverythingClient {
2338
2406
  excludeFolders: [
2339
2407
  ...new Set([
2340
2408
  join6(this.options.root, ".autorag"),
2341
- ...this.options.folders.map((folder) => join6(folder, ".autorag"))
2409
+ ...this.options.folders.map((folder) => join6(folder, ".autorag")),
2410
+ ...this.options.excludeFolders ?? []
2342
2411
  ])
2343
2412
  ]
2344
2413
  }));
@@ -2432,8 +2501,9 @@ function launchDetached(command, args) {
2432
2501
  child.on("error", () => {});
2433
2502
  child.unref();
2434
2503
  }
2435
- var DEFAULT_TIMEOUT_MS2 = 30000, DEFAULT_STARTUP_TIMEOUT_MS = 20000, DEFAULT_INDEX_TIMEOUT_MS, DEFAULT_POLL_INTERVAL_MS = 250, DEFAULT_MAX_RESULTS = 100, MAX_OUTPUT_BYTES, VERSION_PATTERN;
2504
+ var DEFAULT_TIMEOUT_MS2 = 30000, DEFAULT_STARTUP_TIMEOUT_MS = 20000, DEFAULT_INDEX_TIMEOUT_MS, DEFAULT_POLL_INTERVAL_MS = 250, DEFAULT_MAX_RESULTS = 100, MAX_FILTER_CANDIDATES = 1000, MAX_OUTPUT_BYTES, VERSION_PATTERN;
2436
2505
  var init_client = __esm(() => {
2506
+ init_path_exclusions();
2437
2507
  init_bundle();
2438
2508
  DEFAULT_INDEX_TIMEOUT_MS = 10 * 60000;
2439
2509
  MAX_OUTPUT_BYTES = 8 * 1024 * 1024;
@@ -2453,6 +2523,7 @@ function compare(a, b) {
2453
2523
  return a < b ? -1 : a > b ? 1 : 0;
2454
2524
  }
2455
2525
  async function walkFileSearch(folders, request, options = {}) {
2526
+ const isExcluded = createPathExclusionMatcher(options.excludeFolders ?? []);
2456
2527
  const maxVisited = options.maxVisited ?? DEFAULT_MAX_VISITED;
2457
2528
  const maxMatches = options.maxMatches ?? DEFAULT_MAX_MATCHES;
2458
2529
  const deadline = Date.now() + (options.deadlineMs ?? DEFAULT_DEADLINE_MS);
@@ -2475,6 +2546,8 @@ async function walkFileSearch(folders, request, options = {}) {
2475
2546
  outer:
2476
2547
  while (pending.length > 0) {
2477
2548
  const dir = pending.pop();
2549
+ if (isExcluded(dir))
2550
+ continue;
2478
2551
  if (Date.now() >= deadline) {
2479
2552
  truncated = true;
2480
2553
  break;
@@ -2492,6 +2565,8 @@ async function walkFileSearch(folders, request, options = {}) {
2492
2565
  }
2493
2566
  visited += 1;
2494
2567
  const path = join7(dir, dirent.name);
2568
+ if (isExcluded(path))
2569
+ continue;
2495
2570
  if (dirent.isDirectory()) {
2496
2571
  if (dirent.name === ".autorag")
2497
2572
  continue;
@@ -2526,6 +2601,8 @@ async function walkFileSearch(folders, request, options = {}) {
2526
2601
  }
2527
2602
  }
2528
2603
  for (const folder of folders) {
2604
+ if (isExcluded(folder))
2605
+ continue;
2529
2606
  if (!underPath(folder) || !matches(folder))
2530
2607
  continue;
2531
2608
  if (request.kind === "files")
@@ -2552,6 +2629,7 @@ async function walkFileSearch(folders, request, options = {}) {
2552
2629
  }
2553
2630
  var DEFAULT_MAX_VISITED = 500000, DEFAULT_DEADLINE_MS = 20000, DEFAULT_MAX_MATCHES = 1e4, SORT_ORDERS;
2554
2631
  var init_walk = __esm(() => {
2632
+ init_path_exclusions();
2555
2633
  SORT_ORDERS = {
2556
2634
  "name-ascending": (a, b) => compare(a.name, b.name),
2557
2635
  "name-descending": (a, b) => compare(b.name, a.name),
@@ -2661,6 +2739,7 @@ class FSearchClient {
2661
2739
  launch;
2662
2740
  isAlive;
2663
2741
  kill;
2742
+ isExcludedPath;
2664
2743
  stateDir;
2665
2744
  dbPath;
2666
2745
  pidPath;
@@ -2673,6 +2752,7 @@ class FSearchClient {
2673
2752
  this.launch = options.launch ?? launchDetached2;
2674
2753
  this.isAlive = options.isProcessAlive ?? defaultIsProcessAlive;
2675
2754
  this.kill = options.killProcess ?? ((pid) => process.kill(pid, "SIGTERM"));
2755
+ this.isExcludedPath = createPathExclusionMatcher(options.excludeFolders ?? []);
2676
2756
  this.stateDir = join8(options.root, ".autorag", "fsearch");
2677
2757
  this.dbPath = join8(this.stateDir, "fsearch.db");
2678
2758
  this.pidPath = join8(this.stateDir, "watch.pid");
@@ -2690,6 +2770,7 @@ class FSearchClient {
2690
2770
  if (unavailable !== undefined || !binary.ok) {
2691
2771
  try {
2692
2772
  const walked = await walkFileSearch(this.options.folders, request, {
2773
+ excludeFolders: this.options.excludeFolders,
2693
2774
  maxVisited: this.options.walkMaxVisited,
2694
2775
  deadlineMs: this.options.walkDeadlineMs
2695
2776
  });
@@ -2704,7 +2785,9 @@ class FSearchClient {
2704
2785
  return { ok: false, reason: "search-failed", message: error.message };
2705
2786
  }
2706
2787
  }
2707
- const args = buildFSearchSearchArgs(this.dbPath, this.socketPath(), request);
2788
+ const hasExclusions = (this.options.excludeFolders?.length ?? 0) > 0;
2789
+ const nativeRequest = hasExclusions ? { ...request, offset: 0, maxResults: MAX_RESULTS_CAP } : request;
2790
+ const args = buildFSearchSearchArgs(this.dbPath, this.socketPath(), nativeRequest);
2708
2791
  const result = await this.run(binary.path, args, this.options.timeoutMs ?? DEFAULT_TIMEOUT_MS3);
2709
2792
  if (result.code !== 0)
2710
2793
  return this.failure("search-failed", binary.path, args, result);
@@ -2715,6 +2798,10 @@ class FSearchClient {
2715
2798
  return { ok: false, reason: "search-failed", message: error.message };
2716
2799
  }
2717
2800
  let entries = parsed.entries;
2801
+ if (hasExclusions) {
2802
+ const isExcluded = this.isExcludedPath;
2803
+ entries = entries.filter((entry) => !isExcluded(entry.path));
2804
+ }
2718
2805
  if (request.path !== undefined) {
2719
2806
  const prefix = request.path.endsWith(sep3) ? request.path : `${request.path}${sep3}`;
2720
2807
  entries = entries.filter((entry) => entry.path === request.path || entry.path.startsWith(prefix));
@@ -2724,7 +2811,16 @@ class FSearchClient {
2724
2811
  const maxResults = Math.min(request.maxResults ?? DEFAULT_MAX_RESULTS2, MAX_RESULTS_CAP);
2725
2812
  if (entries.length > maxResults)
2726
2813
  entries = entries.slice(0, maxResults);
2727
- return { ok: true, backend: "fsearch-cli", results: entries, total: parsed.total };
2814
+ return {
2815
+ ok: true,
2816
+ backend: "fsearch-cli",
2817
+ results: entries,
2818
+ total: parsed.total,
2819
+ ...hasExclusions && parsed.total !== undefined && parsed.total > parsed.entries.length && entries.length < maxResults ? {
2820
+ truncated: true,
2821
+ note: `FSearch checked ${parsed.entries.length} of ${parsed.total} native matches before exclusions (candidate limit ${MAX_RESULTS_CAP}); results may be incomplete. Narrow the query.`
2822
+ } : {}
2823
+ };
2728
2824
  });
2729
2825
  }
2730
2826
  index() {
@@ -2918,6 +3014,7 @@ function defaultIsProcessAlive(pid) {
2918
3014
  }
2919
3015
  var DEFAULT_TIMEOUT_MS3 = 30000, DEFAULT_STARTUP_TIMEOUT_MS2 = 1e4, STOP_TIMEOUT_MS = 2000, DEFAULT_INDEX_TIMEOUT_MS2, DEFAULT_POLL_INTERVAL_MS2 = 250, DEFAULT_MAX_RESULTS2 = 100, MAX_RESULTS_CAP = 1000, MAX_OUTPUT_BYTES2, SORT_KEYS;
2920
3016
  var init_client2 = __esm(() => {
3017
+ init_path_exclusions();
2921
3018
  init_walk();
2922
3019
  DEFAULT_INDEX_TIMEOUT_MS2 = 10 * 60000;
2923
3020
  MAX_OUTPUT_BYTES2 = 8 * 1024 * 1024;
@@ -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";
@@ -23010,7 +23292,7 @@ var init_connector = () => {};
23010
23292
 
23011
23293
  // src/datasource/sidecar/config.ts
23012
23294
  import { readFileSync as readFileSync19 } from "node:fs";
23013
- import { homedir as homedir3 } from "node:os";
23295
+ import { homedir as homedir4 } from "node:os";
23014
23296
  import { dirname as dirname18, isAbsolute as isAbsolute8, join as join32, resolve as resolve14, sep as sep7 } from "node:path";
23015
23297
  import { parse as parseToml2 } from "smol-toml";
23016
23298
  function resolveSidecarOptions(input) {
@@ -23050,9 +23332,9 @@ function archivePathFromCrawlerConfig(input) {
23050
23332
  }
23051
23333
  function expandHome(path) {
23052
23334
  if (path === "~")
23053
- return homedir3();
23335
+ return homedir4();
23054
23336
  const prefixed = path.startsWith("~/") || sep7 === "\\" && path.startsWith("~\\");
23055
- return prefixed ? join32(homedir3(), path.slice(2)) : path;
23337
+ return prefixed ? join32(homedir4(), path.slice(2)) : path;
23056
23338
  }
23057
23339
  var init_config2 = __esm(() => {
23058
23340
  init_connector();
@@ -23081,7 +23363,7 @@ function chatCrawler(id) {
23081
23363
  hierarchy: (metadata) => ["chats", str(metadata.chatName) ?? str(metadata.chatId) ?? "unknown"]
23082
23364
  };
23083
23365
  }
23084
- var str = (value) => typeof value === "string" && value.length > 0 ? value : undefined, slack, CRAWLER_DESCRIPTORS;
23366
+ var str = (value) => typeof value === "string" && value.length > 0 ? value : undefined, slack, kakao, CRAWLER_DESCRIPTORS;
23085
23367
  var init_descriptors = __esm(() => {
23086
23368
  slack = {
23087
23369
  id: "slack",
@@ -23112,16 +23394,38 @@ var init_descriptors = __esm(() => {
23112
23394
  str(metadata.channelName) ?? str(metadata.channelId) ?? "unknown"
23113
23395
  ]
23114
23396
  };
23397
+ kakao = {
23398
+ id: "kakao",
23399
+ keyExpression: "m.chunk_id",
23400
+ chunkId: (key) => key,
23401
+ fromClause: "chunks m",
23402
+ 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))",
23403
+ textExpression: "coalesce(m.text, '')",
23404
+ titleExpression: "m.chat_name",
23405
+ epochSecondsExpression: "unixepoch(m.started_at)",
23406
+ fts: { table: "chunks_fts", joinOn: "chunks_fts.rowid = m.rowid" },
23407
+ metadata: [
23408
+ { sql: "m.chat_id", key: "chatId" },
23409
+ { sql: "m.chat_name", key: "chatName" },
23410
+ { sql: "m.sender_nickname", key: "senderNickname" },
23411
+ { sql: "m.started_at", key: "startedAt" },
23412
+ { sql: "m.ended_at", key: "endedAt" }
23413
+ ],
23414
+ channelIdExpression: "m.chat_id",
23415
+ channelNameExpression: "m.chat_name",
23416
+ hierarchy: (metadata) => ["chats", str(metadata.chatName) ?? str(metadata.chatId) ?? "unknown"]
23417
+ };
23115
23418
  CRAWLER_DESCRIPTORS = {
23116
23419
  slack,
23117
23420
  telegram: chatCrawler("telegram"),
23118
- whatsapp: chatCrawler("whatsapp")
23421
+ whatsapp: chatCrawler("whatsapp"),
23422
+ kakao
23119
23423
  };
23120
23424
  });
23121
23425
 
23122
23426
  // src/datasource/skills/clawgallery/client.ts
23123
23427
  import { spawn as spawn10 } from "node:child_process";
23124
- import { existsSync as existsSync23 } from "node:fs";
23428
+ import { existsSync as existsSync24 } from "node:fs";
23125
23429
 
23126
23430
  class ClawGalleryClient {
23127
23431
  options;
@@ -23129,7 +23433,7 @@ class ClawGalleryClient {
23129
23433
  this.options = options;
23130
23434
  }
23131
23435
  async bootstrap(signal) {
23132
- if (this.options.path !== undefined && !existsSync23(this.options.path)) {
23436
+ if (this.options.path !== undefined && !existsSync24(this.options.path)) {
23133
23437
  return {
23134
23438
  ok: false,
23135
23439
  reason: "nonzero-exit",
@@ -24047,7 +24351,7 @@ var init_rclone_connector = __esm(() => {
24047
24351
  });
24048
24352
 
24049
24353
  // src/datasource/chunk-store.ts
24050
- import { existsSync as existsSync24, mkdirSync as mkdirSync18, readFileSync as readFileSync21, writeFileSync as writeFileSync17 } from "node:fs";
24354
+ import { existsSync as existsSync25, mkdirSync as mkdirSync18, readFileSync as readFileSync21, writeFileSync as writeFileSync17 } from "node:fs";
24051
24355
  import { dirname as dirname20, join as join34 } from "node:path";
24052
24356
  function datasourceChunkStorePath(workspaceRoot, skillName, instanceId) {
24053
24357
  return join34(workspaceRoot, ".autorag", "datasources", sanitizeIdSegment(skillName), sanitizeIdSegment(instanceId), "chunks.json");
@@ -24087,7 +24391,7 @@ class DatasourceChunkStore {
24087
24391
  if (this.storePath === undefined)
24088
24392
  return false;
24089
24393
  try {
24090
- if (!existsSync24(this.storePath))
24394
+ if (!existsSync25(this.storePath))
24091
24395
  return false;
24092
24396
  const parsed = JSON.parse(readFileSync21(this.storePath, "utf8"));
24093
24397
  if (parsed.version !== STORE_VERSION || !Array.isArray(parsed.chunks))
@@ -25614,7 +25918,7 @@ var init_github = __esm(() => {
25614
25918
 
25615
25919
  // src/datasource/skills/github-gist/connector.ts
25616
25920
  import { execFile as execFile2 } from "node:child_process";
25617
- import { existsSync as existsSync25, mkdirSync as mkdirSync20, readFileSync as readFileSync23, writeFileSync as writeFileSync19 } from "node:fs";
25921
+ import { existsSync as existsSync26, mkdirSync as mkdirSync20, readFileSync as readFileSync23, writeFileSync as writeFileSync19 } from "node:fs";
25618
25922
  import { dirname as dirname22 } from "node:path";
25619
25923
  async function defaultGhTokenRunner() {
25620
25924
  return new Promise((resolve) => {
@@ -25734,7 +26038,7 @@ class GitHubGistConnector {
25734
26038
  }
25735
26039
  loadState() {
25736
26040
  const statePath = this.options.statePath;
25737
- if (statePath === undefined || !existsSync25(statePath))
26041
+ if (statePath === undefined || !existsSync26(statePath))
25738
26042
  return { version: STATE_VERSION, gists: {} };
25739
26043
  try {
25740
26044
  const parsed = JSON.parse(readFileSync23(statePath, "utf8"));
@@ -25797,7 +26101,7 @@ var init_connector3 = () => {};
25797
26101
 
25798
26102
  // src/datasource/skills/github-gist/semantic.ts
25799
26103
  import { createHash as createHash13 } from "node:crypto";
25800
- import { existsSync as existsSync26, mkdirSync as mkdirSync21, readFileSync as readFileSync24, writeFileSync as writeFileSync20 } from "node:fs";
26104
+ import { existsSync as existsSync27, mkdirSync as mkdirSync21, readFileSync as readFileSync24, writeFileSync as writeFileSync20 } from "node:fs";
25801
26105
  import { dirname as dirname23 } from "node:path";
25802
26106
  function contentHash(chunk) {
25803
26107
  return createHash13("sha256").update(`${chunk.title ?? ""}
@@ -25888,7 +26192,7 @@ ${chunk.content}`));
25888
26192
  if (this.loaded)
25889
26193
  return;
25890
26194
  this.loaded = true;
25891
- if (this.statePath === undefined || !existsSync26(this.statePath))
26195
+ if (this.statePath === undefined || !existsSync27(this.statePath))
25892
26196
  return;
25893
26197
  try {
25894
26198
  const parsed = JSON.parse(readFileSync24(this.statePath, "utf8"));
@@ -26751,7 +27055,7 @@ var init_types5 = __esm(() => {
26751
27055
 
26752
27056
  // src/datasource/skills/lazykatok/client.ts
26753
27057
  import { spawn as spawn14 } from "node:child_process";
26754
- import { existsSync as existsSync27 } from "node:fs";
27058
+ import { existsSync as existsSync28 } from "node:fs";
26755
27059
  import { delimiter as delimiter4, join as join36 } from "node:path";
26756
27060
 
26757
27061
  class LazykatokClient {
@@ -26941,7 +27245,7 @@ function lookupInPath2(executable, env) {
26941
27245
  if (dir.length === 0)
26942
27246
  continue;
26943
27247
  const candidate = join36(dir, executable);
26944
- if (existsSync27(candidate))
27248
+ if (existsSync28(candidate))
26945
27249
  return candidate;
26946
27250
  }
26947
27251
  return;
@@ -27263,1436 +27567,1528 @@ var init_methods3 = __esm(() => {
27263
27567
  DEFAULT_KAKAO_TAGS = ["kakaotalk", "personal", "pii"];
27264
27568
  });
27265
27569
 
27266
- // src/datasource/skills/lazykatok/skill.ts
27267
- class LazykatokSkill {
27268
- client;
27269
- instanceId;
27270
- instances;
27271
- pollingIntervalMs;
27272
- tags;
27273
- lastIndexedAt;
27274
- constructor(options) {
27275
- this.client = options.client;
27276
- this.instanceId = options.instanceId ?? DEFAULT_INSTANCE_ID4;
27277
- this.instances = options.instances !== undefined && options.instances.length > 0 ? options.instances : [this.instanceId];
27278
- this.pollingIntervalMs = options.pollingIntervalMs ?? DEFAULT_POLLING_INTERVAL_MS3;
27279
- this.tags = options.tags ?? DEFAULT_KAKAO_TAGS2;
27280
- this.lastIndexedAt = options.lastIndexedAt;
27281
- }
27282
- describe() {
27283
- return {
27284
- name: KAKAO_DATASOURCE_ID2,
27285
- id: KAKAO_DATASOURCE_ID2,
27286
- type: KAKAO_SKILL_TYPE,
27287
- description: "KakaoTalk datasource via the external lazykatok CLI",
27288
- capabilities: ["chat", "external-cli", "polling", "bm25", "semantic"],
27289
- tags: this.tags,
27290
- status: "active",
27291
- requiresExternalCli: true,
27292
- datasourceId: KAKAO_DATASOURCE_ID2,
27293
- instanceId: this.instanceId,
27294
- instances: this.instances
27295
- };
27570
+ // src/datasource/sidecar/fusion.ts
27571
+ function encodeVector(vector) {
27572
+ const floats = Float32Array.from(vector);
27573
+ return new Uint8Array(floats.buffer, floats.byteOffset, floats.byteLength);
27574
+ }
27575
+ function decodeVector(blob) {
27576
+ if (blob.byteLength % Float32Array.BYTES_PER_ELEMENT !== 0)
27577
+ return;
27578
+ return new Float32Array(new Uint8Array(blob).buffer);
27579
+ }
27580
+ function cosine3(a, b) {
27581
+ if (a.length === 0 || a.length !== b.length)
27582
+ return 0;
27583
+ let dot = 0;
27584
+ let normA = 0;
27585
+ let normB = 0;
27586
+ for (let index = 0;index < a.length; index += 1) {
27587
+ const x = a[index] ?? 0;
27588
+ const y = b[index] ?? 0;
27589
+ dot += x * y;
27590
+ normA += x * x;
27591
+ normB += y * y;
27296
27592
  }
27297
- polling() {
27298
- return {
27299
- mode: "poll",
27300
- intervalMs: this.pollingIntervalMs,
27301
- lastIndexedAt: this.lastIndexedAt
27302
- };
27593
+ if (normA === 0 || normB === 0)
27594
+ return 0;
27595
+ return dot / Math.sqrt(normA * normB);
27596
+ }
27597
+ function topKByCosine(query, rows, k) {
27598
+ if (k <= 0)
27599
+ return [];
27600
+ const scored = [];
27601
+ for (const row of rows) {
27602
+ const score = cosine3(query, row.vector);
27603
+ if (score > 0)
27604
+ scored.push({ key: row.key, score });
27303
27605
  }
27304
- async index(options) {
27305
- try {
27306
- const doctor = await this.client.doctor(options?.signal);
27307
- if (!doctor.ok)
27308
- return this.fail("datasource-unavailable", doctor);
27309
- const sync = await this.client.sync(options?.signal);
27310
- if (!sync.ok)
27311
- return this.fail("datasource-index-failed", sync);
27312
- const indexResult = await this.client.index(options?.signal);
27313
- if (!indexResult.ok)
27314
- return this.fail("datasource-index-failed", indexResult);
27315
- this.lastIndexedAt = Date.now();
27316
- 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;
27317
- return {
27318
- ok: true,
27319
- instanceId: this.instanceId,
27320
- skill: KAKAO_DATASOURCE_ID2,
27321
- chunkCount,
27322
- indexedAt: this.lastIndexedAt,
27323
- diagnostics: []
27324
- };
27325
- } catch (error) {
27326
- return this.fail("datasource-unavailable", {
27327
- ok: false,
27328
- reason: "spawn-error",
27329
- stdout: "",
27330
- stderr: describeRetrievalError(error),
27331
- code: null
27332
- });
27606
+ scored.sort((a, b) => b.score - a.score || a.key.localeCompare(b.key));
27607
+ return scored.slice(0, k);
27608
+ }
27609
+ function reciprocalRankFusion(lists) {
27610
+ const scores = new Map;
27611
+ for (const list of lists) {
27612
+ const seen = new Set;
27613
+ for (const [index, key] of list.entries()) {
27614
+ if (seen.has(key))
27615
+ continue;
27616
+ seen.add(key);
27617
+ scores.set(key, (scores.get(key) ?? 0) + 1 / (RRF_K2 + index + 1));
27333
27618
  }
27334
27619
  }
27335
- retrievalMethods() {
27336
- return [
27337
- new LazykatokBm25Method({ client: this.client, instanceId: this.instanceId, tags: this.tags }),
27338
- new LazykatokSemanticMethod({ client: this.client, instanceId: this.instanceId, tags: this.tags })
27339
- ];
27340
- }
27341
- describeSources() {
27342
- return this.instances.map((instanceId) => ({
27343
- source: datasourceSourcePath(KAKAO_DATASOURCE_ID2, instanceId),
27344
- datasourceId: KAKAO_DATASOURCE_ID2,
27345
- skill: KAKAO_DATASOURCE_ID2,
27346
- instanceId,
27347
- contentType: "chat",
27348
- metadata: {
27349
- datasourceId: KAKAO_DATASOURCE_ID2,
27350
- instanceId,
27351
- tags: this.tags
27352
- }
27353
- }));
27354
- }
27355
- skillManifest() {
27356
- const instanceSources = this.instances.map((instanceId) => `- \`${datasourceSourcePath(KAKAO_DATASOURCE_ID2, instanceId)}\``).join(`
27357
- `);
27358
- const cadence = this.pollingIntervalMs > 0 ? `roughly every ${Math.round(this.pollingIntervalMs / 60000)} minute(s) when auto-refresh runs` : "on manual refresh only";
27359
- return {
27360
- name: `datasource-${KAKAO_DATASOURCE_ID2}`,
27361
- description: "Search indexed KakaoTalk chat history (messages, senders, room context). Use for questions about KakaoTalk conversations, decisions made in chats, or who said what.",
27362
- content: [
27363
- `# KakaoTalk datasource (${KAKAO_SKILL_TYPE})`,
27364
- "",
27365
- "This skill searches KakaoTalk chats that are indexed through the external `lazykatok` CLI. AutoRAG never reads KakaoTalk databases directly.",
27366
- "",
27367
- "## When to use",
27368
- "Use this skill when the question is about KakaoTalk conversations, chat participants, or content shared inside chats.",
27369
- "",
27370
- "## Indexing",
27371
- `Indexing is server-managed and refreshed ${cadence}. You do not trigger indexing; just search.`,
27372
- "",
27373
- "## How to search",
27374
- `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:`,
27375
- instanceSources.length > 0 ? instanceSources : "- (no configured instances)",
27376
- "",
27377
- "Chat/channel filtering is owned by lazykatok.",
27378
- "",
27379
- "## Native CLI",
27380
- "The external `lazykatok` CLI owns the archive, index, and credentials. When the dedicated tool cannot express what you need, call lazykatok directly through `bash`:",
27381
- '- `lazykatok search bm25 "<query>" --json --limit 20` — lexical search (`semantic` mode also available)',
27382
- "- `lazykatok chunk get <chunkId> --json` — fetch one chunk by id (chunk ids appear in result metadata)",
27383
- "- `lazykatok chunk context <chunkId> --json` — surrounding messages of a chunk",
27384
- "Never pass datasource virtual paths (`/kakao/...`) to bash; they are not OS paths.",
27385
- "",
27386
- "## Output rules",
27387
- "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."
27388
- ].join(`
27389
- `)
27390
- };
27391
- }
27392
- fail(code, result) {
27393
- const base = result.stderr.length > 0 ? `${result.reason}: ${result.stderr.trim().slice(0, 4000)}` : result.reason;
27394
- const message = code === "datasource-index-failed" && result.reason === "timeout" ? `${base} — ${connectorSyncTimeoutHint()}` : base;
27395
- const diagnostic = {
27396
- code,
27397
- severity: code === "datasource-unavailable" ? "warning" : "error",
27398
- message,
27399
- instanceId: this.instanceId,
27400
- source: KAKAO_DATASOURCE_ID2
27401
- };
27402
- return {
27403
- ok: false,
27404
- instanceId: this.instanceId,
27405
- skill: KAKAO_DATASOURCE_ID2,
27406
- indexedAt: Date.now(),
27407
- diagnostics: [diagnostic],
27408
- error: result.reason,
27409
- code,
27410
- message
27411
- };
27412
- }
27620
+ return [...scores.entries()].map(([key, score]) => ({ key, score })).sort((a, b) => b.score - a.score);
27413
27621
  }
27414
- var KAKAO_DATASOURCE_ID2 = "kakao", KAKAO_SKILL_TYPE = "kakaotalk", DEFAULT_INSTANCE_ID4 = "default", DEFAULT_POLLING_INTERVAL_MS3, DEFAULT_KAKAO_TAGS2;
27415
- var init_skill7 = __esm(() => {
27416
- init_skip();
27417
- init_scope2();
27418
- init_methods3();
27419
- DEFAULT_POLLING_INTERVAL_MS3 = 15 * 60 * 1000;
27420
- DEFAULT_KAKAO_TAGS2 = ["kakaotalk", "personal", "pii"];
27421
- });
27422
-
27423
- // src/datasource/skills/lazykatok/index.ts
27424
- var init_lazykatok = __esm(() => {
27425
- init_client8();
27426
- init_methods3();
27427
- init_skill7();
27428
- init_types5();
27429
- });
27622
+ var RRF_K2 = 60;
27430
27623
 
27431
- // src/datasource/skills/mail-export/connector.ts
27432
- import { readdir as readdir4, readFile as readFile5, stat as stat6 } from "node:fs/promises";
27433
- import { basename as basename12, dirname as dirname24, extname as extname6, join as join37 } from "node:path";
27434
- import { simpleParser as simpleParser2 } from "mailparser";
27624
+ // src/datasource/sidecar/index.ts
27625
+ import { createHash as createHash14 } from "node:crypto";
27626
+ import { mkdirSync as mkdirSync22, statSync as statSync3 } from "node:fs";
27627
+ import { dirname as dirname24 } from "node:path";
27628
+ import { DatabaseSync as DatabaseSync2 } from "node:sqlite";
27629
+ import { pathToFileURL as pathToFileURL2 } from "node:url";
27435
27630
 
27436
- class MailExportConnector {
27631
+ class CrawlerSidecar {
27632
+ descriptor;
27437
27633
  options;
27438
- constructor(options = {}) {
27634
+ select;
27635
+ where;
27636
+ opened;
27637
+ attached;
27638
+ lastSemantic;
27639
+ constructor(options) {
27439
27640
  this.options = options;
27641
+ const { descriptor } = options;
27642
+ this.descriptor = descriptor;
27643
+ const metadataColumns = descriptor.metadata.map((column, index) => `${column.sql} as m${index}`);
27644
+ this.select = [
27645
+ `${descriptor.keyExpression} as k`,
27646
+ `${descriptor.textExpression} as content`,
27647
+ `${descriptor.titleExpression ?? "null"} as title`,
27648
+ `${descriptor.epochSecondsExpression ?? "null"} as epoch`,
27649
+ ...metadataColumns
27650
+ ].join(", ");
27651
+ const scope = descriptor.scopeExpression !== undefined && options.workspaceId !== undefined;
27652
+ const allowlist = [
27653
+ ...(options.channelIds?.length ?? 0) > 0 ? [`${descriptor.channelIdExpression} in (${options.channelIds?.map(() => "?").join(", ")})`] : [],
27654
+ ...(options.channelNames?.length ?? 0) > 0 ? [`${descriptor.channelNameExpression} in (${options.channelNames?.map(() => "?").join(", ")})`] : []
27655
+ ];
27656
+ this.where = [
27657
+ descriptor.liveWhere,
27658
+ ...scope ? [`${descriptor.scopeExpression} = ?`] : [],
27659
+ ...allowlist.length > 0 ? [`(${allowlist.join(" or ")})`] : []
27660
+ ].join(" and ");
27440
27661
  }
27441
- async fetch() {
27442
- const paths = this.options.paths ?? [];
27443
- if (paths.length === 0)
27444
- return { ok: false, reason: "not-configured", message: "no mail export paths configured" };
27445
- const maxDocuments = this.options.maxDocuments ?? DEFAULT_MAX_DOCUMENTS3;
27446
- const maxBytes = this.options.maxBytesPerFile ?? DEFAULT_MAX_BYTES_PER_FILE2;
27447
- const files = [];
27448
- let missingPaths = 0;
27449
- for (const path of paths) {
27450
- try {
27451
- const info = await stat6(path);
27452
- if (info.isDirectory()) {
27453
- const entries = await readdir4(path, { recursive: true, withFileTypes: true });
27454
- for (const entry of entries) {
27455
- if (!entry.isFile())
27456
- continue;
27457
- const extension = extname6(entry.name).toLowerCase();
27458
- if (extension === ".eml" || extension === ".mbox") {
27459
- files.push(join37(entry.parentPath, entry.name));
27460
- }
27461
- }
27462
- } else if (info.isFile()) {
27463
- files.push(path);
27464
- }
27465
- } catch {
27466
- missingPaths += 1;
27467
- }
27662
+ get db() {
27663
+ if (this.opened !== undefined)
27664
+ return this.opened;
27665
+ mkdirSync22(dirname24(this.options.sidecarDbPath), { recursive: true });
27666
+ const db = new DatabaseSync2(this.options.sidecarDbPath);
27667
+ db.exec(`pragma busy_timeout = ${BUSY_TIMEOUT_MS}`);
27668
+ db.exec("pragma journal_mode = wal");
27669
+ db.exec(`
27670
+ create table if not exists sidecar_meta (k text primary key, v text not null);
27671
+ create table if not exists sidecar_vec (message_key text primary key, hash text not null, vec blob not null);
27672
+ `);
27673
+ this.opened = db;
27674
+ return db;
27675
+ }
27676
+ attachCrawler() {
27677
+ let file;
27678
+ try {
27679
+ const { dev, ino } = statSync3(this.options.crawlerDbPath);
27680
+ file = { dev, ino };
27681
+ } catch {
27682
+ this.detachCrawler();
27683
+ return false;
27468
27684
  }
27469
- if (missingPaths === paths.length) {
27470
- return { ok: false, reason: "unavailable", message: "no configured mail export paths were readable" };
27685
+ if (this.attached !== undefined) {
27686
+ if (this.attached.dev === file.dev && this.attached.ino === file.ino)
27687
+ return true;
27688
+ this.detachCrawler();
27471
27689
  }
27472
- const documents = [];
27473
- const warnings = [];
27474
- if (missingPaths > 0)
27475
- warnings.push(`${missingPaths} configured path(s) were unreadable`);
27476
- let parseFailures = 0;
27477
- let skippedLarge = 0;
27478
- for (const file of files.sort()) {
27479
- if (documents.length >= maxDocuments)
27480
- break;
27481
- let bytes;
27482
- try {
27483
- const info = await stat6(file);
27484
- if (info.size > maxBytes) {
27485
- skippedLarge += 1;
27486
- continue;
27487
- }
27488
- bytes = await readFile5(file);
27489
- } catch {
27490
- parseFailures += 1;
27491
- continue;
27492
- }
27493
- const stem = basename12(file, extname6(file));
27494
- const mailboxDir = basename12(dirname24(file));
27495
- const hierarchy = ["mailboxes", mailboxDir, stem];
27496
- const extension = extname6(file).toLowerCase();
27497
- const rawMessages = extension === ".mbox" ? splitMbox(bytes.toString("utf8")) : [bytes.toString("utf8")];
27498
- for (const [index, raw] of rawMessages.entries()) {
27499
- if (documents.length >= maxDocuments)
27500
- break;
27501
- try {
27502
- const parsed = await simpleParser2(raw);
27503
- const subject = parsed.subject ?? "(no subject)";
27504
- const from = Array.isArray(parsed.from) ? parsed.from[0]?.text : parsed.from?.text;
27505
- const to = Array.isArray(parsed.to) ? parsed.to[0]?.text : parsed.to?.text;
27506
- const body = (parsed.text ?? "").trim().slice(0, MAX_BODY_CHARS);
27507
- const headerBlock = [
27508
- `Subject: ${subject}`,
27509
- ...from !== undefined ? [`From: ${from}`] : [],
27510
- ...to !== undefined ? [`To: ${to}`] : [],
27511
- ...parsed.date !== undefined ? [`Date: ${parsed.date.toISOString()}`] : []
27512
- ].join(`
27513
- `);
27514
- const content = `${headerBlock}
27515
-
27516
- ${body}`.trim();
27517
- if (content.length === 0)
27518
- continue;
27519
- documents.push({
27520
- docId: extension === ".mbox" ? `${stem}-${index + 1}` : stem,
27521
- hierarchy,
27522
- title: subject,
27523
- content,
27524
- ...parsed.date !== undefined ? { publishedAt: parsed.date.getTime() } : {},
27525
- metadata: {
27526
- path: file,
27527
- subject,
27528
- ...from !== undefined ? { from } : {},
27529
- messageIndex: index
27530
- }
27531
- });
27532
- } catch {
27533
- parseFailures += 1;
27534
- }
27535
- }
27690
+ const crawlerUri = `${pathToFileURL2(this.options.crawlerDbPath).href}?mode=ro`.replaceAll("'", "''");
27691
+ try {
27692
+ this.db.exec(`attach database '${crawlerUri}' as crawler`);
27693
+ } catch {
27694
+ return false;
27536
27695
  }
27537
- if (skippedLarge > 0)
27538
- warnings.push(`${skippedLarge} file(s) exceeded the size limit and were skipped`);
27539
- if (parseFailures > 0)
27540
- warnings.push(`${parseFailures} message(s) failed to parse`);
27541
- return { ok: true, documents, ...warnings.length > 0 ? { warnings } : {} };
27696
+ const { descriptor } = this.options;
27697
+ try {
27698
+ this.db.prepare(`select ${this.select} from ${descriptor.fromClause} where ${this.where} limit 0`).all(...this.whereParams());
27699
+ this.db.prepare(`select 1 from ${descriptor.fts.table} limit 0`).all();
27700
+ } catch {
27701
+ this.db.exec("detach database crawler");
27702
+ return false;
27703
+ }
27704
+ this.attached = file;
27705
+ this.lastSemantic = undefined;
27706
+ return true;
27542
27707
  }
27543
- }
27544
- function splitMbox(text) {
27545
- const lines = text.split(/\r?\n/u);
27546
- const messages = [];
27547
- let current = [];
27548
- for (const line of lines) {
27549
- if (MBOX_SEPARATOR.test(line) && line.startsWith("From ")) {
27550
- if (current.length > 0)
27551
- messages.push(current.join(`
27552
- `));
27553
- current = [];
27554
- continue;
27555
- }
27556
- current.push(line);
27557
- }
27558
- if (current.length > 0)
27559
- messages.push(current.join(`
27560
- `));
27561
- return messages.filter((message) => message.trim().length > 0);
27562
- }
27563
- var DEFAULT_MAX_DOCUMENTS3 = 500, DEFAULT_MAX_BYTES_PER_FILE2, MAX_BODY_CHARS = 50000, MBOX_SEPARATOR;
27564
- var init_connector4 = __esm(() => {
27565
- DEFAULT_MAX_BYTES_PER_FILE2 = 50 * 1024 * 1024;
27566
- MBOX_SEPARATOR = /^From .*\d{4}$/mu;
27567
- });
27568
-
27569
- // src/datasource/skills/mail-export/skill.ts
27570
- var MAIL_EXPORT_SKILL_DEFINITION, MailExportSkill;
27571
- var init_skill8 = __esm(() => {
27572
- init_connector_skill();
27573
- init_connector4();
27574
- MAIL_EXPORT_SKILL_DEFINITION = {
27575
- skillName: "mail-export",
27576
- skillType: "mail-export",
27577
- description: "Local mail export datasource",
27578
- capabilities: ["email", "filesystem"],
27579
- defaultTags: ["mail-export", "email", "pii"],
27580
- contentType: "email",
27581
- manifestDescription: "Search indexed local mail exports (mbox/eml archives). Use for questions about archived email conversations.",
27582
- manifestNotes: [
27583
- "Only the server-configured export paths are indexed; archives are read locally and never uploaded."
27584
- ]
27585
- };
27586
- MailExportSkill = class MailExportSkill extends ConnectorDatasourceSkill {
27587
- constructor(options = {}) {
27588
- const { connector, connectorOptions, ...rest } = options;
27589
- super(MAIL_EXPORT_SKILL_DEFINITION, {
27590
- ...rest,
27591
- connector: connector ?? new MailExportConnector(connectorOptions ?? {})
27592
- });
27593
- }
27594
- };
27595
- });
27596
-
27597
- // src/datasource/skills/mail-export/index.ts
27598
- var init_mail_export = __esm(() => {
27599
- init_connector4();
27600
- init_skill8();
27601
- });
27602
-
27603
- // src/datasource/skills/mailcrawl/config.ts
27604
- import { chmodSync, mkdirSync as mkdirSync22 } from "node:fs";
27605
- function validateMailcrawlInstanceId(instanceId) {
27606
- if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/u.test(instanceId)) {
27607
- throw new Error("mailcrawl instanceId must be a safe single path segment");
27708
+ detachCrawler() {
27709
+ if (this.attached === undefined)
27710
+ return;
27711
+ this.attached = undefined;
27712
+ this.lastSemantic = undefined;
27713
+ try {
27714
+ this.opened?.exec("detach database crawler");
27715
+ } catch {}
27608
27716
  }
27609
- }
27610
- function ensurePrivateMailcrawlDataDir(path) {
27611
- mkdirSync22(path, { recursive: true, mode: 448 });
27612
- if (process.platform !== "win32")
27613
- chmodSync(path, 448);
27614
- }
27615
- var init_config3 = () => {};
27616
-
27617
- // src/datasource/skills/mailcrawl/client.ts
27618
- import { spawn as spawn15 } from "node:child_process";
27619
-
27620
- class MailcrawlClient {
27621
- options;
27622
- constructor(options = {}) {
27623
- this.options = options;
27624
- if (options.instanceId !== undefined)
27625
- validateMailcrawlInstanceId(options.instanceId);
27717
+ get semanticEnabled() {
27718
+ return this.options.embedder !== undefined;
27626
27719
  }
27627
- async sync(signal) {
27628
- const args = [
27629
- "sync",
27630
- "--json",
27631
- ...this.options.account === undefined ? [] : ["--account", this.options.account],
27632
- ...this.options.mailbox === undefined ? [] : ["--mailbox", this.options.mailbox],
27633
- ...this.options.backend === undefined ? [] : ["--backend", this.options.backend],
27634
- ...this.options.source === undefined ? [] : ["--source", this.options.source],
27635
- ...this.options.fixture === undefined ? [] : ["--fixture", this.options.fixture],
27636
- ...this.options.himalayaConfig === undefined ? [] : ["--himalaya-config", this.options.himalayaConfig]
27637
- ];
27638
- const result = await this.run(args, signal, this.indexTimeoutMs());
27639
- if (!result.ok)
27640
- return failure3(result);
27641
- const parsed = parseObject2(result.stdout);
27642
- if (parsed === undefined || !hasAnyNumber(parsed, ["added", "updated", "deleted", "unchanged", "chunksAdded"])) {
27643
- return failure3({ ...result, ok: false, reason: "invalid-output" });
27644
- }
27645
- const messages = number(parsed, "added") ?? number(parsed, "updated") ?? number(parsed, "unchanged") ?? 0;
27646
- return ok4({
27647
- messages,
27648
- added: number(parsed, "added"),
27649
- updated: number(parsed, "updated"),
27650
- deleted: number(parsed, "deleted"),
27651
- unchanged: number(parsed, "unchanged"),
27652
- chunksAdded: number(parsed, "chunksAdded"),
27653
- archiveRevision: string(parsed, "archiveRevision")
27654
- }, result);
27720
+ close() {
27721
+ this.opened?.close();
27722
+ this.opened = undefined;
27723
+ this.attached = undefined;
27724
+ this.lastSemantic = undefined;
27655
27725
  }
27656
- async search(mode, query, options) {
27657
- if (query.trim().length === 0)
27658
- return { ok: true, hits: [], stdout: "", stderr: "", code: 0 };
27659
- const args = [
27660
- "search",
27661
- "--mode",
27662
- mode === "keyword" ? "bm25" : mode,
27663
- ...this.options.account === undefined ? [] : ["--account", this.options.account],
27664
- ...this.options.mailbox === undefined ? [] : ["--mailbox", this.options.mailbox],
27665
- "--limit",
27666
- String(options?.topK ?? 20),
27667
- "--json",
27668
- query.trim()
27726
+ whereParams() {
27727
+ const { descriptor, workspaceId, channelIds = [], channelNames = [] } = this.options;
27728
+ return [
27729
+ ...descriptor.scopeExpression !== undefined && workspaceId !== undefined ? [workspaceId] : [],
27730
+ ...channelIds,
27731
+ ...channelNames
27669
27732
  ];
27670
- const result = await this.run(args, options?.signal, this.options.timeoutMs ?? DEFAULT_SEARCH_TIMEOUT_MS2);
27671
- if (!result.ok)
27672
- return failure3(result);
27673
- const parsed = parseJson3(result.stdout);
27674
- if (parsed === undefined)
27675
- return failure3({ ...result, ok: false, reason: "invalid-output" });
27676
- const rows = Array.isArray(parsed) ? parsed : objectArray(parsed, ["hits", "results", "items"]);
27677
- if (rows === undefined)
27678
- return failure3({ ...result, ok: false, reason: "invalid-output" });
27679
- const hits = rows.map((row, index) => normalizeHit2(row, mode, index));
27680
- if (hits.some((hit) => hit === undefined)) {
27681
- return failure3({ ...result, ok: false, reason: "invalid-output" });
27682
- }
27683
- return {
27684
- ok: true,
27685
- hits,
27686
- stdout: result.stdout,
27687
- stderr: result.stderr,
27688
- code: result.code ?? 0
27689
- };
27690
27733
  }
27691
- indexTimeoutMs() {
27692
- return this.options.indexTimeoutMs ?? this.options.timeoutMs ?? DEFAULT_INDEX_TIMEOUT_MS3;
27734
+ searchLexical(query, topK) {
27735
+ const match = toMatchQuery(query);
27736
+ if (match === undefined || topK <= 0 || !this.attachCrawler())
27737
+ return [];
27738
+ const { descriptor } = this.options;
27739
+ const rows = this.db.prepare(`select ${this.select}, bm25(${descriptor.fts.table}) as rank
27740
+ from ${descriptor.fromClause} join ${descriptor.fts.table} on ${descriptor.fts.joinOn}
27741
+ where ${descriptor.fts.table} match ? and ${this.where}
27742
+ order by rank limit ?`).all(match, ...this.whereParams(), topK);
27743
+ return rows.map((row) => this.toHit(this.toRow(row), -Number(row.rank)));
27693
27744
  }
27694
- async run(args, signal, timeoutMs) {
27695
- const violation = findRemoteEmbeddingViolation({ ...process.env, ...this.options.env ?? {} });
27696
- if (violation !== undefined) {
27697
- return { ok: false, reason: "remote-embedding-rejected", stdout: "", stderr: "", code: null };
27745
+ async searchSemantic(query, topK) {
27746
+ const trimmed = query.trim();
27747
+ if (this.options.embedder === undefined || trimmed.length === 0 || topK <= 0 || !this.attachCrawler())
27748
+ return [];
27749
+ const ranked = await this.rankSemantic(trimmed, topK * POOL_FACTOR);
27750
+ if (ranked.length === 0)
27751
+ return [];
27752
+ const live = this.hydrate(ranked.map((entry) => entry.key));
27753
+ const hits = [];
27754
+ for (const entry of ranked) {
27755
+ const row = live.get(entry.key);
27756
+ if (row !== undefined)
27757
+ hits.push(this.toHit(row, entry.score));
27758
+ if (hits.length >= topK)
27759
+ break;
27698
27760
  }
27699
- const dataDir = this.options.dataDir;
27700
- if (dataDir !== undefined)
27701
- ensurePrivateMailcrawlDataDir(dataDir);
27702
- const env = {};
27703
- for (const [key, value] of Object.entries(process.env)) {
27704
- if (value !== undefined && (SAFE_ENV_KEYS2.has(key) || SAFE_MAILCRAWL_ENV_KEYS.has(key)))
27705
- env[key] = value;
27761
+ return hits;
27762
+ }
27763
+ async rankSemantic(query, pool) {
27764
+ const now = Date.now();
27765
+ let cached = this.lastSemantic;
27766
+ if (cached === undefined || cached.query !== query || now - cached.at > SEMANTIC_REUSE_MS) {
27767
+ const embedder = this.options.embedder;
27768
+ if (embedder === undefined)
27769
+ return [];
27770
+ cached = {
27771
+ query,
27772
+ at: now,
27773
+ vector: embedder.embed([query]).then((vectors) => vectors[0], () => {
27774
+ return;
27775
+ }),
27776
+ ranked: undefined
27777
+ };
27778
+ this.lastSemantic = cached;
27706
27779
  }
27707
- for (const [key, value] of Object.entries(this.options.env ?? {})) {
27708
- if (value === undefined)
27709
- delete env[key];
27710
- else if (SAFE_ENV_KEYS2.has(key) || SAFE_MAILCRAWL_ENV_KEYS.has(key))
27711
- env[key] = value;
27780
+ const vector = await cached.vector;
27781
+ if (vector === undefined) {
27782
+ if (this.lastSemantic === cached)
27783
+ this.lastSemantic = undefined;
27784
+ return [];
27712
27785
  }
27713
- if (dataDir !== undefined)
27714
- env.MAILCRAWL_DATA_DIR = dataDir;
27715
- return spawnMailcrawl(this.options.binaryPath ?? DEFAULT_BINARY4, args, env, this.options, signal, timeoutMs);
27716
- }
27717
- async index(signal) {
27718
- const result = await this.run(["index", "--json"], signal, this.indexTimeoutMs());
27719
- if (!result.ok)
27720
- return failure3(result);
27721
- const parsed = parseObject2(result.stdout);
27722
- if (parsed === undefined || number(parsed, "embedded") === undefined && number(parsed, "reused") === undefined && string(parsed, "generation") === undefined) {
27723
- return failure3({ ...result, ok: false, reason: "invalid-output" });
27786
+ if (cached.ranked === undefined || cached.ranked.pool < pool) {
27787
+ const wide = pool * POOL_FACTOR;
27788
+ cached.ranked = { pool: wide, keys: topKByCosine(vector, this.storedVectors(), wide) };
27724
27789
  }
27725
- return ok4({
27726
- embedded: number(parsed, "embedded"),
27727
- reused: number(parsed, "reused"),
27728
- generation: string(parsed, "generation"),
27729
- archiveRevision: string(parsed, "archiveRevision"),
27730
- rebuilt: typeof parsed.rebuilt === "boolean" ? parsed.rebuilt : undefined,
27731
- embedder: string(parsed, "embedder")
27732
- }, result);
27790
+ return cached.ranked.keys.slice(0, pool);
27733
27791
  }
27734
- }
27735
- function spawnMailcrawl(binary, args, env, options, signal, timeoutMs) {
27736
- return new Promise((resolveResult) => {
27737
- const portable = portableSpawnCommand(binary, args);
27738
- const child = spawn15(portable.command, portable.args, {
27739
- env,
27740
- detached: process.platform !== "win32",
27741
- stdio: ["ignore", "pipe", "pipe"]
27742
- });
27743
- let stdout = { text: "", bytes: 0, exceeded: false }, stderr = { text: "", bytes: 0, exceeded: false }, settled = false, reason;
27744
- const max = options.maxBufferBytes ?? DEFAULT_MAX_BUFFER_BYTES4;
27745
- const timer = setTimeout(() => {
27746
- reason = "timeout";
27747
- terminate4(child);
27748
- }, timeoutMs);
27749
- const abort = () => {
27750
- reason = "aborted";
27751
- terminate4(child);
27792
+ async searchHybrid(query, topK) {
27793
+ if (topK <= 0)
27794
+ return [];
27795
+ const pool = topK * POOL_FACTOR;
27796
+ const lexical = this.searchLexical(query, pool);
27797
+ const semantic = await this.searchSemantic(query, pool);
27798
+ if (semantic.length === 0)
27799
+ return lexical.slice(0, topK);
27800
+ const byKey = new Map;
27801
+ for (const hit of [...semantic, ...lexical])
27802
+ byKey.set(hit.key, hit);
27803
+ const fused = [];
27804
+ for (const entry of reciprocalRankFusion([lexical.map((hit) => hit.key), semantic.map((hit) => hit.key)])) {
27805
+ const hit = byKey.get(entry.key);
27806
+ if (hit !== undefined)
27807
+ fused.push({ ...hit, score: entry.score });
27808
+ if (fused.length >= topK)
27809
+ break;
27810
+ }
27811
+ return fused;
27812
+ }
27813
+ async sync() {
27814
+ const { embedder } = this.options;
27815
+ if (embedder === undefined)
27816
+ return { ok: false, message: "no embedder configured" };
27817
+ if (!this.attachCrawler())
27818
+ return { ok: false, message: "crawler archive not available yet" };
27819
+ let identity;
27820
+ try {
27821
+ identity = await embedder.identity();
27822
+ } catch (error) {
27823
+ return { ok: false, message: errorMessage2(error) };
27824
+ }
27825
+ this.adoptIdentity(identity);
27826
+ const pruned = this.prune();
27827
+ const pending = this.pendingKeys();
27828
+ const batchSize = Math.max(1, this.options.batchSize ?? DEFAULT_BATCH_SIZE);
27829
+ const budget = Math.max(1, this.options.maxEmbedPerSync ?? DEFAULT_MAX_EMBED_PER_SYNC);
27830
+ const target = Math.min(pending.length, budget);
27831
+ const embed = async (rows) => {
27832
+ const vectors = await embedder.embed(rows.map((row) => row.input));
27833
+ if (vectors.length !== rows.length) {
27834
+ throw new Error(`embedder returned ${vectors.length} vectors for ${rows.length} messages`);
27835
+ }
27836
+ const wrong = vectors.find((vector) => vector.length !== identity.dimension);
27837
+ if (wrong !== undefined) {
27838
+ throw new Error(`embedder returned a ${wrong.length}-dimension vector, expected ${identity.dimension}`);
27839
+ }
27840
+ return vectors;
27752
27841
  };
27753
- if (signal?.aborted)
27754
- abort();
27755
- signal?.addEventListener("abort", abort, { once: true });
27756
- child.stdout.setEncoding("utf8");
27757
- child.stderr.setEncoding("utf8");
27758
- child.stdout.on("data", (chunk) => {
27759
- stdout = appendOutput(stdout, chunk, max);
27760
- if (stdout.exceeded && reason === undefined) {
27761
- reason = "stdout-too-large";
27762
- terminate4(child);
27842
+ let embedded = 0;
27843
+ let skipped = 0;
27844
+ for (let offset = 0;offset < target; offset += batchSize) {
27845
+ const rows = [...this.hydrate(pending.slice(offset, Math.min(offset + batchSize, target))).values()];
27846
+ if (rows.length === 0)
27847
+ continue;
27848
+ try {
27849
+ this.store(rows, await embed(rows));
27850
+ embedded += rows.length;
27851
+ } catch (batchError) {
27852
+ const rejected = [];
27853
+ let lastError = batchError;
27854
+ for (const row of rows) {
27855
+ try {
27856
+ this.store([row], await embed([row]));
27857
+ embedded += 1;
27858
+ } catch (error) {
27859
+ rejected.push(row);
27860
+ lastError = error;
27861
+ }
27862
+ }
27863
+ if (rejected.length === 0)
27864
+ continue;
27865
+ if (embedded === 0)
27866
+ return { ok: false, message: errorMessage2(lastError) };
27867
+ this.store(rejected, undefined);
27868
+ skipped += rejected.length;
27763
27869
  }
27870
+ }
27871
+ return { ok: true, embedded, skipped, pruned, remaining: pending.length - embedded - skipped };
27872
+ }
27873
+ adoptIdentity(identity) {
27874
+ const serialized = JSON.stringify(identity);
27875
+ const current = this.db.prepare("select v from sidecar_meta where k = ?").get(IDENTITY_KEY);
27876
+ if (current?.v === serialized)
27877
+ return;
27878
+ this.transaction(() => {
27879
+ this.db.exec("delete from sidecar_vec");
27880
+ this.db.prepare("insert or replace into sidecar_meta (k, v) values (?, ?)").run(IDENTITY_KEY, serialized);
27764
27881
  });
27765
- child.stderr.on("data", (chunk) => {
27766
- stderr = appendOutput(stderr, chunk, max);
27767
- if (stderr.exceeded && reason === undefined) {
27768
- reason = "stderr-too-large";
27769
- terminate4(child);
27882
+ this.lastSemantic = undefined;
27883
+ }
27884
+ prune() {
27885
+ const { descriptor } = this.options;
27886
+ const result = this.db.prepare(`delete from sidecar_vec where message_key not in (
27887
+ select ${descriptor.keyExpression} from ${descriptor.fromClause} where ${this.where}
27888
+ )`).run(...this.whereParams());
27889
+ if (Number(result.changes) > 0)
27890
+ this.lastSemantic = undefined;
27891
+ return Number(result.changes);
27892
+ }
27893
+ pendingKeys() {
27894
+ const { descriptor } = this.options;
27895
+ const rows = this.db.prepare(`select ${this.select}, v.hash as stored_hash
27896
+ from ${descriptor.fromClause}
27897
+ left join sidecar_vec v on v.message_key = ${descriptor.keyExpression}
27898
+ where ${this.where}`).iterate(...this.whereParams());
27899
+ const pending = [];
27900
+ for (const row of rows) {
27901
+ if (row.stored_hash !== this.toRow(row).hash)
27902
+ pending.push(String(row.k));
27903
+ }
27904
+ return pending;
27905
+ }
27906
+ hydrate(keys) {
27907
+ const { descriptor } = this.options;
27908
+ const out = new Map;
27909
+ if (keys.length === 0)
27910
+ return out;
27911
+ const rows = this.db.prepare(`select ${this.select} from ${descriptor.fromClause}
27912
+ where ${descriptor.keyExpression} in (${keys.map(() => "?").join(", ")}) and ${this.where}`).all(...keys, ...this.whereParams());
27913
+ const byKey = new Map(rows.map((row) => [String(row.k), this.toRow(row)]));
27914
+ for (const key of keys) {
27915
+ const row = byKey.get(key);
27916
+ if (row !== undefined)
27917
+ out.set(key, row);
27918
+ }
27919
+ return out;
27920
+ }
27921
+ store(rows, vectors) {
27922
+ const upsert = this.db.prepare("insert or replace into sidecar_vec (message_key, hash, vec) values (?, ?, ?)");
27923
+ this.transaction(() => {
27924
+ for (const [index, row] of rows.entries()) {
27925
+ upsert.run(row.key, row.hash, encodeVector(vectors?.[index] ?? []));
27770
27926
  }
27771
27927
  });
27772
- child.on("error", (error) => {
27773
- if (settled)
27774
- return;
27775
- settled = true;
27776
- clearTimeout(timer);
27777
- signal?.removeEventListener("abort", abort);
27778
- resolveResult({
27779
- ok: false,
27780
- reason: error.code === "ENOENT" ? "binary-missing" : "spawn-error",
27781
- stdout: stdout.text,
27782
- stderr: "",
27783
- code: null
27784
- });
27785
- });
27786
- child.on("close", (code) => {
27787
- if (settled)
27788
- return;
27789
- settled = true;
27790
- clearTimeout(timer);
27791
- signal?.removeEventListener("abort", abort);
27792
- resolveResult({
27793
- ok: code === 0 && reason === undefined,
27794
- ...reason === undefined ? code === 0 ? {} : { reason: "nonzero-exit" } : { reason },
27795
- stdout: stdout.text,
27796
- stderr: stderr.text,
27797
- code
27798
- });
27799
- });
27800
- });
27801
- }
27802
- function terminate4(child) {
27803
- if (child.killed)
27804
- return;
27805
- if (process.platform !== "win32" && child.pid !== undefined) {
27928
+ this.lastSemantic = undefined;
27929
+ }
27930
+ *storedVectors() {
27931
+ const rows = this.db.prepare("select message_key, vec from sidecar_vec where length(vec) > 0").iterate();
27932
+ for (const row of rows) {
27933
+ const vector = row.vec instanceof Uint8Array ? decodeVector(row.vec) : undefined;
27934
+ if (vector !== undefined)
27935
+ yield { key: String(row.message_key), vector };
27936
+ }
27937
+ }
27938
+ transaction(work) {
27939
+ this.db.exec("begin immediate");
27806
27940
  try {
27807
- process.kill(-child.pid, "SIGTERM");
27808
- return;
27809
- } catch {}
27941
+ work();
27942
+ this.db.exec("commit");
27943
+ } catch (error) {
27944
+ this.db.exec("rollback");
27945
+ throw error;
27946
+ }
27810
27947
  }
27811
- child.kill("SIGTERM");
27812
- }
27813
- function parseJson3(text) {
27814
- try {
27815
- return JSON.parse(text.trim());
27816
- } catch {
27817
- return;
27948
+ toRow(row) {
27949
+ const metadata = {};
27950
+ for (const [index, column] of this.options.descriptor.metadata.entries()) {
27951
+ const value = row[`m${index}`];
27952
+ if (typeof value === "string" && value.length > 0 || typeof value === "number")
27953
+ metadata[column.key] = String(value);
27954
+ }
27955
+ const title = typeof row.title === "string" && row.title.length > 0 ? row.title : undefined;
27956
+ const epoch = typeof row.epoch === "number" && Number.isFinite(row.epoch) ? row.epoch : undefined;
27957
+ const content = String(row.content ?? "");
27958
+ const input = `${title ?? ""}
27959
+ ${content}`.slice(0, MAX_EMBED_INPUT_CHARS);
27960
+ return {
27961
+ key: String(row.k),
27962
+ content,
27963
+ title,
27964
+ epoch,
27965
+ metadata,
27966
+ input,
27967
+ hash: createHash14("sha1").update(input).digest("hex")
27968
+ };
27969
+ }
27970
+ toHit(row, score) {
27971
+ return {
27972
+ key: row.key,
27973
+ content: row.content,
27974
+ score,
27975
+ ...row.title !== undefined ? { title: row.title } : {},
27976
+ ...row.epoch !== undefined ? { publishedAt: Math.round(row.epoch * 1000) } : {},
27977
+ metadata: row.metadata
27978
+ };
27818
27979
  }
27819
27980
  }
27820
- function parseObject2(text) {
27821
- const value = parseJson3(text);
27822
- return value !== null && typeof value === "object" && !Array.isArray(value) ? value : undefined;
27981
+ function toMatchQuery(text) {
27982
+ const terms = [...new Set(text.match(/[\p{L}\p{N}_]+/gu) ?? [])];
27983
+ return terms.length === 0 ? undefined : terms.map((term) => `"${term}"`).join(" OR ");
27823
27984
  }
27824
- function objectArray(value, keys) {
27825
- if (value === null || typeof value !== "object" || Array.isArray(value))
27826
- return;
27827
- for (const key of keys)
27828
- if (Array.isArray(value[key]))
27829
- return value[key];
27830
- return;
27831
- }
27832
- function hasAnyNumber(value, keys) {
27833
- return keys.some((key) => number(value, key) !== undefined);
27834
- }
27835
- function normalizeHit2(value, mode, index) {
27836
- if (value === null || typeof value !== "object" || Array.isArray(value))
27837
- return;
27838
- const row = value;
27839
- const text = (key) => typeof row[key] === "string" ? row[key] : "";
27840
- const chunkId = text("chunkId") || text("chunk_id");
27841
- const messageId = text("messageId") || text("message_id");
27842
- const accountId = text("accountId") || text("account_id");
27843
- const mailbox = text("mailbox");
27844
- const snippet = text("snippet") || text("content") || text("text");
27845
- if (!isSafeOpaqueId(chunkId) || !isSafeOpaqueId(messageId) || !snippet)
27846
- return;
27847
- const to = Array.isArray(row.to) ? row.to.filter((item) => typeof item === "string") : [];
27848
- return {
27849
- chunkId,
27850
- messageId,
27851
- threadId: text("threadId") || text("thread_id"),
27852
- accountId,
27853
- mailbox,
27854
- subject: text("subject"),
27855
- from: text("from"),
27856
- to,
27857
- date: text("date"),
27858
- snippet,
27859
- score: typeof row.score === "number" ? row.score : 1 / (index + 1),
27860
- mode
27861
- };
27862
- }
27863
- function number(value, key) {
27864
- return typeof value[key] === "number" && Number.isFinite(value[key]) ? value[key] : undefined;
27865
- }
27866
- function string(value, key) {
27867
- return typeof value[key] === "string" ? value[key] : undefined;
27868
- }
27869
- function ok4(data, result) {
27870
- return { ok: true, data, stdout: result.stdout, stderr: result.stderr, code: result.code ?? 0 };
27871
- }
27872
- function failure3(result) {
27873
- return {
27874
- ok: false,
27875
- reason: result.reason ?? "nonzero-exit",
27876
- stdout: result.stdout,
27877
- stderr: boundDiagnosticText6(result.stderr),
27878
- code: result.code
27879
- };
27880
- }
27881
- function boundDiagnosticText6(value) {
27882
- if (value.length === 0)
27883
- return "";
27884
- return value.trim().slice(0, 4000);
27885
- }
27886
- function appendOutput(current, chunk, maxBytes) {
27887
- if (current.exceeded)
27888
- return current;
27889
- const bytes = Buffer.from(chunk, "utf8");
27890
- const remaining = Math.max(0, maxBytes - current.bytes);
27891
- const prefix = truncateUtf8(chunk, remaining);
27892
- const total = current.bytes + bytes.byteLength;
27893
- return {
27894
- text: current.text + prefix,
27895
- bytes: Math.min(total, maxBytes),
27896
- exceeded: total > maxBytes
27897
- };
27985
+ function errorMessage2(error) {
27986
+ return error instanceof Error ? error.message : String(error);
27898
27987
  }
27899
- function isSafeOpaqueId(value) {
27900
- return value.length > 0 && value !== "." && value !== ".." && !/[\\/#\u0000-\u001f\u007f]/u.test(value);
27988
+ 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";
27989
+ var init_sidecar = () => {};
27990
+
27991
+ // src/datasource/sidecar/methods.ts
27992
+ function sidecarMethodName(datasourceId, mode) {
27993
+ return mode === "bm25" ? `${datasourceId}-sidecar-bm25` : `${datasourceId}-${mode}`;
27901
27994
  }
27902
- function findRemoteEmbeddingViolation(env) {
27903
- for (const [key, value] of Object.entries(env)) {
27904
- if (value === undefined)
27905
- continue;
27906
- const lower = key.toLowerCase();
27907
- if (REMOTE_EMBEDDING_ENV_KEYS.has(lower))
27908
- return key;
27909
- if (lower === "mailcrawl_embedder" || lower === "mailcrawl_embed_url") {
27910
- if (isRemoteHttpEndpoint(value))
27911
- return key;
27912
- }
27995
+
27996
+ class SidecarRetrievalMethod {
27997
+ options;
27998
+ constructor(options) {
27999
+ this.options = options;
27913
28000
  }
27914
- return;
27915
- }
27916
- function isRemoteHttpEndpoint(value) {
27917
- const trimmed = value.trim();
27918
- if (!/^https?:\/\//iu.test(trimmed))
27919
- return false;
27920
- try {
27921
- return !LOOPBACK_HOSTS2.has(new URL(trimmed).hostname);
27922
- } catch {
27923
- return true;
28001
+ describe() {
28002
+ const { datasourceId, skillType, mode, tags } = this.options;
28003
+ const { type, text, capabilities } = DESCRIPTIONS[mode];
28004
+ return {
28005
+ name: sidecarMethodName(datasourceId, mode),
28006
+ type,
28007
+ description: `${text} over ${skillType} via a read-only sidecar index`,
28008
+ status: "active",
28009
+ capabilities: [...capabilities, "scoped", "path-opaque-sources"],
28010
+ datasourceId,
28011
+ tags
28012
+ };
27924
28013
  }
27925
- }
27926
- function truncateUtf8(value, maxBytes) {
27927
- if (Buffer.byteLength(value) <= maxBytes)
27928
- return value;
27929
- const decoder = new TextDecoder("utf-8", { fatal: true });
27930
- const bytes = Buffer.from(value, "utf8");
27931
- for (let end = Math.min(maxBytes, bytes.length);end >= 0; end -= 1) {
27932
- try {
27933
- return decoder.decode(bytes.subarray(0, end));
27934
- } catch {}
28014
+ async retrieve(query, options) {
28015
+ const trimmed = query.trim();
28016
+ if (trimmed.length === 0)
28017
+ return [];
28018
+ const topK = options.topK ?? DEFAULT_TOP_K5;
28019
+ const { sidecar, mode } = this.options;
28020
+ const hits = mode === "bm25" ? sidecar.searchLexical(trimmed, topK) : mode === "semantic" ? await sidecar.searchSemantic(trimmed, topK) : await sidecar.searchHybrid(trimmed, topK);
28021
+ const results = [];
28022
+ for (const hit of hits) {
28023
+ const result = this.toResult(hit, options);
28024
+ if (result !== undefined)
28025
+ results.push(result);
28026
+ }
28027
+ return results;
28028
+ }
28029
+ toResult(hit, options) {
28030
+ const { descriptor, datasourceId, instanceId, mode } = this.options;
28031
+ const chunkId = descriptor.chunkId(hit.key, hit.metadata);
28032
+ const source = datasourceSourcePath(datasourceId, instanceId, chunkId);
28033
+ if (!matchesDatasourceScope(source, options.scope))
28034
+ return;
28035
+ const method = sidecarMethodName(datasourceId, mode);
28036
+ return {
28037
+ id: `${datasourceId}:${instanceId}:${chunkId}`,
28038
+ content: hit.content,
28039
+ source,
28040
+ score: hit.score,
28041
+ metadata: {
28042
+ ...hit.metadata,
28043
+ method,
28044
+ datasourceId,
28045
+ instanceId,
28046
+ backend: "sidecar",
28047
+ ...hit.title !== undefined ? { title: hit.title } : {},
28048
+ hierarchy: descriptor.hierarchy(hit.metadata).join("/"),
28049
+ ...hit.publishedAt !== undefined ? { publishedAt: hit.publishedAt } : {}
28050
+ }
28051
+ };
27935
28052
  }
27936
- return "";
27937
28053
  }
27938
- 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;
27939
- var init_client9 = __esm(() => {
27940
- init_portable_spawn();
27941
- init_config3();
27942
- LOOPBACK_HOSTS2 = new Set(["127.0.0.1", "localhost", "[::1]", "::1"]);
27943
- SAFE_ENV_KEYS2 = new Set(["HOME", "LANG", "LC_ALL", "PATH", "TMPDIR", "TMP", "TEMP", "NO_COLOR"]);
27944
- SAFE_MAILCRAWL_ENV_KEYS = new Set([
27945
- "MAILCRAWL_DATA_DIR",
27946
- "MAILCRAWL_EMBEDDER",
27947
- "MAILCRAWL_EMBEDDER_PROVIDER",
27948
- "MAILCRAWL_EMBED_URL",
27949
- "MAILCRAWL_EMBED_MODEL",
27950
- "MAILCRAWL_EMBED_DIM",
27951
- "MAILCRAWL_EMBED_TIMEOUT",
27952
- "MAILCRAWL_EMBED_BATCH_SIZE",
27953
- "MAILCRAWL_QUERY_PREFIX",
27954
- "MAILCRAWL_PASSAGE_PREFIX",
27955
- "MAILCRAWL_NATIVE_MODEL",
27956
- "MAILCRAWL_NATIVE_DEVICE",
27957
- "MAILCRAWL_NATIVE_DTYPE",
27958
- "MAILCRAWL_KIWI_MODEL",
27959
- "MAILCRAWL_JA_HELPER",
27960
- "MAILCRAWL_ZH_HELPER"
27961
- ]);
27962
- REMOTE_EMBEDDING_ENV_KEYS = new Set([
27963
- "mailcrawl_embedder_base_url",
27964
- "mailcrawl_embedding_base_url",
27965
- "mailcrawl_allow_remote_embeddings"
27966
- ]);
28054
+ var DEFAULT_TOP_K5 = 20, DESCRIPTIONS;
28055
+ var init_methods4 = __esm(() => {
28056
+ init_scope2();
28057
+ DESCRIPTIONS = {
28058
+ bm25: { type: "bm25", text: "BM25-ranked FTS5 retrieval", capabilities: ["lexical", "fts5", "bm25"] },
28059
+ semantic: { type: "vector", text: "Semantic (embedding) retrieval", capabilities: ["semantic"] },
28060
+ hybrid: {
28061
+ type: "hybrid",
28062
+ text: "Hybrid BM25 + semantic retrieval (reciprocal rank fusion)",
28063
+ capabilities: ["lexical", "semantic", "hybrid"]
28064
+ }
28065
+ };
27967
28066
  });
27968
28067
 
27969
- // src/datasource/skills/mailcrawl/methods.ts
27970
- class MailcrawlMethod {
28068
+ // src/datasource/skills/lazykatok/skill.ts
28069
+ class LazykatokSkill {
27971
28070
  client;
27972
- mode;
27973
28071
  instanceId;
28072
+ instances;
28073
+ pollingIntervalMs;
27974
28074
  tags;
27975
- account;
27976
- mailbox;
27977
- constructor(client, mode, instanceId, tags, account, mailbox) {
27978
- this.client = client;
27979
- this.mode = mode;
27980
- this.instanceId = instanceId;
27981
- this.tags = tags;
27982
- this.account = account;
27983
- this.mailbox = mailbox;
28075
+ sidecar;
28076
+ lastIndexedAt;
28077
+ constructor(options) {
28078
+ this.client = options.client;
28079
+ this.instanceId = options.instanceId ?? DEFAULT_INSTANCE_ID4;
28080
+ this.instances = options.instances !== undefined && options.instances.length > 0 ? options.instances : [this.instanceId];
28081
+ this.pollingIntervalMs = options.pollingIntervalMs ?? DEFAULT_POLLING_INTERVAL_MS3;
28082
+ this.tags = options.tags ?? DEFAULT_KAKAO_TAGS2;
28083
+ this.lastIndexedAt = options.lastIndexedAt;
28084
+ this.sidecar = options.sidecar === undefined ? undefined : new CrawlerSidecar({
28085
+ ...options.sidecar,
28086
+ channelIds: options.channelIds ?? options.sidecar.channelIds ?? [],
28087
+ channelNames: options.channelNames ?? options.sidecar.channelNames ?? []
28088
+ });
27984
28089
  }
27985
28090
  describe() {
27986
28091
  return {
27987
- name: `mailcrawl-${this.mode}`,
27988
- type: this.mode === "bm25" ? "bm25" : this.mode === "semantic" ? "vector" : "hybrid",
27989
- description: `${this.mode} retrieval over archived email via the external mailcrawl CLI`,
28092
+ name: KAKAO_DATASOURCE_ID2,
28093
+ id: KAKAO_DATASOURCE_ID2,
28094
+ type: KAKAO_SKILL_TYPE,
28095
+ description: "KakaoTalk datasource via the external lazykatok CLI",
28096
+ capabilities: ["chat", "external-cli", "polling", "bm25", "semantic"],
28097
+ tags: this.tags,
27990
28098
  status: "active",
27991
- capabilities: ["email", this.mode, "scoped", "external-cli", "path-opaque-sources"],
27992
- datasourceId: "mailcrawl",
27993
- tags: this.tags
27994
- };
27995
- }
27996
- async retrieve(query, options) {
27997
- if (!query.trim())
27998
- return [];
27999
- const result = await this.client.search(this.mode, query, {
28000
- topK: options.topK,
28001
- signal: options.signal
28002
- });
28003
- if (!result.ok)
28004
- throw datasourceCliError("mailcrawl", `search --mode ${this.mode}`, result);
28005
- return result.hits.filter((hit) => {
28006
- if (this.account !== undefined && hit.accountId !== this.account)
28007
- return false;
28008
- if (this.mailbox !== undefined && hit.mailbox !== this.mailbox)
28009
- return false;
28010
- const source = datasourceSourcePath("mailcrawl", this.instanceId, hit.chunkId);
28011
- return matchesDatasourceScope(source, options.scope);
28012
- }).map((hit) => {
28013
- const source = datasourceSourcePath("mailcrawl", this.instanceId, hit.chunkId);
28014
- return {
28015
- id: `mailcrawl:${this.instanceId}:${hit.chunkId}`,
28016
- content: hit.snippet,
28017
- source,
28018
- score: hit.score,
28019
- metadata: {
28020
- method: `mailcrawl-${this.mode}`,
28021
- datasourceId: "mailcrawl",
28022
- instanceId: this.instanceId,
28023
- mode: this.mode,
28024
- chunkId: hit.chunkId,
28025
- messageId: hit.messageId,
28026
- threadId: hit.threadId,
28027
- accountId: hit.accountId,
28028
- mailbox: hit.mailbox,
28029
- subject: hit.subject,
28030
- from: hit.from,
28031
- to: hit.to,
28032
- date: hit.date
28033
- }
28034
- };
28035
- }).slice(0, options.topK ?? 20);
28036
- }
28037
- }
28038
- var modes, MAILCRAWL_MODES;
28039
- var init_methods4 = __esm(() => {
28040
- init_errors4();
28041
- init_scope2();
28042
- modes = ["bm25", "semantic", "hybrid"];
28043
- MAILCRAWL_MODES = modes;
28044
- });
28045
-
28046
- // src/datasource/skills/mailcrawl/skill.ts
28047
- class MailcrawlSkill {
28048
- client;
28049
- instanceId;
28050
- instances;
28051
- interval;
28052
- tags;
28053
- account;
28054
- mailbox;
28055
- lastIndexedAt;
28056
- constructor(options = {}) {
28057
- const connectorOptions = options.connectorOptions ?? options;
28058
- this.instanceId = options.instanceId ?? "default";
28059
- this.instances = options.instances?.length ? options.instances : [this.instanceId];
28060
- validateMailcrawlInstanceId(this.instanceId);
28061
- for (const instance of this.instances)
28062
- validateMailcrawlInstanceId(instance);
28063
- this.interval = options.pollingIntervalMs ?? 15 * 60 * 1000;
28064
- this.tags = options.tags ?? TAGS;
28065
- this.account = connectorOptions.account;
28066
- this.mailbox = connectorOptions.mailbox;
28067
- this.client = options.client ?? new MailcrawlClient({ ...connectorOptions, instanceId: this.instanceId });
28068
- }
28069
- describe() {
28070
- return {
28071
- name: "mailcrawl",
28072
- id: "mailcrawl",
28073
- type: "mailcrawl-archive",
28074
- description: "Local email archive datasource via the external mailcrawl CLI",
28075
- capabilities: ["email", "external-cli", "polling", "incremental", "bm25", "semantic", "hybrid"],
28076
- tags: this.tags,
28077
- status: "active",
28078
- requiresExternalCli: true,
28079
- datasourceId: "mailcrawl",
28080
- instanceId: this.instanceId,
28081
- instances: this.instances
28099
+ requiresExternalCli: true,
28100
+ datasourceId: KAKAO_DATASOURCE_ID2,
28101
+ instanceId: this.instanceId,
28102
+ instances: this.instances
28082
28103
  };
28083
28104
  }
28084
28105
  polling() {
28085
28106
  return {
28086
- mode: this.interval > 0 ? "poll" : "none",
28087
- ...this.interval > 0 ? { intervalMs: this.interval } : {},
28107
+ mode: "poll",
28108
+ intervalMs: this.pollingIntervalMs,
28088
28109
  lastIndexedAt: this.lastIndexedAt
28089
28110
  };
28090
28111
  }
28091
28112
  async index(options) {
28092
28113
  try {
28093
- const result = await this.client.sync(options?.signal);
28094
- if (!result.ok) {
28095
- 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);
28096
- }
28097
- const indexed = await this.client.index(options?.signal);
28098
- const diagnostics = [];
28099
- if (!indexed.ok) {
28100
- diagnostics.push({
28101
- code: indexed.reason === "remote-embedding-rejected" ? "datasource-embedding-egress-rejected" : "datasource-index-failed",
28102
- severity: "warning",
28103
- message: `mailcrawl semantic index unavailable (${indexed.reason}); BM25 remains available`,
28114
+ const doctor = await this.client.doctor(options?.signal);
28115
+ if (!doctor.ok)
28116
+ return this.fail("datasource-unavailable", doctor);
28117
+ const sync = await this.client.sync(options?.signal);
28118
+ if (!sync.ok)
28119
+ return this.fail("datasource-index-failed", sync);
28120
+ if (this.sidecar !== undefined) {
28121
+ this.lastIndexedAt = Date.now();
28122
+ return {
28123
+ ok: true,
28104
28124
  instanceId: this.instanceId,
28105
- source: "mailcrawl"
28106
- });
28125
+ skill: KAKAO_DATASOURCE_ID2,
28126
+ chunkCount: syncChunkCount(sync),
28127
+ indexedAt: this.lastIndexedAt,
28128
+ diagnostics: await this.syncSidecar()
28129
+ };
28107
28130
  }
28131
+ const indexResult = await this.client.index(options?.signal);
28132
+ if (!indexResult.ok)
28133
+ return this.fail("datasource-index-failed", indexResult);
28108
28134
  this.lastIndexedAt = Date.now();
28135
+ 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;
28109
28136
  return {
28110
28137
  ok: true,
28111
28138
  instanceId: this.instanceId,
28112
- skill: "mailcrawl",
28113
- chunkCount: result.data.chunksAdded ?? result.data.messages,
28139
+ skill: KAKAO_DATASOURCE_ID2,
28140
+ chunkCount,
28114
28141
  indexedAt: this.lastIndexedAt,
28115
- diagnostics
28142
+ diagnostics: []
28116
28143
  };
28117
- } catch {
28118
- return this.fail("datasource-unavailable", "mailcrawl sync failed unexpectedly");
28144
+ } catch (error) {
28145
+ return this.fail("datasource-unavailable", {
28146
+ ok: false,
28147
+ reason: "spawn-error",
28148
+ stdout: "",
28149
+ stderr: describeRetrievalError(error),
28150
+ code: null
28151
+ });
28119
28152
  }
28120
28153
  }
28154
+ close() {
28155
+ this.sidecar?.close();
28156
+ }
28121
28157
  retrievalMethods() {
28158
+ const cliBm25 = new LazykatokBm25Method({ client: this.client, instanceId: this.instanceId, tags: this.tags });
28159
+ const { sidecar } = this;
28160
+ if (sidecar === undefined) {
28161
+ return [
28162
+ cliBm25,
28163
+ new LazykatokSemanticMethod({ client: this.client, instanceId: this.instanceId, tags: this.tags })
28164
+ ];
28165
+ }
28166
+ const shared = {
28167
+ sidecar,
28168
+ descriptor: sidecar.descriptor,
28169
+ datasourceId: KAKAO_DATASOURCE_ID2,
28170
+ skillType: KAKAO_SKILL_TYPE,
28171
+ instanceId: this.instanceId,
28172
+ tags: this.tags
28173
+ };
28174
+ const bm25 = new SidecarRetrievalMethod({ ...shared, mode: "bm25" });
28175
+ if (!sidecar.semanticEnabled)
28176
+ return [cliBm25, bm25];
28122
28177
  return [
28123
- new MailcrawlMethod(this.client, "bm25", this.instanceId, this.tags, this.account, this.mailbox),
28124
- new MailcrawlMethod(this.client, "semantic", this.instanceId, this.tags, this.account, this.mailbox),
28125
- new MailcrawlMethod(this.client, "hybrid", this.instanceId, this.tags, this.account, this.mailbox)
28178
+ cliBm25,
28179
+ bm25,
28180
+ new SidecarRetrievalMethod({ ...shared, mode: "semantic" }),
28181
+ new SidecarRetrievalMethod({ ...shared, mode: "hybrid" })
28126
28182
  ];
28127
28183
  }
28184
+ async syncSidecar() {
28185
+ if (this.sidecar === undefined || !this.sidecar.semanticEnabled)
28186
+ return [];
28187
+ const base = { instanceId: this.instanceId, source: KAKAO_DATASOURCE_ID2 };
28188
+ let result;
28189
+ try {
28190
+ result = await this.sidecar.sync();
28191
+ } catch (error) {
28192
+ result = { ok: false, message: error instanceof Error ? error.message : String(error) };
28193
+ }
28194
+ if (!result.ok) {
28195
+ return [
28196
+ {
28197
+ code: "datasource-index-failed",
28198
+ severity: "warning",
28199
+ message: `semantic-unavailable: ${result.message}`.slice(0, 300),
28200
+ ...base
28201
+ }
28202
+ ];
28203
+ }
28204
+ const diagnostics = [];
28205
+ if (result.skipped > 0) {
28206
+ diagnostics.push({
28207
+ code: "datasource-index-partial",
28208
+ severity: "warning",
28209
+ message: `semantic index skipped ${result.skipped} message(s) the embedder rejected; they stay searchable by BM25 and are retried when edited`,
28210
+ ...base
28211
+ });
28212
+ }
28213
+ if (result.remaining > 0) {
28214
+ diagnostics.push({
28215
+ code: "datasource-index-partial",
28216
+ severity: "info",
28217
+ message: `semantic index catching up: ${result.embedded} embedded this refresh, ${result.remaining} remaining; hybrid search uses BM25 for the rest until a later refresh`,
28218
+ ...base
28219
+ });
28220
+ }
28221
+ return diagnostics;
28222
+ }
28128
28223
  describeSources() {
28129
28224
  return this.instances.map((instanceId) => ({
28130
- source: datasourceSourcePath("mailcrawl", instanceId),
28131
- datasourceId: "mailcrawl",
28132
- skill: "mailcrawl",
28225
+ source: datasourceSourcePath(KAKAO_DATASOURCE_ID2, instanceId),
28226
+ datasourceId: KAKAO_DATASOURCE_ID2,
28227
+ skill: KAKAO_DATASOURCE_ID2,
28133
28228
  instanceId,
28134
- contentType: "email",
28135
- metadata: { datasourceId: "mailcrawl", instanceId, tags: this.tags }
28229
+ contentType: "chat",
28230
+ metadata: {
28231
+ datasourceId: KAKAO_DATASOURCE_ID2,
28232
+ instanceId,
28233
+ tags: this.tags
28234
+ }
28136
28235
  }));
28137
28236
  }
28138
28237
  skillManifest() {
28139
- const scopes = this.instances.map((id) => `- \`${datasourceSourcePath("mailcrawl", id)}\``).join(`
28238
+ const instanceSources = this.instances.map((instanceId) => `- \`${datasourceSourcePath(KAKAO_DATASOURCE_ID2, instanceId)}\``).join(`
28140
28239
  `);
28240
+ const cadence = this.pollingIntervalMs > 0 ? `roughly every ${Math.round(this.pollingIntervalMs / 60000)} minute(s) when auto-refresh runs` : "on manual refresh only";
28141
28241
  return {
28142
- name: "datasource-mailcrawl",
28143
- description: "Search local email synchronized and indexed by mailcrawl.",
28144
- content: `# mailcrawl email datasource
28145
-
28146
- Use this skill for authorized local email retrieval. AutoRAG invokes the external \`mailcrawl\` CLI and never opens its SQLite archive directly.
28147
-
28148
- ## Indexing
28149
- Indexing is server-managed and incremental; do not trigger sync yourself.
28150
-
28151
- ## Search
28152
- Call the dedicated \`${datasourceSearchToolName("mailcrawl")}\` tool with a natural-language query. Available scopes:
28153
- ${scopes}
28154
-
28155
- The selectable retrieval modes are BM25, semantic, and hybrid. Scope arguments can only narrow trusted access. Source identifiers remain opaque and hierarchical.
28156
-
28157
- ## Native CLI
28158
- The external \`mailcrawl\` CLI owns the archive and index. When the dedicated tool cannot express what you need, call mailcrawl directly through \`bash\`:
28159
- - \`mailcrawl search --mode hybrid --limit 20 --json "<query>"\` (\`bm25\` and \`semantic\` modes also available; \`--account\` / \`--mailbox\` narrow the archive)
28160
- Never pass datasource virtual paths (\`/mailcrawl/...\`) to bash; they are not OS paths.`
28161
- };
28162
- }
28163
- fail(code, message) {
28164
- return {
28165
- ok: false,
28166
- instanceId: this.instanceId,
28167
- skill: "mailcrawl",
28168
- indexedAt: Date.now(),
28169
- diagnostics: [
28170
- {
28171
- code,
28172
- severity: code === "datasource-unavailable" ? "warning" : "error",
28173
- message: `mailcrawl ${message}`,
28174
- instanceId: this.instanceId,
28175
- source: "mailcrawl"
28176
- }
28177
- ],
28178
- error: message,
28179
- code,
28180
- message
28181
- };
28182
- }
28183
- }
28184
- var TAGS;
28185
- var init_skill9 = __esm(() => {
28186
- init_scope2();
28187
- init_client9();
28188
- init_config3();
28242
+ name: `datasource-${KAKAO_DATASOURCE_ID2}`,
28243
+ description: "Search indexed KakaoTalk chat history (messages, senders, room context). Use for questions about KakaoTalk conversations, decisions made in chats, or who said what.",
28244
+ content: [
28245
+ `# KakaoTalk datasource (${KAKAO_SKILL_TYPE})`,
28246
+ "",
28247
+ "This skill searches KakaoTalk chats that are indexed through the external `lazykatok` CLI. AutoRAG never reads KakaoTalk databases directly.",
28248
+ "",
28249
+ "## When to use",
28250
+ "Use this skill when the question is about KakaoTalk conversations, chat participants, or content shared inside chats.",
28251
+ "",
28252
+ "## Indexing",
28253
+ `Indexing is server-managed and refreshed ${cadence}. You do not trigger indexing; just search.`,
28254
+ "",
28255
+ "## How to search",
28256
+ `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:`,
28257
+ instanceSources.length > 0 ? instanceSources : "- (no configured instances)",
28258
+ "",
28259
+ "Chat/channel filtering is owned by lazykatok.",
28260
+ "",
28261
+ "## Native CLI",
28262
+ "The external `lazykatok` CLI owns the archive, index, and credentials. When the dedicated tool cannot express what you need, call lazykatok directly through `bash`:",
28263
+ '- `lazykatok search bm25 "<query>" --json --limit 20` — lexical search (`semantic` mode also available)',
28264
+ "- `lazykatok chunk get <chunkId> --json` — fetch one chunk by id (chunk ids appear in result metadata)",
28265
+ "- `lazykatok chunk context <chunkId> --json` — surrounding messages of a chunk",
28266
+ "Never pass datasource virtual paths (`/kakao/...`) to bash; they are not OS paths.",
28267
+ "",
28268
+ "## Output rules",
28269
+ "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."
28270
+ ].join(`
28271
+ `)
28272
+ };
28273
+ }
28274
+ fail(code, result) {
28275
+ const base = result.stderr.length > 0 ? `${result.reason}: ${result.stderr.trim().slice(0, 4000)}` : result.reason;
28276
+ const message = code === "datasource-index-failed" && result.reason === "timeout" ? `${base} — ${connectorSyncTimeoutHint()}` : base;
28277
+ const diagnostic = {
28278
+ code,
28279
+ severity: code === "datasource-unavailable" ? "warning" : "error",
28280
+ message,
28281
+ instanceId: this.instanceId,
28282
+ source: KAKAO_DATASOURCE_ID2
28283
+ };
28284
+ return {
28285
+ ok: false,
28286
+ instanceId: this.instanceId,
28287
+ skill: KAKAO_DATASOURCE_ID2,
28288
+ indexedAt: Date.now(),
28289
+ diagnostics: [diagnostic],
28290
+ error: result.reason,
28291
+ code,
28292
+ message
28293
+ };
28294
+ }
28295
+ }
28296
+ function syncChunkCount(sync) {
28297
+ if (!("data" in sync) || typeof sync.data !== "object" || sync.data === null)
28298
+ return 0;
28299
+ const meta = "metadata" in sync.data && typeof sync.data.metadata === "object" && sync.data.metadata !== null ? sync.data.metadata : undefined;
28300
+ if (typeof meta?.chunks === "number")
28301
+ return meta.chunks;
28302
+ if ("messageCount" in sync.data && typeof sync.data.messageCount === "number")
28303
+ return sync.data.messageCount;
28304
+ return 0;
28305
+ }
28306
+ var KAKAO_DATASOURCE_ID2 = "kakao", KAKAO_SKILL_TYPE = "kakaotalk", DEFAULT_INSTANCE_ID4 = "default", DEFAULT_POLLING_INTERVAL_MS3, DEFAULT_KAKAO_TAGS2;
28307
+ var init_skill7 = __esm(() => {
28308
+ init_skip();
28309
+ init_scope2();
28310
+ init_sidecar();
28189
28311
  init_methods4();
28190
- TAGS = ["mailcrawl", "email", "pii"];
28312
+ init_methods3();
28313
+ DEFAULT_POLLING_INTERVAL_MS3 = 15 * 60 * 1000;
28314
+ DEFAULT_KAKAO_TAGS2 = ["kakaotalk", "personal", "pii"];
28191
28315
  });
28192
28316
 
28193
- // src/datasource/skills/mailcrawl/index.ts
28194
- var init_mailcrawl = __esm(() => {
28195
- init_client9();
28196
- init_methods4();
28197
- init_skill9();
28317
+ // src/datasource/skills/lazykatok/index.ts
28318
+ var init_lazykatok = __esm(() => {
28319
+ init_client8();
28320
+ init_methods3();
28321
+ init_skill7();
28322
+ init_types5();
28198
28323
  });
28199
28324
 
28200
- // src/datasource/sidecar/fusion.ts
28201
- function encodeVector(vector) {
28202
- const floats = Float32Array.from(vector);
28203
- return new Uint8Array(floats.buffer, floats.byteOffset, floats.byteLength);
28204
- }
28205
- function decodeVector(blob) {
28206
- if (blob.byteLength % Float32Array.BYTES_PER_ELEMENT !== 0)
28207
- return;
28208
- return new Float32Array(new Uint8Array(blob).buffer);
28209
- }
28210
- function cosine3(a, b) {
28211
- if (a.length === 0 || a.length !== b.length)
28212
- return 0;
28213
- let dot = 0;
28214
- let normA = 0;
28215
- let normB = 0;
28216
- for (let index = 0;index < a.length; index += 1) {
28217
- const x = a[index] ?? 0;
28218
- const y = b[index] ?? 0;
28219
- dot += x * y;
28220
- normA += x * x;
28221
- normB += y * y;
28325
+ // src/datasource/skills/mail-export/connector.ts
28326
+ import { readdir as readdir4, readFile as readFile5, stat as stat6 } from "node:fs/promises";
28327
+ import { basename as basename12, dirname as dirname25, extname as extname6, join as join37 } from "node:path";
28328
+ import { simpleParser as simpleParser2 } from "mailparser";
28329
+
28330
+ class MailExportConnector {
28331
+ options;
28332
+ constructor(options = {}) {
28333
+ this.options = options;
28222
28334
  }
28223
- if (normA === 0 || normB === 0)
28224
- return 0;
28225
- return dot / Math.sqrt(normA * normB);
28226
- }
28227
- function topKByCosine(query, rows, k) {
28228
- if (k <= 0)
28229
- return [];
28230
- const scored = [];
28231
- for (const row of rows) {
28232
- const score = cosine3(query, row.vector);
28233
- if (score > 0)
28234
- scored.push({ key: row.key, score });
28335
+ async fetch() {
28336
+ const paths = this.options.paths ?? [];
28337
+ if (paths.length === 0)
28338
+ return { ok: false, reason: "not-configured", message: "no mail export paths configured" };
28339
+ const maxDocuments = this.options.maxDocuments ?? DEFAULT_MAX_DOCUMENTS3;
28340
+ const maxBytes = this.options.maxBytesPerFile ?? DEFAULT_MAX_BYTES_PER_FILE2;
28341
+ const files = [];
28342
+ let missingPaths = 0;
28343
+ for (const path of paths) {
28344
+ try {
28345
+ const info = await stat6(path);
28346
+ if (info.isDirectory()) {
28347
+ const entries = await readdir4(path, { recursive: true, withFileTypes: true });
28348
+ for (const entry of entries) {
28349
+ if (!entry.isFile())
28350
+ continue;
28351
+ const extension = extname6(entry.name).toLowerCase();
28352
+ if (extension === ".eml" || extension === ".mbox") {
28353
+ files.push(join37(entry.parentPath, entry.name));
28354
+ }
28355
+ }
28356
+ } else if (info.isFile()) {
28357
+ files.push(path);
28358
+ }
28359
+ } catch {
28360
+ missingPaths += 1;
28361
+ }
28362
+ }
28363
+ if (missingPaths === paths.length) {
28364
+ return { ok: false, reason: "unavailable", message: "no configured mail export paths were readable" };
28365
+ }
28366
+ const documents = [];
28367
+ const warnings = [];
28368
+ if (missingPaths > 0)
28369
+ warnings.push(`${missingPaths} configured path(s) were unreadable`);
28370
+ let parseFailures = 0;
28371
+ let skippedLarge = 0;
28372
+ for (const file of files.sort()) {
28373
+ if (documents.length >= maxDocuments)
28374
+ break;
28375
+ let bytes;
28376
+ try {
28377
+ const info = await stat6(file);
28378
+ if (info.size > maxBytes) {
28379
+ skippedLarge += 1;
28380
+ continue;
28381
+ }
28382
+ bytes = await readFile5(file);
28383
+ } catch {
28384
+ parseFailures += 1;
28385
+ continue;
28386
+ }
28387
+ const stem = basename12(file, extname6(file));
28388
+ const mailboxDir = basename12(dirname25(file));
28389
+ const hierarchy = ["mailboxes", mailboxDir, stem];
28390
+ const extension = extname6(file).toLowerCase();
28391
+ const rawMessages = extension === ".mbox" ? splitMbox(bytes.toString("utf8")) : [bytes.toString("utf8")];
28392
+ for (const [index, raw] of rawMessages.entries()) {
28393
+ if (documents.length >= maxDocuments)
28394
+ break;
28395
+ try {
28396
+ const parsed = await simpleParser2(raw);
28397
+ const subject = parsed.subject ?? "(no subject)";
28398
+ const from = Array.isArray(parsed.from) ? parsed.from[0]?.text : parsed.from?.text;
28399
+ const to = Array.isArray(parsed.to) ? parsed.to[0]?.text : parsed.to?.text;
28400
+ const body = (parsed.text ?? "").trim().slice(0, MAX_BODY_CHARS);
28401
+ const headerBlock = [
28402
+ `Subject: ${subject}`,
28403
+ ...from !== undefined ? [`From: ${from}`] : [],
28404
+ ...to !== undefined ? [`To: ${to}`] : [],
28405
+ ...parsed.date !== undefined ? [`Date: ${parsed.date.toISOString()}`] : []
28406
+ ].join(`
28407
+ `);
28408
+ const content = `${headerBlock}
28409
+
28410
+ ${body}`.trim();
28411
+ if (content.length === 0)
28412
+ continue;
28413
+ documents.push({
28414
+ docId: extension === ".mbox" ? `${stem}-${index + 1}` : stem,
28415
+ hierarchy,
28416
+ title: subject,
28417
+ content,
28418
+ ...parsed.date !== undefined ? { publishedAt: parsed.date.getTime() } : {},
28419
+ metadata: {
28420
+ path: file,
28421
+ subject,
28422
+ ...from !== undefined ? { from } : {},
28423
+ messageIndex: index
28424
+ }
28425
+ });
28426
+ } catch {
28427
+ parseFailures += 1;
28428
+ }
28429
+ }
28430
+ }
28431
+ if (skippedLarge > 0)
28432
+ warnings.push(`${skippedLarge} file(s) exceeded the size limit and were skipped`);
28433
+ if (parseFailures > 0)
28434
+ warnings.push(`${parseFailures} message(s) failed to parse`);
28435
+ return { ok: true, documents, ...warnings.length > 0 ? { warnings } : {} };
28235
28436
  }
28236
- scored.sort((a, b) => b.score - a.score || a.key.localeCompare(b.key));
28237
- return scored.slice(0, k);
28238
28437
  }
28239
- function reciprocalRankFusion(lists) {
28240
- const scores = new Map;
28241
- for (const list of lists) {
28242
- const seen = new Set;
28243
- for (const [index, key] of list.entries()) {
28244
- if (seen.has(key))
28245
- continue;
28246
- seen.add(key);
28247
- scores.set(key, (scores.get(key) ?? 0) + 1 / (RRF_K2 + index + 1));
28438
+ function splitMbox(text) {
28439
+ const lines = text.split(/\r?\n/u);
28440
+ const messages = [];
28441
+ let current = [];
28442
+ for (const line of lines) {
28443
+ if (MBOX_SEPARATOR.test(line) && line.startsWith("From ")) {
28444
+ if (current.length > 0)
28445
+ messages.push(current.join(`
28446
+ `));
28447
+ current = [];
28448
+ continue;
28248
28449
  }
28450
+ current.push(line);
28249
28451
  }
28250
- return [...scores.entries()].map(([key, score]) => ({ key, score })).sort((a, b) => b.score - a.score);
28452
+ if (current.length > 0)
28453
+ messages.push(current.join(`
28454
+ `));
28455
+ return messages.filter((message) => message.trim().length > 0);
28251
28456
  }
28252
- var RRF_K2 = 60;
28457
+ var DEFAULT_MAX_DOCUMENTS3 = 500, DEFAULT_MAX_BYTES_PER_FILE2, MAX_BODY_CHARS = 50000, MBOX_SEPARATOR;
28458
+ var init_connector4 = __esm(() => {
28459
+ DEFAULT_MAX_BYTES_PER_FILE2 = 50 * 1024 * 1024;
28460
+ MBOX_SEPARATOR = /^From .*\d{4}$/mu;
28461
+ });
28253
28462
 
28254
- // src/datasource/sidecar/index.ts
28255
- import { createHash as createHash14 } from "node:crypto";
28256
- import { mkdirSync as mkdirSync23, statSync as statSync3 } from "node:fs";
28257
- import { dirname as dirname25 } from "node:path";
28258
- import { DatabaseSync as DatabaseSync2 } from "node:sqlite";
28259
- import { pathToFileURL as pathToFileURL2 } from "node:url";
28463
+ // src/datasource/skills/mail-export/skill.ts
28464
+ var MAIL_EXPORT_SKILL_DEFINITION, MailExportSkill;
28465
+ var init_skill8 = __esm(() => {
28466
+ init_connector_skill();
28467
+ init_connector4();
28468
+ MAIL_EXPORT_SKILL_DEFINITION = {
28469
+ skillName: "mail-export",
28470
+ skillType: "mail-export",
28471
+ description: "Local mail export datasource",
28472
+ capabilities: ["email", "filesystem"],
28473
+ defaultTags: ["mail-export", "email", "pii"],
28474
+ contentType: "email",
28475
+ manifestDescription: "Search indexed local mail exports (mbox/eml archives). Use for questions about archived email conversations.",
28476
+ manifestNotes: [
28477
+ "Only the server-configured export paths are indexed; archives are read locally and never uploaded."
28478
+ ]
28479
+ };
28480
+ MailExportSkill = class MailExportSkill extends ConnectorDatasourceSkill {
28481
+ constructor(options = {}) {
28482
+ const { connector, connectorOptions, ...rest } = options;
28483
+ super(MAIL_EXPORT_SKILL_DEFINITION, {
28484
+ ...rest,
28485
+ connector: connector ?? new MailExportConnector(connectorOptions ?? {})
28486
+ });
28487
+ }
28488
+ };
28489
+ });
28260
28490
 
28261
- class CrawlerSidecar {
28262
- descriptor;
28491
+ // src/datasource/skills/mail-export/index.ts
28492
+ var init_mail_export = __esm(() => {
28493
+ init_connector4();
28494
+ init_skill8();
28495
+ });
28496
+
28497
+ // src/datasource/skills/mailcrawl/config.ts
28498
+ import { chmodSync, mkdirSync as mkdirSync23 } from "node:fs";
28499
+ function validateMailcrawlInstanceId(instanceId) {
28500
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/u.test(instanceId)) {
28501
+ throw new Error("mailcrawl instanceId must be a safe single path segment");
28502
+ }
28503
+ }
28504
+ function ensurePrivateMailcrawlDataDir(path) {
28505
+ mkdirSync23(path, { recursive: true, mode: 448 });
28506
+ if (process.platform !== "win32")
28507
+ chmodSync(path, 448);
28508
+ }
28509
+ var init_config3 = () => {};
28510
+
28511
+ // src/datasource/skills/mailcrawl/client.ts
28512
+ import { spawn as spawn15 } from "node:child_process";
28513
+
28514
+ class MailcrawlClient {
28263
28515
  options;
28264
- select;
28265
- where;
28266
- opened;
28267
- attached;
28268
- lastSemantic;
28269
- constructor(options) {
28516
+ constructor(options = {}) {
28270
28517
  this.options = options;
28271
- const { descriptor } = options;
28272
- this.descriptor = descriptor;
28273
- const metadataColumns = descriptor.metadata.map((column, index) => `${column.sql} as m${index}`);
28274
- this.select = [
28275
- `${descriptor.keyExpression} as k`,
28276
- `${descriptor.textExpression} as content`,
28277
- `${descriptor.titleExpression ?? "null"} as title`,
28278
- `${descriptor.epochSecondsExpression ?? "null"} as epoch`,
28279
- ...metadataColumns
28280
- ].join(", ");
28281
- const scope = descriptor.scopeExpression !== undefined && options.workspaceId !== undefined;
28282
- const allowlist = [
28283
- ...(options.channelIds?.length ?? 0) > 0 ? [`${descriptor.channelIdExpression} in (${options.channelIds?.map(() => "?").join(", ")})`] : [],
28284
- ...(options.channelNames?.length ?? 0) > 0 ? [`${descriptor.channelNameExpression} in (${options.channelNames?.map(() => "?").join(", ")})`] : []
28285
- ];
28286
- this.where = [
28287
- descriptor.liveWhere,
28288
- ...scope ? [`${descriptor.scopeExpression} = ?`] : [],
28289
- ...allowlist.length > 0 ? [`(${allowlist.join(" or ")})`] : []
28290
- ].join(" and ");
28518
+ if (options.instanceId !== undefined)
28519
+ validateMailcrawlInstanceId(options.instanceId);
28291
28520
  }
28292
- get db() {
28293
- if (this.opened !== undefined)
28294
- return this.opened;
28295
- mkdirSync23(dirname25(this.options.sidecarDbPath), { recursive: true });
28296
- const db = new DatabaseSync2(this.options.sidecarDbPath);
28297
- db.exec(`pragma busy_timeout = ${BUSY_TIMEOUT_MS}`);
28298
- db.exec("pragma journal_mode = wal");
28299
- db.exec(`
28300
- create table if not exists sidecar_meta (k text primary key, v text not null);
28301
- create table if not exists sidecar_vec (message_key text primary key, hash text not null, vec blob not null);
28302
- `);
28303
- this.opened = db;
28304
- return db;
28521
+ async sync(signal) {
28522
+ const args = [
28523
+ "sync",
28524
+ "--json",
28525
+ ...this.options.account === undefined ? [] : ["--account", this.options.account],
28526
+ ...this.options.mailbox === undefined ? [] : ["--mailbox", this.options.mailbox],
28527
+ ...this.options.backend === undefined ? [] : ["--backend", this.options.backend],
28528
+ ...this.options.source === undefined ? [] : ["--source", this.options.source],
28529
+ ...this.options.fixture === undefined ? [] : ["--fixture", this.options.fixture],
28530
+ ...this.options.himalayaConfig === undefined ? [] : ["--himalaya-config", this.options.himalayaConfig]
28531
+ ];
28532
+ const result = await this.run(args, signal, this.indexTimeoutMs());
28533
+ if (!result.ok)
28534
+ return failure3(result);
28535
+ const parsed = parseObject2(result.stdout);
28536
+ if (parsed === undefined || !hasAnyNumber(parsed, ["added", "updated", "deleted", "unchanged", "chunksAdded"])) {
28537
+ return failure3({ ...result, ok: false, reason: "invalid-output" });
28538
+ }
28539
+ const messages = number(parsed, "added") ?? number(parsed, "updated") ?? number(parsed, "unchanged") ?? 0;
28540
+ return ok4({
28541
+ messages,
28542
+ added: number(parsed, "added"),
28543
+ updated: number(parsed, "updated"),
28544
+ deleted: number(parsed, "deleted"),
28545
+ unchanged: number(parsed, "unchanged"),
28546
+ chunksAdded: number(parsed, "chunksAdded"),
28547
+ archiveRevision: string(parsed, "archiveRevision")
28548
+ }, result);
28305
28549
  }
28306
- attachCrawler() {
28307
- let file;
28308
- try {
28309
- const { dev, ino } = statSync3(this.options.crawlerDbPath);
28310
- file = { dev, ino };
28311
- } catch {
28312
- this.detachCrawler();
28313
- return false;
28550
+ async search(mode, query, options) {
28551
+ if (query.trim().length === 0)
28552
+ return { ok: true, hits: [], stdout: "", stderr: "", code: 0 };
28553
+ const args = [
28554
+ "search",
28555
+ "--mode",
28556
+ mode === "keyword" ? "bm25" : mode,
28557
+ ...this.options.account === undefined ? [] : ["--account", this.options.account],
28558
+ ...this.options.mailbox === undefined ? [] : ["--mailbox", this.options.mailbox],
28559
+ "--limit",
28560
+ String(options?.topK ?? 20),
28561
+ "--json",
28562
+ query.trim()
28563
+ ];
28564
+ const result = await this.run(args, options?.signal, this.options.timeoutMs ?? DEFAULT_SEARCH_TIMEOUT_MS2);
28565
+ if (!result.ok)
28566
+ return failure3(result);
28567
+ const parsed = parseJson3(result.stdout);
28568
+ if (parsed === undefined)
28569
+ return failure3({ ...result, ok: false, reason: "invalid-output" });
28570
+ const rows = Array.isArray(parsed) ? parsed : objectArray(parsed, ["hits", "results", "items"]);
28571
+ if (rows === undefined)
28572
+ return failure3({ ...result, ok: false, reason: "invalid-output" });
28573
+ const hits = rows.map((row, index) => normalizeHit2(row, mode, index));
28574
+ if (hits.some((hit) => hit === undefined)) {
28575
+ return failure3({ ...result, ok: false, reason: "invalid-output" });
28314
28576
  }
28315
- if (this.attached !== undefined) {
28316
- if (this.attached.dev === file.dev && this.attached.ino === file.ino)
28317
- return true;
28318
- this.detachCrawler();
28577
+ return {
28578
+ ok: true,
28579
+ hits,
28580
+ stdout: result.stdout,
28581
+ stderr: result.stderr,
28582
+ code: result.code ?? 0
28583
+ };
28584
+ }
28585
+ indexTimeoutMs() {
28586
+ return this.options.indexTimeoutMs ?? this.options.timeoutMs ?? DEFAULT_INDEX_TIMEOUT_MS3;
28587
+ }
28588
+ async run(args, signal, timeoutMs) {
28589
+ const violation = findRemoteEmbeddingViolation({ ...process.env, ...this.options.env ?? {} });
28590
+ if (violation !== undefined) {
28591
+ return { ok: false, reason: "remote-embedding-rejected", stdout: "", stderr: "", code: null };
28319
28592
  }
28320
- const crawlerUri = `${pathToFileURL2(this.options.crawlerDbPath).href}?mode=ro`.replaceAll("'", "''");
28321
- try {
28322
- this.db.exec(`attach database '${crawlerUri}' as crawler`);
28323
- } catch {
28324
- return false;
28593
+ const dataDir = this.options.dataDir;
28594
+ if (dataDir !== undefined)
28595
+ ensurePrivateMailcrawlDataDir(dataDir);
28596
+ const env = {};
28597
+ for (const [key, value] of Object.entries(process.env)) {
28598
+ if (value !== undefined && (SAFE_ENV_KEYS2.has(key) || SAFE_MAILCRAWL_ENV_KEYS.has(key)))
28599
+ env[key] = value;
28325
28600
  }
28326
- const { descriptor } = this.options;
28327
- try {
28328
- this.db.prepare(`select ${this.select} from ${descriptor.fromClause} where ${this.where} limit 0`).all(...this.whereParams());
28329
- this.db.prepare(`select 1 from ${descriptor.fts.table} limit 0`).all();
28330
- } catch {
28331
- this.db.exec("detach database crawler");
28332
- return false;
28601
+ for (const [key, value] of Object.entries(this.options.env ?? {})) {
28602
+ if (value === undefined)
28603
+ delete env[key];
28604
+ else if (SAFE_ENV_KEYS2.has(key) || SAFE_MAILCRAWL_ENV_KEYS.has(key))
28605
+ env[key] = value;
28333
28606
  }
28334
- this.attached = file;
28335
- this.lastSemantic = undefined;
28336
- return true;
28337
- }
28338
- detachCrawler() {
28339
- if (this.attached === undefined)
28340
- return;
28341
- this.attached = undefined;
28342
- this.lastSemantic = undefined;
28343
- try {
28344
- this.opened?.exec("detach database crawler");
28345
- } catch {}
28346
- }
28347
- get semanticEnabled() {
28348
- return this.options.embedder !== undefined;
28349
- }
28350
- close() {
28351
- this.opened?.close();
28352
- this.opened = undefined;
28353
- this.attached = undefined;
28354
- this.lastSemantic = undefined;
28607
+ if (dataDir !== undefined)
28608
+ env.MAILCRAWL_DATA_DIR = dataDir;
28609
+ return spawnMailcrawl(this.options.binaryPath ?? DEFAULT_BINARY4, args, env, this.options, signal, timeoutMs);
28355
28610
  }
28356
- whereParams() {
28357
- const { descriptor, workspaceId, channelIds = [], channelNames = [] } = this.options;
28358
- return [
28359
- ...descriptor.scopeExpression !== undefined && workspaceId !== undefined ? [workspaceId] : [],
28360
- ...channelIds,
28361
- ...channelNames
28362
- ];
28611
+ async index(signal) {
28612
+ const result = await this.run(["index", "--json"], signal, this.indexTimeoutMs());
28613
+ if (!result.ok)
28614
+ return failure3(result);
28615
+ const parsed = parseObject2(result.stdout);
28616
+ if (parsed === undefined || number(parsed, "embedded") === undefined && number(parsed, "reused") === undefined && string(parsed, "generation") === undefined) {
28617
+ return failure3({ ...result, ok: false, reason: "invalid-output" });
28618
+ }
28619
+ return ok4({
28620
+ embedded: number(parsed, "embedded"),
28621
+ reused: number(parsed, "reused"),
28622
+ generation: string(parsed, "generation"),
28623
+ archiveRevision: string(parsed, "archiveRevision"),
28624
+ rebuilt: typeof parsed.rebuilt === "boolean" ? parsed.rebuilt : undefined,
28625
+ embedder: string(parsed, "embedder")
28626
+ }, result);
28363
28627
  }
28364
- searchLexical(query, topK) {
28365
- const match = toMatchQuery(query);
28366
- if (match === undefined || topK <= 0 || !this.attachCrawler())
28367
- return [];
28368
- const { descriptor } = this.options;
28369
- const rows = this.db.prepare(`select ${this.select}, bm25(${descriptor.fts.table}) as rank
28370
- from ${descriptor.fromClause} join ${descriptor.fts.table} on ${descriptor.fts.joinOn}
28371
- where ${descriptor.fts.table} match ? and ${this.where}
28372
- order by rank limit ?`).all(match, ...this.whereParams(), topK);
28373
- return rows.map((row) => this.toHit(this.toRow(row), -Number(row.rank)));
28374
- }
28375
- async searchSemantic(query, topK) {
28376
- const trimmed = query.trim();
28377
- if (this.options.embedder === undefined || trimmed.length === 0 || topK <= 0 || !this.attachCrawler())
28378
- return [];
28379
- const ranked = await this.rankSemantic(trimmed, topK * POOL_FACTOR);
28380
- if (ranked.length === 0)
28381
- return [];
28382
- const live = this.hydrate(ranked.map((entry) => entry.key));
28383
- const hits = [];
28384
- for (const entry of ranked) {
28385
- const row = live.get(entry.key);
28386
- if (row !== undefined)
28387
- hits.push(this.toHit(row, entry.score));
28388
- if (hits.length >= topK)
28389
- break;
28390
- }
28391
- return hits;
28392
- }
28393
- async rankSemantic(query, pool) {
28394
- const now = Date.now();
28395
- let cached = this.lastSemantic;
28396
- if (cached === undefined || cached.query !== query || now - cached.at > SEMANTIC_REUSE_MS) {
28397
- const embedder = this.options.embedder;
28398
- if (embedder === undefined)
28399
- return [];
28400
- cached = {
28401
- query,
28402
- at: now,
28403
- vector: embedder.embed([query]).then((vectors) => vectors[0], () => {
28404
- return;
28405
- }),
28406
- ranked: undefined
28407
- };
28408
- this.lastSemantic = cached;
28409
- }
28410
- const vector = await cached.vector;
28411
- if (vector === undefined) {
28412
- if (this.lastSemantic === cached)
28413
- this.lastSemantic = undefined;
28414
- return [];
28415
- }
28416
- if (cached.ranked === undefined || cached.ranked.pool < pool) {
28417
- const wide = pool * POOL_FACTOR;
28418
- cached.ranked = { pool: wide, keys: topKByCosine(vector, this.storedVectors(), wide) };
28419
- }
28420
- return cached.ranked.keys.slice(0, pool);
28421
- }
28422
- async searchHybrid(query, topK) {
28423
- if (topK <= 0)
28424
- return [];
28425
- const pool = topK * POOL_FACTOR;
28426
- const lexical = this.searchLexical(query, pool);
28427
- const semantic = await this.searchSemantic(query, pool);
28428
- if (semantic.length === 0)
28429
- return lexical.slice(0, topK);
28430
- const byKey = new Map;
28431
- for (const hit of [...semantic, ...lexical])
28432
- byKey.set(hit.key, hit);
28433
- const fused = [];
28434
- for (const entry of reciprocalRankFusion([lexical.map((hit) => hit.key), semantic.map((hit) => hit.key)])) {
28435
- const hit = byKey.get(entry.key);
28436
- if (hit !== undefined)
28437
- fused.push({ ...hit, score: entry.score });
28438
- if (fused.length >= topK)
28439
- break;
28440
- }
28441
- return fused;
28442
- }
28443
- async sync() {
28444
- const { embedder } = this.options;
28445
- if (embedder === undefined)
28446
- return { ok: false, message: "no embedder configured" };
28447
- if (!this.attachCrawler())
28448
- return { ok: false, message: "crawler archive not available yet" };
28449
- let identity;
28450
- try {
28451
- identity = await embedder.identity();
28452
- } catch (error) {
28453
- return { ok: false, message: errorMessage2(error) };
28454
- }
28455
- this.adoptIdentity(identity);
28456
- const pruned = this.prune();
28457
- const pending = this.pendingKeys();
28458
- const batchSize = Math.max(1, this.options.batchSize ?? DEFAULT_BATCH_SIZE);
28459
- const budget = Math.max(1, this.options.maxEmbedPerSync ?? DEFAULT_MAX_EMBED_PER_SYNC);
28460
- const target = Math.min(pending.length, budget);
28461
- const embed = async (rows) => {
28462
- const vectors = await embedder.embed(rows.map((row) => row.input));
28463
- if (vectors.length !== rows.length) {
28464
- throw new Error(`embedder returned ${vectors.length} vectors for ${rows.length} messages`);
28465
- }
28466
- const wrong = vectors.find((vector) => vector.length !== identity.dimension);
28467
- if (wrong !== undefined) {
28468
- throw new Error(`embedder returned a ${wrong.length}-dimension vector, expected ${identity.dimension}`);
28469
- }
28470
- return vectors;
28628
+ }
28629
+ function spawnMailcrawl(binary, args, env, options, signal, timeoutMs) {
28630
+ return new Promise((resolveResult) => {
28631
+ const portable = portableSpawnCommand(binary, args);
28632
+ const child = spawn15(portable.command, portable.args, {
28633
+ env,
28634
+ detached: process.platform !== "win32",
28635
+ stdio: ["ignore", "pipe", "pipe"]
28636
+ });
28637
+ let stdout = { text: "", bytes: 0, exceeded: false }, stderr = { text: "", bytes: 0, exceeded: false }, settled = false, reason;
28638
+ const max = options.maxBufferBytes ?? DEFAULT_MAX_BUFFER_BYTES4;
28639
+ const timer = setTimeout(() => {
28640
+ reason = "timeout";
28641
+ terminate4(child);
28642
+ }, timeoutMs);
28643
+ const abort = () => {
28644
+ reason = "aborted";
28645
+ terminate4(child);
28471
28646
  };
28472
- let embedded = 0;
28473
- let skipped = 0;
28474
- for (let offset = 0;offset < target; offset += batchSize) {
28475
- const rows = [...this.hydrate(pending.slice(offset, Math.min(offset + batchSize, target))).values()];
28476
- if (rows.length === 0)
28477
- continue;
28478
- try {
28479
- this.store(rows, await embed(rows));
28480
- embedded += rows.length;
28481
- } catch (batchError) {
28482
- const rejected = [];
28483
- let lastError = batchError;
28484
- for (const row of rows) {
28485
- try {
28486
- this.store([row], await embed([row]));
28487
- embedded += 1;
28488
- } catch (error) {
28489
- rejected.push(row);
28490
- lastError = error;
28491
- }
28492
- }
28493
- if (rejected.length === 0)
28494
- continue;
28495
- if (embedded === 0)
28496
- return { ok: false, message: errorMessage2(lastError) };
28497
- this.store(rejected, undefined);
28498
- skipped += rejected.length;
28647
+ if (signal?.aborted)
28648
+ abort();
28649
+ signal?.addEventListener("abort", abort, { once: true });
28650
+ child.stdout.setEncoding("utf8");
28651
+ child.stderr.setEncoding("utf8");
28652
+ child.stdout.on("data", (chunk) => {
28653
+ stdout = appendOutput(stdout, chunk, max);
28654
+ if (stdout.exceeded && reason === undefined) {
28655
+ reason = "stdout-too-large";
28656
+ terminate4(child);
28499
28657
  }
28500
- }
28501
- return { ok: true, embedded, skipped, pruned, remaining: pending.length - embedded - skipped };
28502
- }
28503
- adoptIdentity(identity) {
28504
- const serialized = JSON.stringify(identity);
28505
- const current = this.db.prepare("select v from sidecar_meta where k = ?").get(IDENTITY_KEY);
28506
- if (current?.v === serialized)
28507
- return;
28508
- this.transaction(() => {
28509
- this.db.exec("delete from sidecar_vec");
28510
- this.db.prepare("insert or replace into sidecar_meta (k, v) values (?, ?)").run(IDENTITY_KEY, serialized);
28511
28658
  });
28512
- this.lastSemantic = undefined;
28513
- }
28514
- prune() {
28515
- const { descriptor } = this.options;
28516
- const result = this.db.prepare(`delete from sidecar_vec where message_key not in (
28517
- select ${descriptor.keyExpression} from ${descriptor.fromClause} where ${this.where}
28518
- )`).run(...this.whereParams());
28519
- if (Number(result.changes) > 0)
28520
- this.lastSemantic = undefined;
28521
- return Number(result.changes);
28522
- }
28523
- pendingKeys() {
28524
- const { descriptor } = this.options;
28525
- const rows = this.db.prepare(`select ${this.select}, v.hash as stored_hash
28526
- from ${descriptor.fromClause}
28527
- left join sidecar_vec v on v.message_key = ${descriptor.keyExpression}
28528
- where ${this.where}`).iterate(...this.whereParams());
28529
- const pending = [];
28530
- for (const row of rows) {
28531
- if (row.stored_hash !== this.toRow(row).hash)
28532
- pending.push(String(row.k));
28533
- }
28534
- return pending;
28535
- }
28536
- hydrate(keys) {
28537
- const { descriptor } = this.options;
28538
- const out = new Map;
28539
- if (keys.length === 0)
28540
- return out;
28541
- const rows = this.db.prepare(`select ${this.select} from ${descriptor.fromClause}
28542
- where ${descriptor.keyExpression} in (${keys.map(() => "?").join(", ")}) and ${this.where}`).all(...keys, ...this.whereParams());
28543
- const byKey = new Map(rows.map((row) => [String(row.k), this.toRow(row)]));
28544
- for (const key of keys) {
28545
- const row = byKey.get(key);
28546
- if (row !== undefined)
28547
- out.set(key, row);
28548
- }
28549
- return out;
28550
- }
28551
- store(rows, vectors) {
28552
- const upsert = this.db.prepare("insert or replace into sidecar_vec (message_key, hash, vec) values (?, ?, ?)");
28553
- this.transaction(() => {
28554
- for (const [index, row] of rows.entries()) {
28555
- upsert.run(row.key, row.hash, encodeVector(vectors?.[index] ?? []));
28659
+ child.stderr.on("data", (chunk) => {
28660
+ stderr = appendOutput(stderr, chunk, max);
28661
+ if (stderr.exceeded && reason === undefined) {
28662
+ reason = "stderr-too-large";
28663
+ terminate4(child);
28556
28664
  }
28557
28665
  });
28558
- this.lastSemantic = undefined;
28666
+ child.on("error", (error) => {
28667
+ if (settled)
28668
+ return;
28669
+ settled = true;
28670
+ clearTimeout(timer);
28671
+ signal?.removeEventListener("abort", abort);
28672
+ resolveResult({
28673
+ ok: false,
28674
+ reason: error.code === "ENOENT" ? "binary-missing" : "spawn-error",
28675
+ stdout: stdout.text,
28676
+ stderr: "",
28677
+ code: null
28678
+ });
28679
+ });
28680
+ child.on("close", (code) => {
28681
+ if (settled)
28682
+ return;
28683
+ settled = true;
28684
+ clearTimeout(timer);
28685
+ signal?.removeEventListener("abort", abort);
28686
+ resolveResult({
28687
+ ok: code === 0 && reason === undefined,
28688
+ ...reason === undefined ? code === 0 ? {} : { reason: "nonzero-exit" } : { reason },
28689
+ stdout: stdout.text,
28690
+ stderr: stderr.text,
28691
+ code
28692
+ });
28693
+ });
28694
+ });
28695
+ }
28696
+ function terminate4(child) {
28697
+ if (child.killed)
28698
+ return;
28699
+ if (process.platform !== "win32" && child.pid !== undefined) {
28700
+ try {
28701
+ process.kill(-child.pid, "SIGTERM");
28702
+ return;
28703
+ } catch {}
28559
28704
  }
28560
- *storedVectors() {
28561
- const rows = this.db.prepare("select message_key, vec from sidecar_vec where length(vec) > 0").iterate();
28562
- for (const row of rows) {
28563
- const vector = row.vec instanceof Uint8Array ? decodeVector(row.vec) : undefined;
28564
- if (vector !== undefined)
28565
- yield { key: String(row.message_key), vector };
28705
+ child.kill("SIGTERM");
28706
+ }
28707
+ function parseJson3(text) {
28708
+ try {
28709
+ return JSON.parse(text.trim());
28710
+ } catch {
28711
+ return;
28712
+ }
28713
+ }
28714
+ function parseObject2(text) {
28715
+ const value = parseJson3(text);
28716
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : undefined;
28717
+ }
28718
+ function objectArray(value, keys) {
28719
+ if (value === null || typeof value !== "object" || Array.isArray(value))
28720
+ return;
28721
+ for (const key of keys)
28722
+ if (Array.isArray(value[key]))
28723
+ return value[key];
28724
+ return;
28725
+ }
28726
+ function hasAnyNumber(value, keys) {
28727
+ return keys.some((key) => number(value, key) !== undefined);
28728
+ }
28729
+ function normalizeHit2(value, mode, index) {
28730
+ if (value === null || typeof value !== "object" || Array.isArray(value))
28731
+ return;
28732
+ const row = value;
28733
+ const text = (key) => typeof row[key] === "string" ? row[key] : "";
28734
+ const chunkId = text("chunkId") || text("chunk_id");
28735
+ const messageId = text("messageId") || text("message_id");
28736
+ const accountId = text("accountId") || text("account_id");
28737
+ const mailbox = text("mailbox");
28738
+ const snippet = text("snippet") || text("content") || text("text");
28739
+ if (!isSafeOpaqueId(chunkId) || !isSafeOpaqueId(messageId) || !snippet)
28740
+ return;
28741
+ const to = Array.isArray(row.to) ? row.to.filter((item) => typeof item === "string") : [];
28742
+ return {
28743
+ chunkId,
28744
+ messageId,
28745
+ threadId: text("threadId") || text("thread_id"),
28746
+ accountId,
28747
+ mailbox,
28748
+ subject: text("subject"),
28749
+ from: text("from"),
28750
+ to,
28751
+ date: text("date"),
28752
+ snippet,
28753
+ score: typeof row.score === "number" ? row.score : 1 / (index + 1),
28754
+ mode
28755
+ };
28756
+ }
28757
+ function number(value, key) {
28758
+ return typeof value[key] === "number" && Number.isFinite(value[key]) ? value[key] : undefined;
28759
+ }
28760
+ function string(value, key) {
28761
+ return typeof value[key] === "string" ? value[key] : undefined;
28762
+ }
28763
+ function ok4(data, result) {
28764
+ return { ok: true, data, stdout: result.stdout, stderr: result.stderr, code: result.code ?? 0 };
28765
+ }
28766
+ function failure3(result) {
28767
+ return {
28768
+ ok: false,
28769
+ reason: result.reason ?? "nonzero-exit",
28770
+ stdout: result.stdout,
28771
+ stderr: boundDiagnosticText6(result.stderr),
28772
+ code: result.code
28773
+ };
28774
+ }
28775
+ function boundDiagnosticText6(value) {
28776
+ if (value.length === 0)
28777
+ return "";
28778
+ return value.trim().slice(0, 4000);
28779
+ }
28780
+ function appendOutput(current, chunk, maxBytes) {
28781
+ if (current.exceeded)
28782
+ return current;
28783
+ const bytes = Buffer.from(chunk, "utf8");
28784
+ const remaining = Math.max(0, maxBytes - current.bytes);
28785
+ const prefix = truncateUtf8(chunk, remaining);
28786
+ const total = current.bytes + bytes.byteLength;
28787
+ return {
28788
+ text: current.text + prefix,
28789
+ bytes: Math.min(total, maxBytes),
28790
+ exceeded: total > maxBytes
28791
+ };
28792
+ }
28793
+ function isSafeOpaqueId(value) {
28794
+ return value.length > 0 && value !== "." && value !== ".." && !/[\\/#\u0000-\u001f\u007f]/u.test(value);
28795
+ }
28796
+ function findRemoteEmbeddingViolation(env) {
28797
+ for (const [key, value] of Object.entries(env)) {
28798
+ if (value === undefined)
28799
+ continue;
28800
+ const lower = key.toLowerCase();
28801
+ if (REMOTE_EMBEDDING_ENV_KEYS.has(lower))
28802
+ return key;
28803
+ if (lower === "mailcrawl_embedder" || lower === "mailcrawl_embed_url") {
28804
+ if (isRemoteHttpEndpoint(value))
28805
+ return key;
28566
28806
  }
28567
28807
  }
28568
- transaction(work) {
28569
- this.db.exec("begin immediate");
28808
+ return;
28809
+ }
28810
+ function isRemoteHttpEndpoint(value) {
28811
+ const trimmed = value.trim();
28812
+ if (!/^https?:\/\//iu.test(trimmed))
28813
+ return false;
28814
+ try {
28815
+ return !LOOPBACK_HOSTS2.has(new URL(trimmed).hostname);
28816
+ } catch {
28817
+ return true;
28818
+ }
28819
+ }
28820
+ function truncateUtf8(value, maxBytes) {
28821
+ if (Buffer.byteLength(value) <= maxBytes)
28822
+ return value;
28823
+ const decoder = new TextDecoder("utf-8", { fatal: true });
28824
+ const bytes = Buffer.from(value, "utf8");
28825
+ for (let end = Math.min(maxBytes, bytes.length);end >= 0; end -= 1) {
28570
28826
  try {
28571
- work();
28572
- this.db.exec("commit");
28573
- } catch (error) {
28574
- this.db.exec("rollback");
28575
- throw error;
28576
- }
28827
+ return decoder.decode(bytes.subarray(0, end));
28828
+ } catch {}
28577
28829
  }
28578
- toRow(row) {
28579
- const metadata = {};
28580
- for (const [index, column] of this.options.descriptor.metadata.entries()) {
28581
- const value = row[`m${index}`];
28582
- if (typeof value === "string" && value.length > 0 || typeof value === "number")
28583
- metadata[column.key] = String(value);
28584
- }
28585
- const title = typeof row.title === "string" && row.title.length > 0 ? row.title : undefined;
28586
- const epoch = typeof row.epoch === "number" && Number.isFinite(row.epoch) ? row.epoch : undefined;
28587
- const content = String(row.content ?? "");
28588
- const input = `${title ?? ""}
28589
- ${content}`.slice(0, MAX_EMBED_INPUT_CHARS);
28590
- return {
28591
- key: String(row.k),
28592
- content,
28593
- title,
28594
- epoch,
28595
- metadata,
28596
- input,
28597
- hash: createHash14("sha1").update(input).digest("hex")
28598
- };
28830
+ return "";
28831
+ }
28832
+ 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;
28833
+ var init_client9 = __esm(() => {
28834
+ init_portable_spawn();
28835
+ init_config3();
28836
+ LOOPBACK_HOSTS2 = new Set(["127.0.0.1", "localhost", "[::1]", "::1"]);
28837
+ SAFE_ENV_KEYS2 = new Set(["HOME", "LANG", "LC_ALL", "PATH", "TMPDIR", "TMP", "TEMP", "NO_COLOR"]);
28838
+ SAFE_MAILCRAWL_ENV_KEYS = new Set([
28839
+ "MAILCRAWL_DATA_DIR",
28840
+ "MAILCRAWL_EMBEDDER",
28841
+ "MAILCRAWL_EMBEDDER_PROVIDER",
28842
+ "MAILCRAWL_EMBED_URL",
28843
+ "MAILCRAWL_EMBED_MODEL",
28844
+ "MAILCRAWL_EMBED_DIM",
28845
+ "MAILCRAWL_EMBED_TIMEOUT",
28846
+ "MAILCRAWL_EMBED_BATCH_SIZE",
28847
+ "MAILCRAWL_QUERY_PREFIX",
28848
+ "MAILCRAWL_PASSAGE_PREFIX",
28849
+ "MAILCRAWL_NATIVE_MODEL",
28850
+ "MAILCRAWL_NATIVE_DEVICE",
28851
+ "MAILCRAWL_NATIVE_DTYPE",
28852
+ "MAILCRAWL_KIWI_MODEL",
28853
+ "MAILCRAWL_JA_HELPER",
28854
+ "MAILCRAWL_ZH_HELPER"
28855
+ ]);
28856
+ REMOTE_EMBEDDING_ENV_KEYS = new Set([
28857
+ "mailcrawl_embedder_base_url",
28858
+ "mailcrawl_embedding_base_url",
28859
+ "mailcrawl_allow_remote_embeddings"
28860
+ ]);
28861
+ });
28862
+
28863
+ // src/datasource/skills/mailcrawl/methods.ts
28864
+ class MailcrawlMethod {
28865
+ client;
28866
+ mode;
28867
+ instanceId;
28868
+ tags;
28869
+ account;
28870
+ mailbox;
28871
+ constructor(client, mode, instanceId, tags, account, mailbox) {
28872
+ this.client = client;
28873
+ this.mode = mode;
28874
+ this.instanceId = instanceId;
28875
+ this.tags = tags;
28876
+ this.account = account;
28877
+ this.mailbox = mailbox;
28599
28878
  }
28600
- toHit(row, score) {
28879
+ describe() {
28601
28880
  return {
28602
- key: row.key,
28603
- content: row.content,
28604
- score,
28605
- ...row.title !== undefined ? { title: row.title } : {},
28606
- ...row.epoch !== undefined ? { publishedAt: Math.round(row.epoch * 1000) } : {},
28607
- metadata: row.metadata
28881
+ name: `mailcrawl-${this.mode}`,
28882
+ type: this.mode === "bm25" ? "bm25" : this.mode === "semantic" ? "vector" : "hybrid",
28883
+ description: `${this.mode} retrieval over archived email via the external mailcrawl CLI`,
28884
+ status: "active",
28885
+ capabilities: ["email", this.mode, "scoped", "external-cli", "path-opaque-sources"],
28886
+ datasourceId: "mailcrawl",
28887
+ tags: this.tags
28608
28888
  };
28609
28889
  }
28890
+ async retrieve(query, options) {
28891
+ if (!query.trim())
28892
+ return [];
28893
+ const result = await this.client.search(this.mode, query, {
28894
+ topK: options.topK,
28895
+ signal: options.signal
28896
+ });
28897
+ if (!result.ok)
28898
+ throw datasourceCliError("mailcrawl", `search --mode ${this.mode}`, result);
28899
+ return result.hits.filter((hit) => {
28900
+ if (this.account !== undefined && hit.accountId !== this.account)
28901
+ return false;
28902
+ if (this.mailbox !== undefined && hit.mailbox !== this.mailbox)
28903
+ return false;
28904
+ const source = datasourceSourcePath("mailcrawl", this.instanceId, hit.chunkId);
28905
+ return matchesDatasourceScope(source, options.scope);
28906
+ }).map((hit) => {
28907
+ const source = datasourceSourcePath("mailcrawl", this.instanceId, hit.chunkId);
28908
+ return {
28909
+ id: `mailcrawl:${this.instanceId}:${hit.chunkId}`,
28910
+ content: hit.snippet,
28911
+ source,
28912
+ score: hit.score,
28913
+ metadata: {
28914
+ method: `mailcrawl-${this.mode}`,
28915
+ datasourceId: "mailcrawl",
28916
+ instanceId: this.instanceId,
28917
+ mode: this.mode,
28918
+ chunkId: hit.chunkId,
28919
+ messageId: hit.messageId,
28920
+ threadId: hit.threadId,
28921
+ accountId: hit.accountId,
28922
+ mailbox: hit.mailbox,
28923
+ subject: hit.subject,
28924
+ from: hit.from,
28925
+ to: hit.to,
28926
+ date: hit.date
28927
+ }
28928
+ };
28929
+ }).slice(0, options.topK ?? 20);
28930
+ }
28610
28931
  }
28611
- function toMatchQuery(text) {
28612
- const terms = [...new Set(text.match(/[\p{L}\p{N}_]+/gu) ?? [])];
28613
- return terms.length === 0 ? undefined : terms.map((term) => `"${term}"`).join(" OR ");
28614
- }
28615
- function errorMessage2(error) {
28616
- return error instanceof Error ? error.message : String(error);
28617
- }
28618
- 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";
28619
- var init_sidecar = () => {};
28620
-
28621
- // src/datasource/sidecar/methods.ts
28622
- function sidecarMethodName(datasourceId, mode) {
28623
- return mode === "bm25" ? `${datasourceId}-sidecar-bm25` : `${datasourceId}-${mode}`;
28624
- }
28932
+ var modes, MAILCRAWL_MODES;
28933
+ var init_methods5 = __esm(() => {
28934
+ init_errors4();
28935
+ init_scope2();
28936
+ modes = ["bm25", "semantic", "hybrid"];
28937
+ MAILCRAWL_MODES = modes;
28938
+ });
28625
28939
 
28626
- class SidecarRetrievalMethod {
28627
- options;
28628
- constructor(options) {
28629
- this.options = options;
28940
+ // src/datasource/skills/mailcrawl/skill.ts
28941
+ class MailcrawlSkill {
28942
+ client;
28943
+ instanceId;
28944
+ instances;
28945
+ interval;
28946
+ tags;
28947
+ account;
28948
+ mailbox;
28949
+ lastIndexedAt;
28950
+ constructor(options = {}) {
28951
+ const connectorOptions = options.connectorOptions ?? options;
28952
+ this.instanceId = options.instanceId ?? "default";
28953
+ this.instances = options.instances?.length ? options.instances : [this.instanceId];
28954
+ validateMailcrawlInstanceId(this.instanceId);
28955
+ for (const instance of this.instances)
28956
+ validateMailcrawlInstanceId(instance);
28957
+ this.interval = options.pollingIntervalMs ?? 15 * 60 * 1000;
28958
+ this.tags = options.tags ?? TAGS;
28959
+ this.account = connectorOptions.account;
28960
+ this.mailbox = connectorOptions.mailbox;
28961
+ this.client = options.client ?? new MailcrawlClient({ ...connectorOptions, instanceId: this.instanceId });
28630
28962
  }
28631
28963
  describe() {
28632
- const { datasourceId, skillType, mode, tags } = this.options;
28633
- const { type, text, capabilities } = DESCRIPTIONS[mode];
28634
28964
  return {
28635
- name: sidecarMethodName(datasourceId, mode),
28636
- type,
28637
- description: `${text} over ${skillType} via a read-only sidecar index`,
28965
+ name: "mailcrawl",
28966
+ id: "mailcrawl",
28967
+ type: "mailcrawl-archive",
28968
+ description: "Local email archive datasource via the external mailcrawl CLI",
28969
+ capabilities: ["email", "external-cli", "polling", "incremental", "bm25", "semantic", "hybrid"],
28970
+ tags: this.tags,
28638
28971
  status: "active",
28639
- capabilities: [...capabilities, "scoped", "path-opaque-sources"],
28640
- datasourceId,
28641
- tags
28972
+ requiresExternalCli: true,
28973
+ datasourceId: "mailcrawl",
28974
+ instanceId: this.instanceId,
28975
+ instances: this.instances
28642
28976
  };
28643
28977
  }
28644
- async retrieve(query, options) {
28645
- const trimmed = query.trim();
28646
- if (trimmed.length === 0)
28647
- return [];
28648
- const topK = options.topK ?? DEFAULT_TOP_K5;
28649
- const { sidecar, mode } = this.options;
28650
- const hits = mode === "bm25" ? sidecar.searchLexical(trimmed, topK) : mode === "semantic" ? await sidecar.searchSemantic(trimmed, topK) : await sidecar.searchHybrid(trimmed, topK);
28651
- const results = [];
28652
- for (const hit of hits) {
28653
- const result = this.toResult(hit, options);
28654
- if (result !== undefined)
28655
- results.push(result);
28978
+ polling() {
28979
+ return {
28980
+ mode: this.interval > 0 ? "poll" : "none",
28981
+ ...this.interval > 0 ? { intervalMs: this.interval } : {},
28982
+ lastIndexedAt: this.lastIndexedAt
28983
+ };
28984
+ }
28985
+ async index(options) {
28986
+ try {
28987
+ const result = await this.client.sync(options?.signal);
28988
+ if (!result.ok) {
28989
+ 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);
28990
+ }
28991
+ const indexed = await this.client.index(options?.signal);
28992
+ const diagnostics = [];
28993
+ if (!indexed.ok) {
28994
+ diagnostics.push({
28995
+ code: indexed.reason === "remote-embedding-rejected" ? "datasource-embedding-egress-rejected" : "datasource-index-failed",
28996
+ severity: "warning",
28997
+ message: `mailcrawl semantic index unavailable (${indexed.reason}); BM25 remains available`,
28998
+ instanceId: this.instanceId,
28999
+ source: "mailcrawl"
29000
+ });
29001
+ }
29002
+ this.lastIndexedAt = Date.now();
29003
+ return {
29004
+ ok: true,
29005
+ instanceId: this.instanceId,
29006
+ skill: "mailcrawl",
29007
+ chunkCount: result.data.chunksAdded ?? result.data.messages,
29008
+ indexedAt: this.lastIndexedAt,
29009
+ diagnostics
29010
+ };
29011
+ } catch {
29012
+ return this.fail("datasource-unavailable", "mailcrawl sync failed unexpectedly");
28656
29013
  }
28657
- return results;
28658
29014
  }
28659
- toResult(hit, options) {
28660
- const { descriptor, datasourceId, instanceId, mode } = this.options;
28661
- const chunkId = descriptor.chunkId(hit.key, hit.metadata);
28662
- const source = datasourceSourcePath(datasourceId, instanceId, chunkId);
28663
- if (!matchesDatasourceScope(source, options.scope))
28664
- return;
28665
- const method = sidecarMethodName(datasourceId, mode);
29015
+ retrievalMethods() {
29016
+ return [
29017
+ new MailcrawlMethod(this.client, "bm25", this.instanceId, this.tags, this.account, this.mailbox),
29018
+ new MailcrawlMethod(this.client, "semantic", this.instanceId, this.tags, this.account, this.mailbox),
29019
+ new MailcrawlMethod(this.client, "hybrid", this.instanceId, this.tags, this.account, this.mailbox)
29020
+ ];
29021
+ }
29022
+ describeSources() {
29023
+ return this.instances.map((instanceId) => ({
29024
+ source: datasourceSourcePath("mailcrawl", instanceId),
29025
+ datasourceId: "mailcrawl",
29026
+ skill: "mailcrawl",
29027
+ instanceId,
29028
+ contentType: "email",
29029
+ metadata: { datasourceId: "mailcrawl", instanceId, tags: this.tags }
29030
+ }));
29031
+ }
29032
+ skillManifest() {
29033
+ const scopes = this.instances.map((id) => `- \`${datasourceSourcePath("mailcrawl", id)}\``).join(`
29034
+ `);
28666
29035
  return {
28667
- id: `${datasourceId}:${instanceId}:${chunkId}`,
28668
- content: hit.content,
28669
- source,
28670
- score: hit.score,
28671
- metadata: {
28672
- ...hit.metadata,
28673
- method,
28674
- datasourceId,
28675
- instanceId,
28676
- backend: "sidecar",
28677
- ...hit.title !== undefined ? { title: hit.title } : {},
28678
- hierarchy: descriptor.hierarchy(hit.metadata).join("/"),
28679
- ...hit.publishedAt !== undefined ? { publishedAt: hit.publishedAt } : {}
28680
- }
29036
+ name: "datasource-mailcrawl",
29037
+ description: "Search local email synchronized and indexed by mailcrawl.",
29038
+ content: `# mailcrawl email datasource
29039
+
29040
+ Use this skill for authorized local email retrieval. AutoRAG invokes the external \`mailcrawl\` CLI and never opens its SQLite archive directly.
29041
+
29042
+ ## Indexing
29043
+ Indexing is server-managed and incremental; do not trigger sync yourself.
29044
+
29045
+ ## Search
29046
+ Call the dedicated \`${datasourceSearchToolName("mailcrawl")}\` tool with a natural-language query. Available scopes:
29047
+ ${scopes}
29048
+
29049
+ The selectable retrieval modes are BM25, semantic, and hybrid. Scope arguments can only narrow trusted access. Source identifiers remain opaque and hierarchical.
29050
+
29051
+ ## Native CLI
29052
+ The external \`mailcrawl\` CLI owns the archive and index. When the dedicated tool cannot express what you need, call mailcrawl directly through \`bash\`:
29053
+ - \`mailcrawl search --mode hybrid --limit 20 --json "<query>"\` (\`bm25\` and \`semantic\` modes also available; \`--account\` / \`--mailbox\` narrow the archive)
29054
+ Never pass datasource virtual paths (\`/mailcrawl/...\`) to bash; they are not OS paths.`
29055
+ };
29056
+ }
29057
+ fail(code, message) {
29058
+ return {
29059
+ ok: false,
29060
+ instanceId: this.instanceId,
29061
+ skill: "mailcrawl",
29062
+ indexedAt: Date.now(),
29063
+ diagnostics: [
29064
+ {
29065
+ code,
29066
+ severity: code === "datasource-unavailable" ? "warning" : "error",
29067
+ message: `mailcrawl ${message}`,
29068
+ instanceId: this.instanceId,
29069
+ source: "mailcrawl"
29070
+ }
29071
+ ],
29072
+ error: message,
29073
+ code,
29074
+ message
28681
29075
  };
28682
29076
  }
28683
29077
  }
28684
- var DEFAULT_TOP_K5 = 20, DESCRIPTIONS;
28685
- var init_methods5 = __esm(() => {
29078
+ var TAGS;
29079
+ var init_skill9 = __esm(() => {
28686
29080
  init_scope2();
28687
- DESCRIPTIONS = {
28688
- bm25: { type: "bm25", text: "BM25-ranked FTS5 retrieval", capabilities: ["lexical", "fts5", "bm25"] },
28689
- semantic: { type: "vector", text: "Semantic (embedding) retrieval", capabilities: ["semantic"] },
28690
- hybrid: {
28691
- type: "hybrid",
28692
- text: "Hybrid BM25 + semantic retrieval (reciprocal rank fusion)",
28693
- capabilities: ["lexical", "semantic", "hybrid"]
28694
- }
28695
- };
29081
+ init_client9();
29082
+ init_config3();
29083
+ init_methods5();
29084
+ TAGS = ["mailcrawl", "email", "pii"];
29085
+ });
29086
+
29087
+ // src/datasource/skills/mailcrawl/index.ts
29088
+ var init_mailcrawl = __esm(() => {
29089
+ init_client9();
29090
+ init_methods5();
29091
+ init_skill9();
28696
29092
  });
28697
29093
 
28698
29094
  // src/datasource/crawler-skill.ts
@@ -28975,7 +29371,7 @@ var init_crawler_skill = __esm(() => {
28975
29371
  init_errors4();
28976
29372
  init_scope2();
28977
29373
  init_sidecar();
28978
- init_methods5();
29374
+ init_methods4();
28979
29375
  DEFAULT_POLLING_INTERVAL_MS4 = 15 * 60 * 1000;
28980
29376
  });
28981
29377
 
@@ -30602,7 +30998,7 @@ var init_wacrawl = __esm(() => {
30602
30998
  });
30603
30999
 
30604
31000
  // src/datasource/skills/factory.ts
30605
- import { homedir as homedir4 } from "node:os";
31001
+ import { homedir as homedir5 } from "node:os";
30606
31002
  import { join as join39 } from "node:path";
30607
31003
  function chatCrawlerArgs(descriptor, defaultCrawlerDb, config, workspaceRoot, registrationName) {
30608
31004
  const sidecar = resolveSidecarOptions({
@@ -30706,9 +31102,9 @@ var init_factory = __esm(() => {
30706
31102
  init_telecrawl();
30707
31103
  init_wacrawl();
30708
31104
  BUILDERS = {
30709
- telegram: (config, workspaceRoot, registrationName) => new TelecrawlSkill(chatCrawlerArgs(CRAWLER_DESCRIPTORS.telegram, join39(homedir4(), ".telecrawl", "telecrawl.db"), config, workspaceRoot, registrationName)),
30710
- whatsapp: (config, workspaceRoot, registrationName) => new WacrawlSkill(chatCrawlerArgs(CRAWLER_DESCRIPTORS.whatsapp, join39(homedir4(), ".wacrawl", "wacrawl.db"), config, workspaceRoot, registrationName)),
30711
- slack: (config, workspaceRoot, registrationName) => new SlackSkill(chatCrawlerArgs(CRAWLER_DESCRIPTORS.slack, join39(homedir4(), ".slacrawl", "slacrawl.db"), config, workspaceRoot, registrationName)),
31105
+ telegram: (config, workspaceRoot, registrationName) => new TelecrawlSkill(chatCrawlerArgs(CRAWLER_DESCRIPTORS.telegram, join39(homedir5(), ".telecrawl", "telecrawl.db"), config, workspaceRoot, registrationName)),
31106
+ whatsapp: (config, workspaceRoot, registrationName) => new WacrawlSkill(chatCrawlerArgs(CRAWLER_DESCRIPTORS.whatsapp, join39(homedir5(), ".wacrawl", "wacrawl.db"), config, workspaceRoot, registrationName)),
31107
+ slack: (config, workspaceRoot, registrationName) => new SlackSkill(chatCrawlerArgs(CRAWLER_DESCRIPTORS.slack, join39(homedir5(), ".slacrawl", "slacrawl.db"), config, workspaceRoot, registrationName)),
30712
31108
  discord: (config, workspaceRoot, registrationName, embeddingRuntime) => {
30713
31109
  const connector = config.connector ?? {};
30714
31110
  const clientOptions = {
@@ -30764,24 +31160,42 @@ var init_factory = __esm(() => {
30764
31160
  ...config.channels?.ids !== undefined ? { chatIds: config.channels.ids } : {},
30765
31161
  ...larkConnectorOptions(config.connector)
30766
31162
  }),
30767
- kakao: (config, _workspaceRoot, _registrationName) => new LazykatokSkill({
30768
- client: new LazykatokClient({
30769
- ...config.connector
30770
- }),
30771
- ...config.instanceId !== undefined ? { instanceId: config.instanceId } : {},
30772
- ...config.pollingIntervalMs !== undefined ? { pollingIntervalMs: config.pollingIntervalMs } : {},
30773
- ...config.tags !== undefined ? { tags: config.tags } : {}
30774
- }),
31163
+ kakao: (config, workspaceRoot, registrationName) => {
31164
+ const connector = config.connector ?? {};
31165
+ const dataDir = typeof connector.workspacePath === "string" && connector.workspacePath.length > 0 ? connector.workspacePath : join39(homedir5(), "Library", "Application Support", "katok");
31166
+ const sidecar = resolveSidecarOptions({
31167
+ descriptor: CRAWLER_DESCRIPTORS.kakao,
31168
+ connector: config.connector,
31169
+ workspaceRoot,
31170
+ registrationName,
31171
+ instanceId: config.instanceId ?? "default",
31172
+ defaultCrawlerDb: join39(dataDir, "archive.sqlite3")
31173
+ });
31174
+ return new LazykatokSkill({
31175
+ client: new LazykatokClient(connector),
31176
+ ...config.instanceId !== undefined ? { instanceId: config.instanceId } : {},
31177
+ ...config.pollingIntervalMs !== undefined ? { pollingIntervalMs: config.pollingIntervalMs } : {},
31178
+ ...config.tags !== undefined ? { tags: config.tags } : {},
31179
+ channelIds: config.channels?.ids,
31180
+ channelNames: config.channels?.names,
31181
+ ...sidecar === undefined ? {} : { sidecar }
31182
+ });
31183
+ },
30775
31184
  github: (config, workspaceRoot, registrationName) => new GitHubSkill({
30776
31185
  ...common(config, workspaceRoot),
30777
31186
  skillName: registrationName,
30778
31187
  connectorOptions: config.connector
30779
31188
  }),
30780
- "github-gist": (config, workspaceRoot, registrationName) => new GitHubGistSkill({
30781
- ...common(config, workspaceRoot),
30782
- skillName: registrationName,
30783
- connectorOptions: config.connector
30784
- }),
31189
+ "github-gist": (config, workspaceRoot, registrationName) => {
31190
+ const { semantic, ...connectorOptions } = config.connector ?? {};
31191
+ const semanticOff = semantic === false || typeof semantic === "object" && semantic !== null && semantic.enabled === false;
31192
+ return new GitHubGistSkill({
31193
+ ...common(config, workspaceRoot),
31194
+ skillName: registrationName,
31195
+ connectorOptions,
31196
+ ...semanticOff ? { semantic: { enabled: false } } : {}
31197
+ });
31198
+ },
30785
31199
  "cloud-drive": (config, workspaceRoot, registrationName) => new CloudDriveSkill({
30786
31200
  ...common(config, workspaceRoot),
30787
31201
  skillName: registrationName,
@@ -30827,7 +31241,7 @@ var init_factory = __esm(() => {
30827
31241
 
30828
31242
  // src/cli/config.ts
30829
31243
  import { randomUUID as randomUUID8 } from "node:crypto";
30830
- import { existsSync as existsSync28, mkdirSync as mkdirSync25, readFileSync as readFileSync25, renameSync as renameSync10, unlinkSync as unlinkSync5, writeFileSync as writeFileSync22 } from "node:fs";
31244
+ import { existsSync as existsSync29, mkdirSync as mkdirSync25, readFileSync as readFileSync25, renameSync as renameSync10, unlinkSync as unlinkSync5, writeFileSync as writeFileSync22 } from "node:fs";
30831
31245
  import { basename as basename14, dirname as dirname26, isAbsolute as isAbsolute10, join as join40, resolve as resolve16 } from "node:path";
30832
31246
  import { findEnvKeys, getEnvApiKey } from "@earendil-works/pi-ai/compat";
30833
31247
  import { getAgentDir as getAgentDir4, ModelRuntime as ModelRuntime2, SettingsManager as SettingsManager2 } from "@earendil-works/pi-coding-agent";
@@ -30852,7 +31266,7 @@ function resolveConfigPath(input) {
30852
31266
  function readConfigFile(configPath, explicit) {
30853
31267
  let exists;
30854
31268
  try {
30855
- exists = existsSync28(configPath);
31269
+ exists = existsSync29(configPath);
30856
31270
  } catch {
30857
31271
  exists = false;
30858
31272
  }
@@ -30912,12 +31326,12 @@ function replaceFileAtomically(path, contents, assertCommitAllowed) {
30912
31326
  }
30913
31327
  }
30914
31328
  function migrateLegacyConfig(configPath, legacyPath) {
30915
- if (existsSync28(configPath) || !existsSync28(legacyPath))
31329
+ if (existsSync29(configPath) || !existsSync29(legacyPath))
30916
31330
  return;
30917
31331
  const legacy = readConfigFile(legacyPath, true);
30918
31332
  const legacyBytes = readFileSync25(legacyPath);
30919
31333
  const migrated = normalizeLegacyConfigPaths(legacy ?? {}, dirname26(legacyPath));
30920
- const migratedPathsUnchanged = legacy?.workspacePath === migrated.workspacePath && legacy?.memoryPath === migrated.memoryPath && JSON.stringify(legacy?.searchPaths) === JSON.stringify(migrated.searchPaths);
31334
+ 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);
30921
31335
  const migratedBytes = migratedPathsUnchanged ? legacyBytes : `${JSON.stringify(migrated, null, 2)}
30922
31336
  `;
30923
31337
  mkdirSync25(dirname26(configPath), { recursive: true });
@@ -30955,15 +31369,25 @@ function resolveConfigFileReadOnly(configPath, explicit, legacyPath) {
30955
31369
  function resolveSearchPaths(searchPaths, origin) {
30956
31370
  return searchPaths.map((searchPath) => resolvePersistedPath(searchPath, origin));
30957
31371
  }
31372
+ function resolveExcludePaths(value, origin) {
31373
+ if (value === undefined)
31374
+ return;
31375
+ if (!Array.isArray(value) || value.some((path) => typeof path !== "string" || path.length === 0)) {
31376
+ throw new ConfigError("Config field 'excludePaths' must be a non-empty string array");
31377
+ }
31378
+ return resolveSearchPaths(value, origin);
31379
+ }
30958
31380
  function resolvePersistedPath(path, origin) {
30959
31381
  return isAbsolute10(path) ? path : resolve16(origin, path);
30960
31382
  }
30961
31383
  function normalizeLegacyConfigPaths(partial, origin) {
30962
31384
  const workspacePath = resolvePersistedPath(partial.workspacePath ?? ".", origin);
31385
+ const excludePaths = resolveExcludePaths(partial.excludePaths, workspacePath);
30963
31386
  return {
30964
31387
  ...partial,
30965
31388
  workspacePath,
30966
31389
  ...partial.searchPaths === undefined ? {} : { searchPaths: resolveSearchPaths(partial.searchPaths, workspacePath) },
31390
+ ...excludePaths === undefined ? {} : { excludePaths },
30967
31391
  ...partial.memoryPath === undefined ? {} : { memoryPath: resolvePersistedPath(partial.memoryPath, workspacePath) }
30968
31392
  };
30969
31393
  }
@@ -31418,10 +31842,7 @@ function resolveConfig(input) {
31418
31842
  const configOrigin = dirname26(resolve16(configPath));
31419
31843
  const fileWorkspacePath = typeof file.workspacePath === "string" ? resolvePersistedPath(file.workspacePath, configOrigin) : undefined;
31420
31844
  const fileSearchPaths = file.searchPaths ? resolveSearchPaths(file.searchPaths, fileWorkspacePath ?? configOrigin) : undefined;
31421
- if (file.excludePaths !== undefined && (!Array.isArray(file.excludePaths) || file.excludePaths.some((path) => typeof path !== "string" || path.length === 0))) {
31422
- throw new ConfigError("Config field 'excludePaths' must be a non-empty string array");
31423
- }
31424
- const fileExcludePaths = file.excludePaths ? resolveSearchPaths(file.excludePaths, fileWorkspacePath ?? configOrigin) : undefined;
31845
+ const fileExcludePaths = resolveExcludePaths(file.excludePaths, fileWorkspacePath ?? configOrigin);
31425
31846
  const searchPaths = flagSearchPaths ?? envSearchPaths ?? fileSearchPaths ?? defaultSearchPaths;
31426
31847
  const flagWorkspacePath = flagString(flags, "workspace");
31427
31848
  const envWorkspacePath = envString(env, "AUTORAG_WORKSPACE");
@@ -32157,7 +32578,7 @@ function writeDefaultConfig(path, partial, opts = {}) {
32157
32578
  full.dupey = partial.dupey ?? { enabled: true };
32158
32579
  full.excludeExactDuplicates = partial.excludeExactDuplicates ?? true;
32159
32580
  if (partial.excludePaths !== undefined)
32160
- full.excludePaths = resolveSearchPaths(partial.excludePaths, workspacePath);
32581
+ full.excludePaths = resolveExcludePaths(partial.excludePaths, workspacePath);
32161
32582
  if (partial.limits !== undefined)
32162
32583
  full.limits = normalizeLimitsConfig(partial.limits);
32163
32584
  if (partial.refresh !== undefined)
@@ -32176,7 +32597,7 @@ function writeDefaultConfig(path, partial, opts = {}) {
32176
32597
  `;
32177
32598
  const lock = acquireConfigWriteLock(path);
32178
32599
  try {
32179
- const exists = existsSync28(path);
32600
+ const exists = existsSync29(path);
32180
32601
  if (!opts.force && exists) {
32181
32602
  throw new ConfigError(`Config file already exists: ${path}`);
32182
32603
  }