@autorag/librarian 2.10.2 → 2.10.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/index.js CHANGED
@@ -2657,7 +2657,7 @@ function chatCrawler(id) {
2657
2657
  hierarchy: (metadata) => ["chats", str(metadata.chatName) ?? str(metadata.chatId) ?? "unknown"]
2658
2658
  };
2659
2659
  }
2660
- var str = (value) => typeof value === "string" && value.length > 0 ? value : undefined, slack, CRAWLER_DESCRIPTORS;
2660
+ var str = (value) => typeof value === "string" && value.length > 0 ? value : undefined, slack, kakao, CRAWLER_DESCRIPTORS;
2661
2661
  var init_descriptors = __esm(() => {
2662
2662
  slack = {
2663
2663
  id: "slack",
@@ -2688,10 +2688,32 @@ var init_descriptors = __esm(() => {
2688
2688
  str(metadata.channelName) ?? str(metadata.channelId) ?? "unknown"
2689
2689
  ]
2690
2690
  };
2691
+ kakao = {
2692
+ id: "kakao",
2693
+ keyExpression: "m.chunk_id",
2694
+ chunkId: (key) => key,
2695
+ fromClause: "chunks m",
2696
+ 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))",
2697
+ textExpression: "coalesce(m.text, '')",
2698
+ titleExpression: "m.chat_name",
2699
+ epochSecondsExpression: "unixepoch(m.started_at)",
2700
+ fts: { table: "chunks_fts", joinOn: "chunks_fts.rowid = m.rowid" },
2701
+ metadata: [
2702
+ { sql: "m.chat_id", key: "chatId" },
2703
+ { sql: "m.chat_name", key: "chatName" },
2704
+ { sql: "m.sender_nickname", key: "senderNickname" },
2705
+ { sql: "m.started_at", key: "startedAt" },
2706
+ { sql: "m.ended_at", key: "endedAt" }
2707
+ ],
2708
+ channelIdExpression: "m.chat_id",
2709
+ channelNameExpression: "m.chat_name",
2710
+ hierarchy: (metadata) => ["chats", str(metadata.chatName) ?? str(metadata.chatId) ?? "unknown"]
2711
+ };
2691
2712
  CRAWLER_DESCRIPTORS = {
2692
2713
  slack,
2693
2714
  telegram: chatCrawler("telegram"),
2694
- whatsapp: chatCrawler("whatsapp")
2715
+ whatsapp: chatCrawler("whatsapp"),
2716
+ kakao
2695
2717
  };
2696
2718
  });
2697
2719
 
@@ -8054,6 +8076,504 @@ var init_skip = __esm(() => {
8054
8076
  MINSYNC_METHOD_NAMES = new Set(["minsync", "hybrid", "bm25"]);
8055
8077
  });
8056
8078
 
8079
+ // src/datasource/sidecar/fusion.ts
8080
+ function encodeVector(vector) {
8081
+ const floats = Float32Array.from(vector);
8082
+ return new Uint8Array(floats.buffer, floats.byteOffset, floats.byteLength);
8083
+ }
8084
+ function decodeVector(blob) {
8085
+ if (blob.byteLength % Float32Array.BYTES_PER_ELEMENT !== 0)
8086
+ return;
8087
+ return new Float32Array(new Uint8Array(blob).buffer);
8088
+ }
8089
+ function cosine2(a, b) {
8090
+ if (a.length === 0 || a.length !== b.length)
8091
+ return 0;
8092
+ let dot = 0;
8093
+ let normA = 0;
8094
+ let normB = 0;
8095
+ for (let index = 0;index < a.length; index += 1) {
8096
+ const x = a[index] ?? 0;
8097
+ const y = b[index] ?? 0;
8098
+ dot += x * y;
8099
+ normA += x * x;
8100
+ normB += y * y;
8101
+ }
8102
+ if (normA === 0 || normB === 0)
8103
+ return 0;
8104
+ return dot / Math.sqrt(normA * normB);
8105
+ }
8106
+ function topKByCosine(query, rows, k) {
8107
+ if (k <= 0)
8108
+ return [];
8109
+ const scored = [];
8110
+ for (const row of rows) {
8111
+ const score = cosine2(query, row.vector);
8112
+ if (score > 0)
8113
+ scored.push({ key: row.key, score });
8114
+ }
8115
+ scored.sort((a, b) => b.score - a.score || a.key.localeCompare(b.key));
8116
+ return scored.slice(0, k);
8117
+ }
8118
+ function reciprocalRankFusion(lists) {
8119
+ const scores = new Map;
8120
+ for (const list of lists) {
8121
+ const seen = new Set;
8122
+ for (const [index, key] of list.entries()) {
8123
+ if (seen.has(key))
8124
+ continue;
8125
+ seen.add(key);
8126
+ scores.set(key, (scores.get(key) ?? 0) + 1 / (RRF_K + index + 1));
8127
+ }
8128
+ }
8129
+ return [...scores.entries()].map(([key, score]) => ({ key, score })).sort((a, b) => b.score - a.score);
8130
+ }
8131
+ var RRF_K = 60;
8132
+
8133
+ // src/datasource/sidecar/index.ts
8134
+ import { createHash as createHash6 } from "node:crypto";
8135
+ import { mkdirSync as mkdirSync7, statSync } from "node:fs";
8136
+ import { dirname as dirname11 } from "node:path";
8137
+ import { DatabaseSync } from "node:sqlite";
8138
+ import { pathToFileURL } from "node:url";
8139
+
8140
+ class CrawlerSidecar {
8141
+ descriptor;
8142
+ options;
8143
+ select;
8144
+ where;
8145
+ opened;
8146
+ attached;
8147
+ lastSemantic;
8148
+ constructor(options) {
8149
+ this.options = options;
8150
+ const { descriptor } = options;
8151
+ this.descriptor = descriptor;
8152
+ const metadataColumns = descriptor.metadata.map((column, index) => `${column.sql} as m${index}`);
8153
+ this.select = [
8154
+ `${descriptor.keyExpression} as k`,
8155
+ `${descriptor.textExpression} as content`,
8156
+ `${descriptor.titleExpression ?? "null"} as title`,
8157
+ `${descriptor.epochSecondsExpression ?? "null"} as epoch`,
8158
+ ...metadataColumns
8159
+ ].join(", ");
8160
+ const scope = descriptor.scopeExpression !== undefined && options.workspaceId !== undefined;
8161
+ const allowlist = [
8162
+ ...(options.channelIds?.length ?? 0) > 0 ? [`${descriptor.channelIdExpression} in (${options.channelIds?.map(() => "?").join(", ")})`] : [],
8163
+ ...(options.channelNames?.length ?? 0) > 0 ? [`${descriptor.channelNameExpression} in (${options.channelNames?.map(() => "?").join(", ")})`] : []
8164
+ ];
8165
+ this.where = [
8166
+ descriptor.liveWhere,
8167
+ ...scope ? [`${descriptor.scopeExpression} = ?`] : [],
8168
+ ...allowlist.length > 0 ? [`(${allowlist.join(" or ")})`] : []
8169
+ ].join(" and ");
8170
+ }
8171
+ get db() {
8172
+ if (this.opened !== undefined)
8173
+ return this.opened;
8174
+ mkdirSync7(dirname11(this.options.sidecarDbPath), { recursive: true });
8175
+ const db = new DatabaseSync(this.options.sidecarDbPath);
8176
+ db.exec(`pragma busy_timeout = ${BUSY_TIMEOUT_MS}`);
8177
+ db.exec("pragma journal_mode = wal");
8178
+ db.exec(`
8179
+ create table if not exists sidecar_meta (k text primary key, v text not null);
8180
+ create table if not exists sidecar_vec (message_key text primary key, hash text not null, vec blob not null);
8181
+ `);
8182
+ this.opened = db;
8183
+ return db;
8184
+ }
8185
+ attachCrawler() {
8186
+ let file;
8187
+ try {
8188
+ const { dev, ino } = statSync(this.options.crawlerDbPath);
8189
+ file = { dev, ino };
8190
+ } catch {
8191
+ this.detachCrawler();
8192
+ return false;
8193
+ }
8194
+ if (this.attached !== undefined) {
8195
+ if (this.attached.dev === file.dev && this.attached.ino === file.ino)
8196
+ return true;
8197
+ this.detachCrawler();
8198
+ }
8199
+ const crawlerUri = `${pathToFileURL(this.options.crawlerDbPath).href}?mode=ro`.replaceAll("'", "''");
8200
+ try {
8201
+ this.db.exec(`attach database '${crawlerUri}' as crawler`);
8202
+ } catch {
8203
+ return false;
8204
+ }
8205
+ const { descriptor } = this.options;
8206
+ try {
8207
+ this.db.prepare(`select ${this.select} from ${descriptor.fromClause} where ${this.where} limit 0`).all(...this.whereParams());
8208
+ this.db.prepare(`select 1 from ${descriptor.fts.table} limit 0`).all();
8209
+ } catch {
8210
+ this.db.exec("detach database crawler");
8211
+ return false;
8212
+ }
8213
+ this.attached = file;
8214
+ this.lastSemantic = undefined;
8215
+ return true;
8216
+ }
8217
+ detachCrawler() {
8218
+ if (this.attached === undefined)
8219
+ return;
8220
+ this.attached = undefined;
8221
+ this.lastSemantic = undefined;
8222
+ try {
8223
+ this.opened?.exec("detach database crawler");
8224
+ } catch {}
8225
+ }
8226
+ get semanticEnabled() {
8227
+ return this.options.embedder !== undefined;
8228
+ }
8229
+ close() {
8230
+ this.opened?.close();
8231
+ this.opened = undefined;
8232
+ this.attached = undefined;
8233
+ this.lastSemantic = undefined;
8234
+ }
8235
+ whereParams() {
8236
+ const { descriptor, workspaceId, channelIds = [], channelNames = [] } = this.options;
8237
+ return [
8238
+ ...descriptor.scopeExpression !== undefined && workspaceId !== undefined ? [workspaceId] : [],
8239
+ ...channelIds,
8240
+ ...channelNames
8241
+ ];
8242
+ }
8243
+ searchLexical(query, topK) {
8244
+ const match = toMatchQuery(query);
8245
+ if (match === undefined || topK <= 0 || !this.attachCrawler())
8246
+ return [];
8247
+ const { descriptor } = this.options;
8248
+ const rows = this.db.prepare(`select ${this.select}, bm25(${descriptor.fts.table}) as rank
8249
+ from ${descriptor.fromClause} join ${descriptor.fts.table} on ${descriptor.fts.joinOn}
8250
+ where ${descriptor.fts.table} match ? and ${this.where}
8251
+ order by rank limit ?`).all(match, ...this.whereParams(), topK);
8252
+ return rows.map((row) => this.toHit(this.toRow(row), -Number(row.rank)));
8253
+ }
8254
+ async searchSemantic(query, topK) {
8255
+ const trimmed = query.trim();
8256
+ if (this.options.embedder === undefined || trimmed.length === 0 || topK <= 0 || !this.attachCrawler())
8257
+ return [];
8258
+ const ranked = await this.rankSemantic(trimmed, topK * POOL_FACTOR);
8259
+ if (ranked.length === 0)
8260
+ return [];
8261
+ const live = this.hydrate(ranked.map((entry) => entry.key));
8262
+ const hits = [];
8263
+ for (const entry of ranked) {
8264
+ const row = live.get(entry.key);
8265
+ if (row !== undefined)
8266
+ hits.push(this.toHit(row, entry.score));
8267
+ if (hits.length >= topK)
8268
+ break;
8269
+ }
8270
+ return hits;
8271
+ }
8272
+ async rankSemantic(query, pool) {
8273
+ const now = Date.now();
8274
+ let cached = this.lastSemantic;
8275
+ if (cached === undefined || cached.query !== query || now - cached.at > SEMANTIC_REUSE_MS) {
8276
+ const embedder = this.options.embedder;
8277
+ if (embedder === undefined)
8278
+ return [];
8279
+ cached = {
8280
+ query,
8281
+ at: now,
8282
+ vector: embedder.embed([query]).then((vectors) => vectors[0], () => {
8283
+ return;
8284
+ }),
8285
+ ranked: undefined
8286
+ };
8287
+ this.lastSemantic = cached;
8288
+ }
8289
+ const vector = await cached.vector;
8290
+ if (vector === undefined) {
8291
+ if (this.lastSemantic === cached)
8292
+ this.lastSemantic = undefined;
8293
+ return [];
8294
+ }
8295
+ if (cached.ranked === undefined || cached.ranked.pool < pool) {
8296
+ const wide = pool * POOL_FACTOR;
8297
+ cached.ranked = { pool: wide, keys: topKByCosine(vector, this.storedVectors(), wide) };
8298
+ }
8299
+ return cached.ranked.keys.slice(0, pool);
8300
+ }
8301
+ async searchHybrid(query, topK) {
8302
+ if (topK <= 0)
8303
+ return [];
8304
+ const pool = topK * POOL_FACTOR;
8305
+ const lexical = this.searchLexical(query, pool);
8306
+ const semantic = await this.searchSemantic(query, pool);
8307
+ if (semantic.length === 0)
8308
+ return lexical.slice(0, topK);
8309
+ const byKey = new Map;
8310
+ for (const hit of [...semantic, ...lexical])
8311
+ byKey.set(hit.key, hit);
8312
+ const fused = [];
8313
+ for (const entry of reciprocalRankFusion([lexical.map((hit) => hit.key), semantic.map((hit) => hit.key)])) {
8314
+ const hit = byKey.get(entry.key);
8315
+ if (hit !== undefined)
8316
+ fused.push({ ...hit, score: entry.score });
8317
+ if (fused.length >= topK)
8318
+ break;
8319
+ }
8320
+ return fused;
8321
+ }
8322
+ async sync() {
8323
+ const { embedder } = this.options;
8324
+ if (embedder === undefined)
8325
+ return { ok: false, message: "no embedder configured" };
8326
+ if (!this.attachCrawler())
8327
+ return { ok: false, message: "crawler archive not available yet" };
8328
+ let identity;
8329
+ try {
8330
+ identity = await embedder.identity();
8331
+ } catch (error) {
8332
+ return { ok: false, message: errorMessage(error) };
8333
+ }
8334
+ this.adoptIdentity(identity);
8335
+ const pruned = this.prune();
8336
+ const pending = this.pendingKeys();
8337
+ const batchSize = Math.max(1, this.options.batchSize ?? DEFAULT_BATCH_SIZE);
8338
+ const budget = Math.max(1, this.options.maxEmbedPerSync ?? DEFAULT_MAX_EMBED_PER_SYNC);
8339
+ const target = Math.min(pending.length, budget);
8340
+ const embed = async (rows) => {
8341
+ const vectors = await embedder.embed(rows.map((row) => row.input));
8342
+ if (vectors.length !== rows.length) {
8343
+ throw new Error(`embedder returned ${vectors.length} vectors for ${rows.length} messages`);
8344
+ }
8345
+ const wrong = vectors.find((vector) => vector.length !== identity.dimension);
8346
+ if (wrong !== undefined) {
8347
+ throw new Error(`embedder returned a ${wrong.length}-dimension vector, expected ${identity.dimension}`);
8348
+ }
8349
+ return vectors;
8350
+ };
8351
+ let embedded = 0;
8352
+ let skipped = 0;
8353
+ for (let offset = 0;offset < target; offset += batchSize) {
8354
+ const rows = [...this.hydrate(pending.slice(offset, Math.min(offset + batchSize, target))).values()];
8355
+ if (rows.length === 0)
8356
+ continue;
8357
+ try {
8358
+ this.store(rows, await embed(rows));
8359
+ embedded += rows.length;
8360
+ } catch (batchError) {
8361
+ const rejected = [];
8362
+ let lastError = batchError;
8363
+ for (const row of rows) {
8364
+ try {
8365
+ this.store([row], await embed([row]));
8366
+ embedded += 1;
8367
+ } catch (error) {
8368
+ rejected.push(row);
8369
+ lastError = error;
8370
+ }
8371
+ }
8372
+ if (rejected.length === 0)
8373
+ continue;
8374
+ if (embedded === 0)
8375
+ return { ok: false, message: errorMessage(lastError) };
8376
+ this.store(rejected, undefined);
8377
+ skipped += rejected.length;
8378
+ }
8379
+ }
8380
+ return { ok: true, embedded, skipped, pruned, remaining: pending.length - embedded - skipped };
8381
+ }
8382
+ adoptIdentity(identity) {
8383
+ const serialized = JSON.stringify(identity);
8384
+ const current = this.db.prepare("select v from sidecar_meta where k = ?").get(IDENTITY_KEY);
8385
+ if (current?.v === serialized)
8386
+ return;
8387
+ this.transaction(() => {
8388
+ this.db.exec("delete from sidecar_vec");
8389
+ this.db.prepare("insert or replace into sidecar_meta (k, v) values (?, ?)").run(IDENTITY_KEY, serialized);
8390
+ });
8391
+ this.lastSemantic = undefined;
8392
+ }
8393
+ prune() {
8394
+ const { descriptor } = this.options;
8395
+ const result = this.db.prepare(`delete from sidecar_vec where message_key not in (
8396
+ select ${descriptor.keyExpression} from ${descriptor.fromClause} where ${this.where}
8397
+ )`).run(...this.whereParams());
8398
+ if (Number(result.changes) > 0)
8399
+ this.lastSemantic = undefined;
8400
+ return Number(result.changes);
8401
+ }
8402
+ pendingKeys() {
8403
+ const { descriptor } = this.options;
8404
+ const rows = this.db.prepare(`select ${this.select}, v.hash as stored_hash
8405
+ from ${descriptor.fromClause}
8406
+ left join sidecar_vec v on v.message_key = ${descriptor.keyExpression}
8407
+ where ${this.where}`).iterate(...this.whereParams());
8408
+ const pending = [];
8409
+ for (const row of rows) {
8410
+ if (row.stored_hash !== this.toRow(row).hash)
8411
+ pending.push(String(row.k));
8412
+ }
8413
+ return pending;
8414
+ }
8415
+ hydrate(keys) {
8416
+ const { descriptor } = this.options;
8417
+ const out = new Map;
8418
+ if (keys.length === 0)
8419
+ return out;
8420
+ const rows = this.db.prepare(`select ${this.select} from ${descriptor.fromClause}
8421
+ where ${descriptor.keyExpression} in (${keys.map(() => "?").join(", ")}) and ${this.where}`).all(...keys, ...this.whereParams());
8422
+ const byKey = new Map(rows.map((row) => [String(row.k), this.toRow(row)]));
8423
+ for (const key of keys) {
8424
+ const row = byKey.get(key);
8425
+ if (row !== undefined)
8426
+ out.set(key, row);
8427
+ }
8428
+ return out;
8429
+ }
8430
+ store(rows, vectors) {
8431
+ const upsert = this.db.prepare("insert or replace into sidecar_vec (message_key, hash, vec) values (?, ?, ?)");
8432
+ this.transaction(() => {
8433
+ for (const [index, row] of rows.entries()) {
8434
+ upsert.run(row.key, row.hash, encodeVector(vectors?.[index] ?? []));
8435
+ }
8436
+ });
8437
+ this.lastSemantic = undefined;
8438
+ }
8439
+ *storedVectors() {
8440
+ const rows = this.db.prepare("select message_key, vec from sidecar_vec where length(vec) > 0").iterate();
8441
+ for (const row of rows) {
8442
+ const vector = row.vec instanceof Uint8Array ? decodeVector(row.vec) : undefined;
8443
+ if (vector !== undefined)
8444
+ yield { key: String(row.message_key), vector };
8445
+ }
8446
+ }
8447
+ transaction(work) {
8448
+ this.db.exec("begin immediate");
8449
+ try {
8450
+ work();
8451
+ this.db.exec("commit");
8452
+ } catch (error) {
8453
+ this.db.exec("rollback");
8454
+ throw error;
8455
+ }
8456
+ }
8457
+ toRow(row) {
8458
+ const metadata = {};
8459
+ for (const [index, column] of this.options.descriptor.metadata.entries()) {
8460
+ const value = row[`m${index}`];
8461
+ if (typeof value === "string" && value.length > 0 || typeof value === "number")
8462
+ metadata[column.key] = String(value);
8463
+ }
8464
+ const title = typeof row.title === "string" && row.title.length > 0 ? row.title : undefined;
8465
+ const epoch = typeof row.epoch === "number" && Number.isFinite(row.epoch) ? row.epoch : undefined;
8466
+ const content = String(row.content ?? "");
8467
+ const input = `${title ?? ""}
8468
+ ${content}`.slice(0, MAX_EMBED_INPUT_CHARS);
8469
+ return {
8470
+ key: String(row.k),
8471
+ content,
8472
+ title,
8473
+ epoch,
8474
+ metadata,
8475
+ input,
8476
+ hash: createHash6("sha1").update(input).digest("hex")
8477
+ };
8478
+ }
8479
+ toHit(row, score) {
8480
+ return {
8481
+ key: row.key,
8482
+ content: row.content,
8483
+ score,
8484
+ ...row.title !== undefined ? { title: row.title } : {},
8485
+ ...row.epoch !== undefined ? { publishedAt: Math.round(row.epoch * 1000) } : {},
8486
+ metadata: row.metadata
8487
+ };
8488
+ }
8489
+ }
8490
+ function toMatchQuery(text) {
8491
+ const terms = [...new Set(text.match(/[\p{L}\p{N}_]+/gu) ?? [])];
8492
+ return terms.length === 0 ? undefined : terms.map((term) => `"${term}"`).join(" OR ");
8493
+ }
8494
+ function errorMessage(error) {
8495
+ return error instanceof Error ? error.message : String(error);
8496
+ }
8497
+ 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";
8498
+ var init_sidecar = () => {};
8499
+
8500
+ // src/datasource/sidecar/methods.ts
8501
+ function sidecarMethodName(datasourceId, mode) {
8502
+ return mode === "bm25" ? `${datasourceId}-sidecar-bm25` : `${datasourceId}-${mode}`;
8503
+ }
8504
+
8505
+ class SidecarRetrievalMethod {
8506
+ options;
8507
+ constructor(options) {
8508
+ this.options = options;
8509
+ }
8510
+ describe() {
8511
+ const { datasourceId, skillType, mode, tags } = this.options;
8512
+ const { type, text, capabilities } = DESCRIPTIONS[mode];
8513
+ return {
8514
+ name: sidecarMethodName(datasourceId, mode),
8515
+ type,
8516
+ description: `${text} over ${skillType} via a read-only sidecar index`,
8517
+ status: "active",
8518
+ capabilities: [...capabilities, "scoped", "path-opaque-sources"],
8519
+ datasourceId,
8520
+ tags
8521
+ };
8522
+ }
8523
+ async retrieve(query, options) {
8524
+ const trimmed = query.trim();
8525
+ if (trimmed.length === 0)
8526
+ return [];
8527
+ const topK = options.topK ?? DEFAULT_TOP_K5;
8528
+ const { sidecar, mode } = this.options;
8529
+ const hits = mode === "bm25" ? sidecar.searchLexical(trimmed, topK) : mode === "semantic" ? await sidecar.searchSemantic(trimmed, topK) : await sidecar.searchHybrid(trimmed, topK);
8530
+ const results = [];
8531
+ for (const hit of hits) {
8532
+ const result = this.toResult(hit, options);
8533
+ if (result !== undefined)
8534
+ results.push(result);
8535
+ }
8536
+ return results;
8537
+ }
8538
+ toResult(hit, options) {
8539
+ const { descriptor, datasourceId, instanceId, mode } = this.options;
8540
+ const chunkId = descriptor.chunkId(hit.key, hit.metadata);
8541
+ const source = datasourceSourcePath(datasourceId, instanceId, chunkId);
8542
+ if (!matchesDatasourceScope(source, options.scope))
8543
+ return;
8544
+ const method = sidecarMethodName(datasourceId, mode);
8545
+ return {
8546
+ id: `${datasourceId}:${instanceId}:${chunkId}`,
8547
+ content: hit.content,
8548
+ source,
8549
+ score: hit.score,
8550
+ metadata: {
8551
+ ...hit.metadata,
8552
+ method,
8553
+ datasourceId,
8554
+ instanceId,
8555
+ backend: "sidecar",
8556
+ ...hit.title !== undefined ? { title: hit.title } : {},
8557
+ hierarchy: descriptor.hierarchy(hit.metadata).join("/"),
8558
+ ...hit.publishedAt !== undefined ? { publishedAt: hit.publishedAt } : {}
8559
+ }
8560
+ };
8561
+ }
8562
+ }
8563
+ var DEFAULT_TOP_K5 = 20, DESCRIPTIONS;
8564
+ var init_methods4 = __esm(() => {
8565
+ init_scope2();
8566
+ DESCRIPTIONS = {
8567
+ bm25: { type: "bm25", text: "BM25-ranked FTS5 retrieval", capabilities: ["lexical", "fts5", "bm25"] },
8568
+ semantic: { type: "vector", text: "Semantic (embedding) retrieval", capabilities: ["semantic"] },
8569
+ hybrid: {
8570
+ type: "hybrid",
8571
+ text: "Hybrid BM25 + semantic retrieval (reciprocal rank fusion)",
8572
+ capabilities: ["lexical", "semantic", "hybrid"]
8573
+ }
8574
+ };
8575
+ });
8576
+
8057
8577
  // src/datasource/skills/lazykatok/skill.ts
8058
8578
  class LazykatokSkill {
8059
8579
  client;
@@ -8061,6 +8581,7 @@ class LazykatokSkill {
8061
8581
  instances;
8062
8582
  pollingIntervalMs;
8063
8583
  tags;
8584
+ sidecar;
8064
8585
  lastIndexedAt;
8065
8586
  constructor(options) {
8066
8587
  this.client = options.client;
@@ -8069,6 +8590,11 @@ class LazykatokSkill {
8069
8590
  this.pollingIntervalMs = options.pollingIntervalMs ?? DEFAULT_POLLING_INTERVAL_MS3;
8070
8591
  this.tags = options.tags ?? DEFAULT_KAKAO_TAGS2;
8071
8592
  this.lastIndexedAt = options.lastIndexedAt;
8593
+ this.sidecar = options.sidecar === undefined ? undefined : new CrawlerSidecar({
8594
+ ...options.sidecar,
8595
+ channelIds: options.channelIds ?? options.sidecar.channelIds ?? [],
8596
+ channelNames: options.channelNames ?? options.sidecar.channelNames ?? []
8597
+ });
8072
8598
  }
8073
8599
  describe() {
8074
8600
  return {
@@ -8100,6 +8626,17 @@ class LazykatokSkill {
8100
8626
  const sync = await this.client.sync(options?.signal);
8101
8627
  if (!sync.ok)
8102
8628
  return this.fail("datasource-index-failed", sync);
8629
+ if (this.sidecar !== undefined) {
8630
+ this.lastIndexedAt = Date.now();
8631
+ return {
8632
+ ok: true,
8633
+ instanceId: this.instanceId,
8634
+ skill: KAKAO_DATASOURCE_ID2,
8635
+ chunkCount: syncChunkCount(sync),
8636
+ indexedAt: this.lastIndexedAt,
8637
+ diagnostics: await this.syncSidecar()
8638
+ };
8639
+ }
8103
8640
  const indexResult = await this.client.index(options?.signal);
8104
8641
  if (!indexResult.ok)
8105
8642
  return this.fail("datasource-index-failed", indexResult);
@@ -8123,12 +8660,75 @@ class LazykatokSkill {
8123
8660
  });
8124
8661
  }
8125
8662
  }
8663
+ close() {
8664
+ this.sidecar?.close();
8665
+ }
8126
8666
  retrievalMethods() {
8667
+ const cliBm25 = new LazykatokBm25Method({ client: this.client, instanceId: this.instanceId, tags: this.tags });
8668
+ const { sidecar } = this;
8669
+ if (sidecar === undefined) {
8670
+ return [
8671
+ cliBm25,
8672
+ new LazykatokSemanticMethod({ client: this.client, instanceId: this.instanceId, tags: this.tags })
8673
+ ];
8674
+ }
8675
+ const shared = {
8676
+ sidecar,
8677
+ descriptor: sidecar.descriptor,
8678
+ datasourceId: KAKAO_DATASOURCE_ID2,
8679
+ skillType: KAKAO_SKILL_TYPE,
8680
+ instanceId: this.instanceId,
8681
+ tags: this.tags
8682
+ };
8683
+ const bm25 = new SidecarRetrievalMethod({ ...shared, mode: "bm25" });
8684
+ if (!sidecar.semanticEnabled)
8685
+ return [cliBm25, bm25];
8127
8686
  return [
8128
- new LazykatokBm25Method({ client: this.client, instanceId: this.instanceId, tags: this.tags }),
8129
- new LazykatokSemanticMethod({ client: this.client, instanceId: this.instanceId, tags: this.tags })
8687
+ cliBm25,
8688
+ bm25,
8689
+ new SidecarRetrievalMethod({ ...shared, mode: "semantic" }),
8690
+ new SidecarRetrievalMethod({ ...shared, mode: "hybrid" })
8130
8691
  ];
8131
8692
  }
8693
+ async syncSidecar() {
8694
+ if (this.sidecar === undefined || !this.sidecar.semanticEnabled)
8695
+ return [];
8696
+ const base = { instanceId: this.instanceId, source: KAKAO_DATASOURCE_ID2 };
8697
+ let result;
8698
+ try {
8699
+ result = await this.sidecar.sync();
8700
+ } catch (error) {
8701
+ result = { ok: false, message: error instanceof Error ? error.message : String(error) };
8702
+ }
8703
+ if (!result.ok) {
8704
+ return [
8705
+ {
8706
+ code: "datasource-index-failed",
8707
+ severity: "warning",
8708
+ message: `semantic-unavailable: ${result.message}`.slice(0, 300),
8709
+ ...base
8710
+ }
8711
+ ];
8712
+ }
8713
+ const diagnostics = [];
8714
+ if (result.skipped > 0) {
8715
+ diagnostics.push({
8716
+ code: "datasource-index-partial",
8717
+ severity: "warning",
8718
+ message: `semantic index skipped ${result.skipped} message(s) the embedder rejected; they stay searchable by BM25 and are retried when edited`,
8719
+ ...base
8720
+ });
8721
+ }
8722
+ if (result.remaining > 0) {
8723
+ diagnostics.push({
8724
+ code: "datasource-index-partial",
8725
+ severity: "info",
8726
+ message: `semantic index catching up: ${result.embedded} embedded this refresh, ${result.remaining} remaining; hybrid search uses BM25 for the rest until a later refresh`,
8727
+ ...base
8728
+ });
8729
+ }
8730
+ return diagnostics;
8731
+ }
8132
8732
  describeSources() {
8133
8733
  return this.instances.map((instanceId) => ({
8134
8734
  source: datasourceSourcePath(KAKAO_DATASOURCE_ID2, instanceId),
@@ -8202,10 +8802,22 @@ class LazykatokSkill {
8202
8802
  };
8203
8803
  }
8204
8804
  }
8805
+ function syncChunkCount(sync) {
8806
+ if (!("data" in sync) || typeof sync.data !== "object" || sync.data === null)
8807
+ return 0;
8808
+ const meta = "metadata" in sync.data && typeof sync.data.metadata === "object" && sync.data.metadata !== null ? sync.data.metadata : undefined;
8809
+ if (typeof meta?.chunks === "number")
8810
+ return meta.chunks;
8811
+ if ("messageCount" in sync.data && typeof sync.data.messageCount === "number")
8812
+ return sync.data.messageCount;
8813
+ return 0;
8814
+ }
8205
8815
  var KAKAO_DATASOURCE_ID2 = "kakao", KAKAO_SKILL_TYPE = "kakaotalk", DEFAULT_INSTANCE_ID4 = "default", DEFAULT_POLLING_INTERVAL_MS3, DEFAULT_KAKAO_TAGS2;
8206
8816
  var init_skill7 = __esm(() => {
8207
8817
  init_skip();
8208
8818
  init_scope2();
8819
+ init_sidecar();
8820
+ init_methods4();
8209
8821
  init_methods3();
8210
8822
  DEFAULT_POLLING_INTERVAL_MS3 = 15 * 60 * 1000;
8211
8823
  DEFAULT_KAKAO_TAGS2 = ["kakaotalk", "personal", "pii"];
@@ -8221,7 +8833,7 @@ var init_lazykatok = __esm(() => {
8221
8833
 
8222
8834
  // src/datasource/skills/mail-export/connector.ts
8223
8835
  import { readdir as readdir2, readFile as readFile4, stat as stat3 } from "node:fs/promises";
8224
- import { basename as basename5, dirname as dirname11, extname as extname3, join as join14 } from "node:path";
8836
+ import { basename as basename5, dirname as dirname12, extname as extname3, join as join14 } from "node:path";
8225
8837
  import { simpleParser as simpleParser2 } from "mailparser";
8226
8838
 
8227
8839
  class MailExportConnector {
@@ -8282,7 +8894,7 @@ class MailExportConnector {
8282
8894
  continue;
8283
8895
  }
8284
8896
  const stem = basename5(file, extname3(file));
8285
- const mailboxDir = basename5(dirname11(file));
8897
+ const mailboxDir = basename5(dirname12(file));
8286
8898
  const hierarchy = ["mailboxes", mailboxDir, stem];
8287
8899
  const extension = extname3(file).toLowerCase();
8288
8900
  const rawMessages = extension === ".mbox" ? splitMbox(bytes.toString("utf8")) : [bytes.toString("utf8")];
@@ -8392,14 +9004,14 @@ var init_mail_export = __esm(() => {
8392
9004
  });
8393
9005
 
8394
9006
  // src/datasource/skills/mailcrawl/config.ts
8395
- import { chmodSync, mkdirSync as mkdirSync7 } from "node:fs";
9007
+ import { chmodSync, mkdirSync as mkdirSync8 } from "node:fs";
8396
9008
  function validateMailcrawlInstanceId(instanceId) {
8397
9009
  if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/u.test(instanceId)) {
8398
9010
  throw new Error("mailcrawl instanceId must be a safe single path segment");
8399
9011
  }
8400
9012
  }
8401
9013
  function ensurePrivateMailcrawlDataDir(path) {
8402
- mkdirSync7(path, { recursive: true, mode: 448 });
9014
+ mkdirSync8(path, { recursive: true, mode: 448 });
8403
9015
  if (process.platform !== "win32")
8404
9016
  chmodSync(path, 448);
8405
9017
  }
@@ -8827,7 +9439,7 @@ class MailcrawlMethod {
8827
9439
  }
8828
9440
  }
8829
9441
  var modes, MAILCRAWL_MODES;
8830
- var init_methods4 = __esm(() => {
9442
+ var init_methods5 = __esm(() => {
8831
9443
  init_errors2();
8832
9444
  init_scope2();
8833
9445
  modes = ["bm25", "semantic", "hybrid"];
@@ -8977,515 +9589,17 @@ var init_skill9 = __esm(() => {
8977
9589
  init_scope2();
8978
9590
  init_client5();
8979
9591
  init_config3();
8980
- init_methods4();
9592
+ init_methods5();
8981
9593
  TAGS = ["mailcrawl", "email", "pii"];
8982
9594
  });
8983
9595
 
8984
9596
  // src/datasource/skills/mailcrawl/index.ts
8985
9597
  var init_mailcrawl = __esm(() => {
8986
9598
  init_client5();
8987
- init_methods4();
9599
+ init_methods5();
8988
9600
  init_skill9();
8989
9601
  });
8990
9602
 
8991
- // src/datasource/sidecar/fusion.ts
8992
- function encodeVector(vector) {
8993
- const floats = Float32Array.from(vector);
8994
- return new Uint8Array(floats.buffer, floats.byteOffset, floats.byteLength);
8995
- }
8996
- function decodeVector(blob) {
8997
- if (blob.byteLength % Float32Array.BYTES_PER_ELEMENT !== 0)
8998
- return;
8999
- return new Float32Array(new Uint8Array(blob).buffer);
9000
- }
9001
- function cosine2(a, b) {
9002
- if (a.length === 0 || a.length !== b.length)
9003
- return 0;
9004
- let dot = 0;
9005
- let normA = 0;
9006
- let normB = 0;
9007
- for (let index = 0;index < a.length; index += 1) {
9008
- const x = a[index] ?? 0;
9009
- const y = b[index] ?? 0;
9010
- dot += x * y;
9011
- normA += x * x;
9012
- normB += y * y;
9013
- }
9014
- if (normA === 0 || normB === 0)
9015
- return 0;
9016
- return dot / Math.sqrt(normA * normB);
9017
- }
9018
- function topKByCosine(query, rows, k) {
9019
- if (k <= 0)
9020
- return [];
9021
- const scored = [];
9022
- for (const row of rows) {
9023
- const score = cosine2(query, row.vector);
9024
- if (score > 0)
9025
- scored.push({ key: row.key, score });
9026
- }
9027
- scored.sort((a, b) => b.score - a.score || a.key.localeCompare(b.key));
9028
- return scored.slice(0, k);
9029
- }
9030
- function reciprocalRankFusion(lists) {
9031
- const scores = new Map;
9032
- for (const list of lists) {
9033
- const seen = new Set;
9034
- for (const [index, key] of list.entries()) {
9035
- if (seen.has(key))
9036
- continue;
9037
- seen.add(key);
9038
- scores.set(key, (scores.get(key) ?? 0) + 1 / (RRF_K + index + 1));
9039
- }
9040
- }
9041
- return [...scores.entries()].map(([key, score]) => ({ key, score })).sort((a, b) => b.score - a.score);
9042
- }
9043
- var RRF_K = 60;
9044
-
9045
- // src/datasource/sidecar/index.ts
9046
- import { createHash as createHash6 } from "node:crypto";
9047
- import { mkdirSync as mkdirSync8, statSync } from "node:fs";
9048
- import { dirname as dirname12 } from "node:path";
9049
- import { DatabaseSync } from "node:sqlite";
9050
- import { pathToFileURL } from "node:url";
9051
-
9052
- class CrawlerSidecar {
9053
- descriptor;
9054
- options;
9055
- select;
9056
- where;
9057
- opened;
9058
- attached;
9059
- lastSemantic;
9060
- constructor(options) {
9061
- this.options = options;
9062
- const { descriptor } = options;
9063
- this.descriptor = descriptor;
9064
- const metadataColumns = descriptor.metadata.map((column, index) => `${column.sql} as m${index}`);
9065
- this.select = [
9066
- `${descriptor.keyExpression} as k`,
9067
- `${descriptor.textExpression} as content`,
9068
- `${descriptor.titleExpression ?? "null"} as title`,
9069
- `${descriptor.epochSecondsExpression ?? "null"} as epoch`,
9070
- ...metadataColumns
9071
- ].join(", ");
9072
- const scope = descriptor.scopeExpression !== undefined && options.workspaceId !== undefined;
9073
- const allowlist = [
9074
- ...(options.channelIds?.length ?? 0) > 0 ? [`${descriptor.channelIdExpression} in (${options.channelIds?.map(() => "?").join(", ")})`] : [],
9075
- ...(options.channelNames?.length ?? 0) > 0 ? [`${descriptor.channelNameExpression} in (${options.channelNames?.map(() => "?").join(", ")})`] : []
9076
- ];
9077
- this.where = [
9078
- descriptor.liveWhere,
9079
- ...scope ? [`${descriptor.scopeExpression} = ?`] : [],
9080
- ...allowlist.length > 0 ? [`(${allowlist.join(" or ")})`] : []
9081
- ].join(" and ");
9082
- }
9083
- get db() {
9084
- if (this.opened !== undefined)
9085
- return this.opened;
9086
- mkdirSync8(dirname12(this.options.sidecarDbPath), { recursive: true });
9087
- const db = new DatabaseSync(this.options.sidecarDbPath);
9088
- db.exec(`pragma busy_timeout = ${BUSY_TIMEOUT_MS}`);
9089
- db.exec("pragma journal_mode = wal");
9090
- db.exec(`
9091
- create table if not exists sidecar_meta (k text primary key, v text not null);
9092
- create table if not exists sidecar_vec (message_key text primary key, hash text not null, vec blob not null);
9093
- `);
9094
- this.opened = db;
9095
- return db;
9096
- }
9097
- attachCrawler() {
9098
- let file;
9099
- try {
9100
- const { dev, ino } = statSync(this.options.crawlerDbPath);
9101
- file = { dev, ino };
9102
- } catch {
9103
- this.detachCrawler();
9104
- return false;
9105
- }
9106
- if (this.attached !== undefined) {
9107
- if (this.attached.dev === file.dev && this.attached.ino === file.ino)
9108
- return true;
9109
- this.detachCrawler();
9110
- }
9111
- const crawlerUri = `${pathToFileURL(this.options.crawlerDbPath).href}?mode=ro`.replaceAll("'", "''");
9112
- try {
9113
- this.db.exec(`attach database '${crawlerUri}' as crawler`);
9114
- } catch {
9115
- return false;
9116
- }
9117
- const { descriptor } = this.options;
9118
- try {
9119
- this.db.prepare(`select ${this.select} from ${descriptor.fromClause} where ${this.where} limit 0`).all(...this.whereParams());
9120
- this.db.prepare(`select 1 from ${descriptor.fts.table} limit 0`).all();
9121
- } catch {
9122
- this.db.exec("detach database crawler");
9123
- return false;
9124
- }
9125
- this.attached = file;
9126
- this.lastSemantic = undefined;
9127
- return true;
9128
- }
9129
- detachCrawler() {
9130
- if (this.attached === undefined)
9131
- return;
9132
- this.attached = undefined;
9133
- this.lastSemantic = undefined;
9134
- try {
9135
- this.opened?.exec("detach database crawler");
9136
- } catch {}
9137
- }
9138
- get semanticEnabled() {
9139
- return this.options.embedder !== undefined;
9140
- }
9141
- close() {
9142
- this.opened?.close();
9143
- this.opened = undefined;
9144
- this.attached = undefined;
9145
- this.lastSemantic = undefined;
9146
- }
9147
- whereParams() {
9148
- const { descriptor, workspaceId, channelIds = [], channelNames = [] } = this.options;
9149
- return [
9150
- ...descriptor.scopeExpression !== undefined && workspaceId !== undefined ? [workspaceId] : [],
9151
- ...channelIds,
9152
- ...channelNames
9153
- ];
9154
- }
9155
- searchLexical(query, topK) {
9156
- const match = toMatchQuery(query);
9157
- if (match === undefined || topK <= 0 || !this.attachCrawler())
9158
- return [];
9159
- const { descriptor } = this.options;
9160
- const rows = this.db.prepare(`select ${this.select}, bm25(${descriptor.fts.table}) as rank
9161
- from ${descriptor.fromClause} join ${descriptor.fts.table} on ${descriptor.fts.joinOn}
9162
- where ${descriptor.fts.table} match ? and ${this.where}
9163
- order by rank limit ?`).all(match, ...this.whereParams(), topK);
9164
- return rows.map((row) => this.toHit(this.toRow(row), -Number(row.rank)));
9165
- }
9166
- async searchSemantic(query, topK) {
9167
- const trimmed = query.trim();
9168
- if (this.options.embedder === undefined || trimmed.length === 0 || topK <= 0 || !this.attachCrawler())
9169
- return [];
9170
- const ranked = await this.rankSemantic(trimmed, topK * POOL_FACTOR);
9171
- if (ranked.length === 0)
9172
- return [];
9173
- const live = this.hydrate(ranked.map((entry) => entry.key));
9174
- const hits = [];
9175
- for (const entry of ranked) {
9176
- const row = live.get(entry.key);
9177
- if (row !== undefined)
9178
- hits.push(this.toHit(row, entry.score));
9179
- if (hits.length >= topK)
9180
- break;
9181
- }
9182
- return hits;
9183
- }
9184
- async rankSemantic(query, pool) {
9185
- const now = Date.now();
9186
- let cached = this.lastSemantic;
9187
- if (cached === undefined || cached.query !== query || now - cached.at > SEMANTIC_REUSE_MS) {
9188
- const embedder = this.options.embedder;
9189
- if (embedder === undefined)
9190
- return [];
9191
- cached = {
9192
- query,
9193
- at: now,
9194
- vector: embedder.embed([query]).then((vectors) => vectors[0], () => {
9195
- return;
9196
- }),
9197
- ranked: undefined
9198
- };
9199
- this.lastSemantic = cached;
9200
- }
9201
- const vector = await cached.vector;
9202
- if (vector === undefined) {
9203
- if (this.lastSemantic === cached)
9204
- this.lastSemantic = undefined;
9205
- return [];
9206
- }
9207
- if (cached.ranked === undefined || cached.ranked.pool < pool) {
9208
- const wide = pool * POOL_FACTOR;
9209
- cached.ranked = { pool: wide, keys: topKByCosine(vector, this.storedVectors(), wide) };
9210
- }
9211
- return cached.ranked.keys.slice(0, pool);
9212
- }
9213
- async searchHybrid(query, topK) {
9214
- if (topK <= 0)
9215
- return [];
9216
- const pool = topK * POOL_FACTOR;
9217
- const lexical = this.searchLexical(query, pool);
9218
- const semantic = await this.searchSemantic(query, pool);
9219
- if (semantic.length === 0)
9220
- return lexical.slice(0, topK);
9221
- const byKey = new Map;
9222
- for (const hit of [...semantic, ...lexical])
9223
- byKey.set(hit.key, hit);
9224
- const fused = [];
9225
- for (const entry of reciprocalRankFusion([lexical.map((hit) => hit.key), semantic.map((hit) => hit.key)])) {
9226
- const hit = byKey.get(entry.key);
9227
- if (hit !== undefined)
9228
- fused.push({ ...hit, score: entry.score });
9229
- if (fused.length >= topK)
9230
- break;
9231
- }
9232
- return fused;
9233
- }
9234
- async sync() {
9235
- const { embedder } = this.options;
9236
- if (embedder === undefined)
9237
- return { ok: false, message: "no embedder configured" };
9238
- if (!this.attachCrawler())
9239
- return { ok: false, message: "crawler archive not available yet" };
9240
- let identity;
9241
- try {
9242
- identity = await embedder.identity();
9243
- } catch (error) {
9244
- return { ok: false, message: errorMessage(error) };
9245
- }
9246
- this.adoptIdentity(identity);
9247
- const pruned = this.prune();
9248
- const pending = this.pendingKeys();
9249
- const batchSize = Math.max(1, this.options.batchSize ?? DEFAULT_BATCH_SIZE);
9250
- const budget = Math.max(1, this.options.maxEmbedPerSync ?? DEFAULT_MAX_EMBED_PER_SYNC);
9251
- const target = Math.min(pending.length, budget);
9252
- const embed = async (rows) => {
9253
- const vectors = await embedder.embed(rows.map((row) => row.input));
9254
- if (vectors.length !== rows.length) {
9255
- throw new Error(`embedder returned ${vectors.length} vectors for ${rows.length} messages`);
9256
- }
9257
- const wrong = vectors.find((vector) => vector.length !== identity.dimension);
9258
- if (wrong !== undefined) {
9259
- throw new Error(`embedder returned a ${wrong.length}-dimension vector, expected ${identity.dimension}`);
9260
- }
9261
- return vectors;
9262
- };
9263
- let embedded = 0;
9264
- let skipped = 0;
9265
- for (let offset = 0;offset < target; offset += batchSize) {
9266
- const rows = [...this.hydrate(pending.slice(offset, Math.min(offset + batchSize, target))).values()];
9267
- if (rows.length === 0)
9268
- continue;
9269
- try {
9270
- this.store(rows, await embed(rows));
9271
- embedded += rows.length;
9272
- } catch (batchError) {
9273
- const rejected = [];
9274
- let lastError = batchError;
9275
- for (const row of rows) {
9276
- try {
9277
- this.store([row], await embed([row]));
9278
- embedded += 1;
9279
- } catch (error) {
9280
- rejected.push(row);
9281
- lastError = error;
9282
- }
9283
- }
9284
- if (rejected.length === 0)
9285
- continue;
9286
- if (embedded === 0)
9287
- return { ok: false, message: errorMessage(lastError) };
9288
- this.store(rejected, undefined);
9289
- skipped += rejected.length;
9290
- }
9291
- }
9292
- return { ok: true, embedded, skipped, pruned, remaining: pending.length - embedded - skipped };
9293
- }
9294
- adoptIdentity(identity) {
9295
- const serialized = JSON.stringify(identity);
9296
- const current = this.db.prepare("select v from sidecar_meta where k = ?").get(IDENTITY_KEY);
9297
- if (current?.v === serialized)
9298
- return;
9299
- this.transaction(() => {
9300
- this.db.exec("delete from sidecar_vec");
9301
- this.db.prepare("insert or replace into sidecar_meta (k, v) values (?, ?)").run(IDENTITY_KEY, serialized);
9302
- });
9303
- this.lastSemantic = undefined;
9304
- }
9305
- prune() {
9306
- const { descriptor } = this.options;
9307
- const result = this.db.prepare(`delete from sidecar_vec where message_key not in (
9308
- select ${descriptor.keyExpression} from ${descriptor.fromClause} where ${this.where}
9309
- )`).run(...this.whereParams());
9310
- if (Number(result.changes) > 0)
9311
- this.lastSemantic = undefined;
9312
- return Number(result.changes);
9313
- }
9314
- pendingKeys() {
9315
- const { descriptor } = this.options;
9316
- const rows = this.db.prepare(`select ${this.select}, v.hash as stored_hash
9317
- from ${descriptor.fromClause}
9318
- left join sidecar_vec v on v.message_key = ${descriptor.keyExpression}
9319
- where ${this.where}`).iterate(...this.whereParams());
9320
- const pending = [];
9321
- for (const row of rows) {
9322
- if (row.stored_hash !== this.toRow(row).hash)
9323
- pending.push(String(row.k));
9324
- }
9325
- return pending;
9326
- }
9327
- hydrate(keys) {
9328
- const { descriptor } = this.options;
9329
- const out = new Map;
9330
- if (keys.length === 0)
9331
- return out;
9332
- const rows = this.db.prepare(`select ${this.select} from ${descriptor.fromClause}
9333
- where ${descriptor.keyExpression} in (${keys.map(() => "?").join(", ")}) and ${this.where}`).all(...keys, ...this.whereParams());
9334
- const byKey = new Map(rows.map((row) => [String(row.k), this.toRow(row)]));
9335
- for (const key of keys) {
9336
- const row = byKey.get(key);
9337
- if (row !== undefined)
9338
- out.set(key, row);
9339
- }
9340
- return out;
9341
- }
9342
- store(rows, vectors) {
9343
- const upsert = this.db.prepare("insert or replace into sidecar_vec (message_key, hash, vec) values (?, ?, ?)");
9344
- this.transaction(() => {
9345
- for (const [index, row] of rows.entries()) {
9346
- upsert.run(row.key, row.hash, encodeVector(vectors?.[index] ?? []));
9347
- }
9348
- });
9349
- this.lastSemantic = undefined;
9350
- }
9351
- *storedVectors() {
9352
- const rows = this.db.prepare("select message_key, vec from sidecar_vec where length(vec) > 0").iterate();
9353
- for (const row of rows) {
9354
- const vector = row.vec instanceof Uint8Array ? decodeVector(row.vec) : undefined;
9355
- if (vector !== undefined)
9356
- yield { key: String(row.message_key), vector };
9357
- }
9358
- }
9359
- transaction(work) {
9360
- this.db.exec("begin immediate");
9361
- try {
9362
- work();
9363
- this.db.exec("commit");
9364
- } catch (error) {
9365
- this.db.exec("rollback");
9366
- throw error;
9367
- }
9368
- }
9369
- toRow(row) {
9370
- const metadata = {};
9371
- for (const [index, column] of this.options.descriptor.metadata.entries()) {
9372
- const value = row[`m${index}`];
9373
- if (typeof value === "string" && value.length > 0 || typeof value === "number")
9374
- metadata[column.key] = String(value);
9375
- }
9376
- const title = typeof row.title === "string" && row.title.length > 0 ? row.title : undefined;
9377
- const epoch = typeof row.epoch === "number" && Number.isFinite(row.epoch) ? row.epoch : undefined;
9378
- const content = String(row.content ?? "");
9379
- const input = `${title ?? ""}
9380
- ${content}`.slice(0, MAX_EMBED_INPUT_CHARS);
9381
- return {
9382
- key: String(row.k),
9383
- content,
9384
- title,
9385
- epoch,
9386
- metadata,
9387
- input,
9388
- hash: createHash6("sha1").update(input).digest("hex")
9389
- };
9390
- }
9391
- toHit(row, score) {
9392
- return {
9393
- key: row.key,
9394
- content: row.content,
9395
- score,
9396
- ...row.title !== undefined ? { title: row.title } : {},
9397
- ...row.epoch !== undefined ? { publishedAt: Math.round(row.epoch * 1000) } : {},
9398
- metadata: row.metadata
9399
- };
9400
- }
9401
- }
9402
- function toMatchQuery(text) {
9403
- const terms = [...new Set(text.match(/[\p{L}\p{N}_]+/gu) ?? [])];
9404
- return terms.length === 0 ? undefined : terms.map((term) => `"${term}"`).join(" OR ");
9405
- }
9406
- function errorMessage(error) {
9407
- return error instanceof Error ? error.message : String(error);
9408
- }
9409
- 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";
9410
- var init_sidecar = () => {};
9411
-
9412
- // src/datasource/sidecar/methods.ts
9413
- function sidecarMethodName(datasourceId, mode) {
9414
- return mode === "bm25" ? `${datasourceId}-sidecar-bm25` : `${datasourceId}-${mode}`;
9415
- }
9416
-
9417
- class SidecarRetrievalMethod {
9418
- options;
9419
- constructor(options) {
9420
- this.options = options;
9421
- }
9422
- describe() {
9423
- const { datasourceId, skillType, mode, tags } = this.options;
9424
- const { type, text, capabilities } = DESCRIPTIONS[mode];
9425
- return {
9426
- name: sidecarMethodName(datasourceId, mode),
9427
- type,
9428
- description: `${text} over ${skillType} via a read-only sidecar index`,
9429
- status: "active",
9430
- capabilities: [...capabilities, "scoped", "path-opaque-sources"],
9431
- datasourceId,
9432
- tags
9433
- };
9434
- }
9435
- async retrieve(query, options) {
9436
- const trimmed = query.trim();
9437
- if (trimmed.length === 0)
9438
- return [];
9439
- const topK = options.topK ?? DEFAULT_TOP_K5;
9440
- const { sidecar, mode } = this.options;
9441
- const hits = mode === "bm25" ? sidecar.searchLexical(trimmed, topK) : mode === "semantic" ? await sidecar.searchSemantic(trimmed, topK) : await sidecar.searchHybrid(trimmed, topK);
9442
- const results = [];
9443
- for (const hit of hits) {
9444
- const result = this.toResult(hit, options);
9445
- if (result !== undefined)
9446
- results.push(result);
9447
- }
9448
- return results;
9449
- }
9450
- toResult(hit, options) {
9451
- const { descriptor, datasourceId, instanceId, mode } = this.options;
9452
- const chunkId = descriptor.chunkId(hit.key, hit.metadata);
9453
- const source = datasourceSourcePath(datasourceId, instanceId, chunkId);
9454
- if (!matchesDatasourceScope(source, options.scope))
9455
- return;
9456
- const method = sidecarMethodName(datasourceId, mode);
9457
- return {
9458
- id: `${datasourceId}:${instanceId}:${chunkId}`,
9459
- content: hit.content,
9460
- source,
9461
- score: hit.score,
9462
- metadata: {
9463
- ...hit.metadata,
9464
- method,
9465
- datasourceId,
9466
- instanceId,
9467
- backend: "sidecar",
9468
- ...hit.title !== undefined ? { title: hit.title } : {},
9469
- hierarchy: descriptor.hierarchy(hit.metadata).join("/"),
9470
- ...hit.publishedAt !== undefined ? { publishedAt: hit.publishedAt } : {}
9471
- }
9472
- };
9473
- }
9474
- }
9475
- var DEFAULT_TOP_K5 = 20, DESCRIPTIONS;
9476
- var init_methods5 = __esm(() => {
9477
- init_scope2();
9478
- DESCRIPTIONS = {
9479
- bm25: { type: "bm25", text: "BM25-ranked FTS5 retrieval", capabilities: ["lexical", "fts5", "bm25"] },
9480
- semantic: { type: "vector", text: "Semantic (embedding) retrieval", capabilities: ["semantic"] },
9481
- hybrid: {
9482
- type: "hybrid",
9483
- text: "Hybrid BM25 + semantic retrieval (reciprocal rank fusion)",
9484
- capabilities: ["lexical", "semantic", "hybrid"]
9485
- }
9486
- };
9487
- });
9488
-
9489
9603
  // src/datasource/crawler-skill.ts
9490
9604
  class CrawlerDatasourceSkill {
9491
9605
  definition;
@@ -9766,7 +9880,7 @@ var init_crawler_skill = __esm(() => {
9766
9880
  init_errors2();
9767
9881
  init_scope2();
9768
9882
  init_sidecar();
9769
- init_methods5();
9883
+ init_methods4();
9770
9884
  DEFAULT_POLLING_INTERVAL_MS4 = 15 * 60 * 1000;
9771
9885
  });
9772
9886
 
@@ -11555,14 +11669,27 @@ var init_factory = __esm(() => {
11555
11669
  ...config.channels?.ids !== undefined ? { chatIds: config.channels.ids } : {},
11556
11670
  ...larkConnectorOptions(config.connector)
11557
11671
  }),
11558
- kakao: (config, _workspaceRoot, _registrationName) => new LazykatokSkill({
11559
- client: new LazykatokClient({
11560
- ...config.connector
11561
- }),
11562
- ...config.instanceId !== undefined ? { instanceId: config.instanceId } : {},
11563
- ...config.pollingIntervalMs !== undefined ? { pollingIntervalMs: config.pollingIntervalMs } : {},
11564
- ...config.tags !== undefined ? { tags: config.tags } : {}
11565
- }),
11672
+ kakao: (config, workspaceRoot, registrationName) => {
11673
+ const connector = config.connector ?? {};
11674
+ const dataDir = typeof connector.workspacePath === "string" && connector.workspacePath.length > 0 ? connector.workspacePath : join16(homedir4(), "Library", "Application Support", "katok");
11675
+ const sidecar = resolveSidecarOptions({
11676
+ descriptor: CRAWLER_DESCRIPTORS.kakao,
11677
+ connector: config.connector,
11678
+ workspaceRoot,
11679
+ registrationName,
11680
+ instanceId: config.instanceId ?? "default",
11681
+ defaultCrawlerDb: join16(dataDir, "archive.sqlite3")
11682
+ });
11683
+ return new LazykatokSkill({
11684
+ client: new LazykatokClient(connector),
11685
+ ...config.instanceId !== undefined ? { instanceId: config.instanceId } : {},
11686
+ ...config.pollingIntervalMs !== undefined ? { pollingIntervalMs: config.pollingIntervalMs } : {},
11687
+ ...config.tags !== undefined ? { tags: config.tags } : {},
11688
+ channelIds: config.channels?.ids,
11689
+ channelNames: config.channels?.names,
11690
+ ...sidecar === undefined ? {} : { sidecar }
11691
+ });
11692
+ },
11566
11693
  github: (config, workspaceRoot, registrationName) => new GitHubSkill({
11567
11694
  ...common(config, workspaceRoot),
11568
11695
  skillName: registrationName,
@@ -12227,7 +12354,7 @@ function migrateLegacyConfig(configPath, legacyPath) {
12227
12354
  const legacy = readConfigFile(legacyPath, true);
12228
12355
  const legacyBytes = readFileSync14(legacyPath);
12229
12356
  const migrated = normalizeLegacyConfigPaths(legacy ?? {}, dirname13(legacyPath));
12230
- const migratedPathsUnchanged = legacy?.workspacePath === migrated.workspacePath && legacy?.memoryPath === migrated.memoryPath && JSON.stringify(legacy?.searchPaths) === JSON.stringify(migrated.searchPaths);
12357
+ 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);
12231
12358
  const migratedBytes = migratedPathsUnchanged ? legacyBytes : `${JSON.stringify(migrated, null, 2)}
12232
12359
  `;
12233
12360
  mkdirSync11(dirname13(configPath), { recursive: true });
@@ -12265,15 +12392,25 @@ function resolveConfigFileReadOnly(configPath, explicit, legacyPath) {
12265
12392
  function resolveSearchPaths(searchPaths, origin) {
12266
12393
  return searchPaths.map((searchPath) => resolvePersistedPath(searchPath, origin));
12267
12394
  }
12395
+ function resolveExcludePaths(value, origin) {
12396
+ if (value === undefined)
12397
+ return;
12398
+ if (!Array.isArray(value) || value.some((path) => typeof path !== "string" || path.length === 0)) {
12399
+ throw new ConfigError("Config field 'excludePaths' must be a non-empty string array");
12400
+ }
12401
+ return resolveSearchPaths(value, origin);
12402
+ }
12268
12403
  function resolvePersistedPath(path, origin) {
12269
12404
  return isAbsolute5(path) ? path : resolve7(origin, path);
12270
12405
  }
12271
12406
  function normalizeLegacyConfigPaths(partial, origin) {
12272
12407
  const workspacePath = resolvePersistedPath(partial.workspacePath ?? ".", origin);
12408
+ const excludePaths = resolveExcludePaths(partial.excludePaths, workspacePath);
12273
12409
  return {
12274
12410
  ...partial,
12275
12411
  workspacePath,
12276
12412
  ...partial.searchPaths === undefined ? {} : { searchPaths: resolveSearchPaths(partial.searchPaths, workspacePath) },
12413
+ ...excludePaths === undefined ? {} : { excludePaths },
12277
12414
  ...partial.memoryPath === undefined ? {} : { memoryPath: resolvePersistedPath(partial.memoryPath, workspacePath) }
12278
12415
  };
12279
12416
  }
@@ -12728,10 +12865,7 @@ function resolveConfig(input) {
12728
12865
  const configOrigin = dirname13(resolve7(configPath));
12729
12866
  const fileWorkspacePath = typeof file.workspacePath === "string" ? resolvePersistedPath(file.workspacePath, configOrigin) : undefined;
12730
12867
  const fileSearchPaths = file.searchPaths ? resolveSearchPaths(file.searchPaths, fileWorkspacePath ?? configOrigin) : undefined;
12731
- if (file.excludePaths !== undefined && (!Array.isArray(file.excludePaths) || file.excludePaths.some((path) => typeof path !== "string" || path.length === 0))) {
12732
- throw new ConfigError("Config field 'excludePaths' must be a non-empty string array");
12733
- }
12734
- const fileExcludePaths = file.excludePaths ? resolveSearchPaths(file.excludePaths, fileWorkspacePath ?? configOrigin) : undefined;
12868
+ const fileExcludePaths = resolveExcludePaths(file.excludePaths, fileWorkspacePath ?? configOrigin);
12735
12869
  const searchPaths = flagSearchPaths ?? envSearchPaths ?? fileSearchPaths ?? defaultSearchPaths;
12736
12870
  const flagWorkspacePath = flagString(flags, "workspace");
12737
12871
  const envWorkspacePath = envString(env, "AUTORAG_WORKSPACE");
@@ -13467,7 +13601,7 @@ function writeDefaultConfig(path, partial, opts = {}) {
13467
13601
  full.dupey = partial.dupey ?? { enabled: true };
13468
13602
  full.excludeExactDuplicates = partial.excludeExactDuplicates ?? true;
13469
13603
  if (partial.excludePaths !== undefined)
13470
- full.excludePaths = resolveSearchPaths(partial.excludePaths, workspacePath);
13604
+ full.excludePaths = resolveExcludePaths(partial.excludePaths, workspacePath);
13471
13605
  if (partial.limits !== undefined)
13472
13606
  full.limits = normalizeLimitsConfig(partial.limits);
13473
13607
  if (partial.refresh !== undefined)
@@ -13611,7 +13745,7 @@ var init_config4 = __esm(() => {
13611
13745
 
13612
13746
  // src/cli/commands/init.ts
13613
13747
  import { existsSync as existsSync11, readFileSync as readFileSync15 } from "node:fs";
13614
- import { dirname as dirname14 } from "node:path";
13748
+ import { dirname as dirname14, resolve as resolve8 } from "node:path";
13615
13749
  async function runInit(ctx) {
13616
13750
  const flags = ctx.flags;
13617
13751
  const partial = {};
@@ -13619,6 +13753,14 @@ async function runInit(ctx) {
13619
13753
  if (searchPathsFlag) {
13620
13754
  partial.searchPaths = searchPathsFlag.split(",").map((entry) => entry.trim()).filter((entry) => entry.length > 0);
13621
13755
  }
13756
+ const excludedPaths = ctx.repeatedFlags?.exclude ?? (typeof flags.exclude === "string" ? [flags.exclude] : undefined);
13757
+ if (flags.exclude !== undefined || excludedPaths !== undefined) {
13758
+ if (excludedPaths === undefined || excludedPaths.length === 0 || excludedPaths.some((path) => path.trim().length === 0)) {
13759
+ ctx.stderr(renderError(new ConfigError("--exclude requires a non-empty path"), { json: ctx.json, debug: ctx.debug }));
13760
+ return 2;
13761
+ }
13762
+ partial.excludePaths = excludedPaths.map((path) => resolve8(ctx.cwd, path));
13763
+ }
13622
13764
  if (typeof flags.workspace === "string")
13623
13765
  partial.workspacePath = flags.workspace;
13624
13766
  if (typeof flags["memory-path"] === "string")
@@ -13683,6 +13825,7 @@ async function runInit(ctx) {
13683
13825
  const legacy = JSON.parse(readFileSync15(legacyPath, "utf8"));
13684
13826
  const normalizedLegacy = normalizeLegacyConfigPaths(legacy, dirname14(legacyPath));
13685
13827
  partial.searchPaths ??= normalizedLegacy.searchPaths;
13828
+ partial.excludePaths ??= normalizedLegacy.excludePaths;
13686
13829
  partial.workspacePath ??= normalizedLegacy.workspacePath;
13687
13830
  partial.memoryPath ??= normalizedLegacy.memoryPath;
13688
13831
  partial.model ??= legacy.model;
@@ -14265,9 +14408,9 @@ var init_retry = __esm(() => {
14265
14408
 
14266
14409
  // src/dupey/cli.ts
14267
14410
  import { spawn as spawn10 } from "node:child_process";
14268
- import { resolve as resolve8 } from "node:path";
14411
+ import { resolve as resolve9 } from "node:path";
14269
14412
  async function scanWithDupey(dir, options = {}) {
14270
- const absoluteDir = resolve8(dir);
14413
+ const absoluteDir = resolve9(dir);
14271
14414
  const stdout = options.run ? await options.run(["scan", absoluteDir, "--json"]) : await spawnDupey(options.executable ?? "dupey", ["scan", absoluteDir, "--json"], options);
14272
14415
  let parsed;
14273
14416
  try {
@@ -14337,13 +14480,13 @@ var init_cli = __esm(() => {
14337
14480
 
14338
14481
  // src/dupey/filter.ts
14339
14482
  import { stat as stat5 } from "node:fs/promises";
14340
- import { resolve as resolve9 } from "node:path";
14483
+ import { resolve as resolve10 } from "node:path";
14341
14484
  async function selectExactDuplicateExclusions(root, scan, isUnavailable) {
14342
14485
  const byHash = new Map;
14343
14486
  for (const file of scan.files) {
14344
14487
  if (typeof file.content_hash !== "string" || file.content_hash.length === 0)
14345
14488
  continue;
14346
- const path = resolve9(root, file.path);
14489
+ const path = resolve10(root, file.path);
14347
14490
  const group = byHash.get(file.content_hash) ?? [];
14348
14491
  group.push(path);
14349
14492
  byHash.set(file.content_hash, group);
@@ -14380,7 +14523,7 @@ function dupeyExactRanking(scan) {
14380
14523
  return;
14381
14524
  const path = entry.path;
14382
14525
  if (typeof path === "string")
14383
- ranking.set(resolve9(path), index);
14526
+ ranking.set(resolve10(path), index);
14384
14527
  });
14385
14528
  }
14386
14529
  return ranking;
@@ -14536,14 +14679,33 @@ function errorMessage2(error) {
14536
14679
  var MANIFEST_FILENAME = "manifest.json";
14537
14680
  var init_bundle = () => {};
14538
14681
 
14682
+ // src/filesystem/path-exclusions.ts
14683
+ import { posix as posix2, win32 as win322 } from "node:path";
14684
+ function createPathExclusionMatcher(excludedPaths, platform = process.platform) {
14685
+ const paths = platform === "win32" ? win322 : posix2;
14686
+ const normalize = (path) => {
14687
+ const absolute = paths.resolve(path);
14688
+ return platform === "win32" ? absolute.toLowerCase() : absolute;
14689
+ };
14690
+ const excluded = excludedPaths.map((path) => {
14691
+ const absolute = normalize(path);
14692
+ return { absolute, prefix: absolute.endsWith(paths.sep) ? absolute : `${absolute}${paths.sep}` };
14693
+ });
14694
+ return (path) => {
14695
+ const absolute = normalize(path);
14696
+ return excluded.some((entry) => absolute === entry.absolute || absolute.startsWith(entry.prefix));
14697
+ };
14698
+ }
14699
+ var init_path_exclusions = () => {};
14700
+
14539
14701
  // src/everything/client.ts
14540
14702
  import { spawn as spawn11 } from "node:child_process";
14541
14703
  import { createHash as createHash9 } from "node:crypto";
14542
14704
  import { mkdirSync as mkdirSync13, writeFileSync as writeFileSync11 } from "node:fs";
14543
- import { join as join22, resolve as resolve10 } from "node:path";
14705
+ import { join as join22, resolve as resolve11 } from "node:path";
14544
14706
  import { setTimeout as sleep } from "node:timers/promises";
14545
14707
  function everythingInstanceName(root) {
14546
- return `autorag-${createHash9("sha256").update(resolve10(root).toLowerCase()).digest("hex").slice(0, 12)}`;
14708
+ return `autorag-${createHash9("sha256").update(resolve11(root).toLowerCase()).digest("hex").slice(0, 12)}`;
14547
14709
  }
14548
14710
  function buildEverythingIni(input) {
14549
14711
  const list = (values) => values.map((value) => `"${value.replace(/\\/g, "\\\\")}"`).join(",");
@@ -14653,6 +14815,7 @@ class EverythingClient {
14653
14815
  run;
14654
14816
  launch;
14655
14817
  stateDir;
14818
+ isExcludedPath;
14656
14819
  binaries;
14657
14820
  operation = Promise.resolve();
14658
14821
  constructor(options) {
@@ -14662,6 +14825,7 @@ class EverythingClient {
14662
14825
  this.run = options.run ?? runProcess;
14663
14826
  this.launch = options.launch ?? launchDetached;
14664
14827
  this.stateDir = join22(options.root, ".autorag", "everything");
14828
+ this.isExcludedPath = createPathExclusionMatcher(options.excludeFolders ?? [], "win32");
14665
14829
  }
14666
14830
  isSupported() {
14667
14831
  return this.platform === "win32";
@@ -14681,6 +14845,9 @@ class EverythingClient {
14681
14845
  }
14682
14846
  const es = { ok: true, esPath: binaries.esPath };
14683
14847
  const timeoutMs = this.options.timeoutMs ?? DEFAULT_TIMEOUT_MS6;
14848
+ if ((this.options.excludeFolders?.length ?? 0) > 0) {
14849
+ return this.searchWithExclusions(es.esPath, request, timeoutMs);
14850
+ }
14684
14851
  const args = buildEverythingSearchArgs(this.instanceName, request, timeoutMs);
14685
14852
  const result = await this.run(es.esPath, args, timeoutMs + 5000);
14686
14853
  if (result.code !== 0)
@@ -14692,6 +14859,50 @@ class EverythingClient {
14692
14859
  }
14693
14860
  });
14694
14861
  }
14862
+ async searchWithExclusions(esPath, request, timeoutMs) {
14863
+ const deadline = Date.now() + timeoutMs;
14864
+ const offset = request.offset ?? 0;
14865
+ const maxResults = request.maxResults ?? DEFAULT_MAX_RESULTS;
14866
+ const results = [];
14867
+ let skipped = 0;
14868
+ let nativeOffset = 0;
14869
+ const timedOut = () => ({
14870
+ ok: false,
14871
+ reason: "search-failed",
14872
+ message: `Everything search exceeded ${timeoutMs}ms while filtering excluded paths; narrow the query or increase the Everything timeout.`
14873
+ });
14874
+ for (;; ) {
14875
+ const remainingMs = deadline - Date.now();
14876
+ if (remainingMs <= 0)
14877
+ return timedOut();
14878
+ const args = buildEverythingSearchArgs(this.instanceName, { ...request, offset: nativeOffset, maxResults: MAX_FILTER_CANDIDATES }, remainingMs);
14879
+ const response = await this.run(esPath, args, remainingMs + 5000);
14880
+ if (response.code !== 0)
14881
+ return this.failure("search-failed", esPath, args, response);
14882
+ if (Date.now() >= deadline)
14883
+ return timedOut();
14884
+ let entries;
14885
+ try {
14886
+ entries = parseEverythingJson(response.stdout);
14887
+ } catch (error) {
14888
+ return { ok: false, reason: "search-failed", message: error.message };
14889
+ }
14890
+ for (const entry of entries) {
14891
+ if (this.isExcludedPath(entry.path))
14892
+ continue;
14893
+ if (skipped < offset) {
14894
+ skipped += 1;
14895
+ continue;
14896
+ }
14897
+ results.push(entry);
14898
+ if (results.length >= maxResults)
14899
+ return { ok: true, results };
14900
+ }
14901
+ if (entries.length < MAX_FILTER_CANDIDATES)
14902
+ return { ok: true, results };
14903
+ nativeOffset += entries.length;
14904
+ }
14905
+ }
14695
14906
  index() {
14696
14907
  return this.serialize(async () => {
14697
14908
  const es = await this.ensureRunning(true);
@@ -14765,7 +14976,8 @@ class EverythingClient {
14765
14976
  excludeFolders: [
14766
14977
  ...new Set([
14767
14978
  join22(this.options.root, ".autorag"),
14768
- ...this.options.folders.map((folder) => join22(folder, ".autorag"))
14979
+ ...this.options.folders.map((folder) => join22(folder, ".autorag")),
14980
+ ...this.options.excludeFolders ?? []
14769
14981
  ])
14770
14982
  ]
14771
14983
  }));
@@ -14859,8 +15071,9 @@ function launchDetached(command, args) {
14859
15071
  child.on("error", () => {});
14860
15072
  child.unref();
14861
15073
  }
14862
- var DEFAULT_TIMEOUT_MS6 = 30000, DEFAULT_STARTUP_TIMEOUT_MS = 20000, DEFAULT_INDEX_TIMEOUT_MS2, DEFAULT_POLL_INTERVAL_MS = 250, DEFAULT_MAX_RESULTS = 100, MAX_OUTPUT_BYTES, VERSION_PATTERN;
15074
+ var DEFAULT_TIMEOUT_MS6 = 30000, DEFAULT_STARTUP_TIMEOUT_MS = 20000, DEFAULT_INDEX_TIMEOUT_MS2, DEFAULT_POLL_INTERVAL_MS = 250, DEFAULT_MAX_RESULTS = 100, MAX_FILTER_CANDIDATES = 1000, MAX_OUTPUT_BYTES, VERSION_PATTERN;
14863
15075
  var init_client7 = __esm(() => {
15076
+ init_path_exclusions();
14864
15077
  init_bundle();
14865
15078
  DEFAULT_INDEX_TIMEOUT_MS2 = 10 * 60000;
14866
15079
  MAX_OUTPUT_BYTES = 8 * 1024 * 1024;
@@ -14880,6 +15093,7 @@ function compare(a, b) {
14880
15093
  return a < b ? -1 : a > b ? 1 : 0;
14881
15094
  }
14882
15095
  async function walkFileSearch(folders, request, options = {}) {
15096
+ const isExcluded = createPathExclusionMatcher(options.excludeFolders ?? []);
14883
15097
  const maxVisited = options.maxVisited ?? DEFAULT_MAX_VISITED;
14884
15098
  const maxMatches = options.maxMatches ?? DEFAULT_MAX_MATCHES;
14885
15099
  const deadline = Date.now() + (options.deadlineMs ?? DEFAULT_DEADLINE_MS);
@@ -14902,6 +15116,8 @@ async function walkFileSearch(folders, request, options = {}) {
14902
15116
  outer:
14903
15117
  while (pending.length > 0) {
14904
15118
  const dir = pending.pop();
15119
+ if (isExcluded(dir))
15120
+ continue;
14905
15121
  if (Date.now() >= deadline) {
14906
15122
  truncated = true;
14907
15123
  break;
@@ -14919,6 +15135,8 @@ async function walkFileSearch(folders, request, options = {}) {
14919
15135
  }
14920
15136
  visited += 1;
14921
15137
  const path = join23(dir, dirent.name);
15138
+ if (isExcluded(path))
15139
+ continue;
14922
15140
  if (dirent.isDirectory()) {
14923
15141
  if (dirent.name === ".autorag")
14924
15142
  continue;
@@ -14953,6 +15171,8 @@ async function walkFileSearch(folders, request, options = {}) {
14953
15171
  }
14954
15172
  }
14955
15173
  for (const folder of folders) {
15174
+ if (isExcluded(folder))
15175
+ continue;
14956
15176
  if (!underPath(folder) || !matches(folder))
14957
15177
  continue;
14958
15178
  if (request.kind === "files")
@@ -14979,6 +15199,7 @@ async function walkFileSearch(folders, request, options = {}) {
14979
15199
  }
14980
15200
  var DEFAULT_MAX_VISITED = 500000, DEFAULT_DEADLINE_MS = 20000, DEFAULT_MAX_MATCHES = 1e4, SORT_ORDERS;
14981
15201
  var init_walk = __esm(() => {
15202
+ init_path_exclusions();
14982
15203
  SORT_ORDERS = {
14983
15204
  "name-ascending": (a, b) => compare(a.name, b.name),
14984
15205
  "name-descending": (a, b) => compare(b.name, a.name),
@@ -14995,14 +15216,14 @@ var init_walk = __esm(() => {
14995
15216
  import { spawn as spawn12 } from "node:child_process";
14996
15217
  import { createHash as createHash10 } from "node:crypto";
14997
15218
  import { existsSync as existsSync15, mkdirSync as mkdirSync14, readFileSync as readFileSync18, realpathSync as realpathSync2, rmSync as rmSync4, writeFileSync as writeFileSync12 } from "node:fs";
14998
- import { basename as basename9, join as join24, resolve as resolve11, sep as sep4 } from "node:path";
15219
+ import { basename as basename9, join as join24, resolve as resolve12, sep as sep4 } from "node:path";
14999
15220
  import { setTimeout as sleep2 } from "node:timers/promises";
15000
15221
  function fsearchSocketPath(dbPath) {
15001
15222
  let canonical;
15002
15223
  try {
15003
15224
  canonical = realpathSync2(dbPath);
15004
15225
  } catch {
15005
- canonical = resolve11(dbPath);
15226
+ canonical = resolve12(dbPath);
15006
15227
  }
15007
15228
  const hash = createHash10("sha256").update(canonical).digest("hex").slice(0, 12);
15008
15229
  return `/tmp/autorag-fsearch-${process.getuid?.() ?? 0}-${hash}.sock`;
@@ -15088,6 +15309,7 @@ class FSearchClient {
15088
15309
  launch;
15089
15310
  isAlive;
15090
15311
  kill;
15312
+ isExcludedPath;
15091
15313
  stateDir;
15092
15314
  dbPath;
15093
15315
  pidPath;
@@ -15100,6 +15322,7 @@ class FSearchClient {
15100
15322
  this.launch = options.launch ?? launchDetached2;
15101
15323
  this.isAlive = options.isProcessAlive ?? defaultIsProcessAlive;
15102
15324
  this.kill = options.killProcess ?? ((pid) => process.kill(pid, "SIGTERM"));
15325
+ this.isExcludedPath = createPathExclusionMatcher(options.excludeFolders ?? []);
15103
15326
  this.stateDir = join24(options.root, ".autorag", "fsearch");
15104
15327
  this.dbPath = join24(this.stateDir, "fsearch.db");
15105
15328
  this.pidPath = join24(this.stateDir, "watch.pid");
@@ -15117,6 +15340,7 @@ class FSearchClient {
15117
15340
  if (unavailable !== undefined || !binary.ok) {
15118
15341
  try {
15119
15342
  const walked = await walkFileSearch(this.options.folders, request, {
15343
+ excludeFolders: this.options.excludeFolders,
15120
15344
  maxVisited: this.options.walkMaxVisited,
15121
15345
  deadlineMs: this.options.walkDeadlineMs
15122
15346
  });
@@ -15131,7 +15355,9 @@ class FSearchClient {
15131
15355
  return { ok: false, reason: "search-failed", message: error.message };
15132
15356
  }
15133
15357
  }
15134
- const args = buildFSearchSearchArgs(this.dbPath, this.socketPath(), request);
15358
+ const hasExclusions = (this.options.excludeFolders?.length ?? 0) > 0;
15359
+ const nativeRequest = hasExclusions ? { ...request, offset: 0, maxResults: MAX_RESULTS_CAP } : request;
15360
+ const args = buildFSearchSearchArgs(this.dbPath, this.socketPath(), nativeRequest);
15135
15361
  const result = await this.run(binary.path, args, this.options.timeoutMs ?? DEFAULT_TIMEOUT_MS7);
15136
15362
  if (result.code !== 0)
15137
15363
  return this.failure("search-failed", binary.path, args, result);
@@ -15142,6 +15368,10 @@ class FSearchClient {
15142
15368
  return { ok: false, reason: "search-failed", message: error.message };
15143
15369
  }
15144
15370
  let entries = parsed.entries;
15371
+ if (hasExclusions) {
15372
+ const isExcluded = this.isExcludedPath;
15373
+ entries = entries.filter((entry) => !isExcluded(entry.path));
15374
+ }
15145
15375
  if (request.path !== undefined) {
15146
15376
  const prefix = request.path.endsWith(sep4) ? request.path : `${request.path}${sep4}`;
15147
15377
  entries = entries.filter((entry) => entry.path === request.path || entry.path.startsWith(prefix));
@@ -15151,7 +15381,16 @@ class FSearchClient {
15151
15381
  const maxResults = Math.min(request.maxResults ?? DEFAULT_MAX_RESULTS2, MAX_RESULTS_CAP);
15152
15382
  if (entries.length > maxResults)
15153
15383
  entries = entries.slice(0, maxResults);
15154
- return { ok: true, backend: "fsearch-cli", results: entries, total: parsed.total };
15384
+ return {
15385
+ ok: true,
15386
+ backend: "fsearch-cli",
15387
+ results: entries,
15388
+ total: parsed.total,
15389
+ ...hasExclusions && parsed.total !== undefined && parsed.total > parsed.entries.length && entries.length < maxResults ? {
15390
+ truncated: true,
15391
+ 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.`
15392
+ } : {}
15393
+ };
15155
15394
  });
15156
15395
  }
15157
15396
  index() {
@@ -15345,6 +15584,7 @@ function defaultIsProcessAlive(pid) {
15345
15584
  }
15346
15585
  var DEFAULT_TIMEOUT_MS7 = 30000, DEFAULT_STARTUP_TIMEOUT_MS2 = 1e4, STOP_TIMEOUT_MS = 2000, DEFAULT_INDEX_TIMEOUT_MS3, DEFAULT_POLL_INTERVAL_MS2 = 250, DEFAULT_MAX_RESULTS2 = 100, MAX_RESULTS_CAP = 1000, MAX_OUTPUT_BYTES2, SORT_KEYS;
15347
15586
  var init_client8 = __esm(() => {
15587
+ init_path_exclusions();
15348
15588
  init_walk();
15349
15589
  DEFAULT_INDEX_TIMEOUT_MS3 = 10 * 60000;
15350
15590
  MAX_OUTPUT_BYTES2 = 8 * 1024 * 1024;
@@ -15416,7 +15656,7 @@ var init_diagnostics = __esm(() => {
15416
15656
 
15417
15657
  // src/jikji/answer-pack.ts
15418
15658
  import { realpathSync as realpathSync3 } from "node:fs";
15419
- import { isAbsolute as isAbsolute7, join as join25, relative as relative3, resolve as resolve12 } from "node:path";
15659
+ import { isAbsolute as isAbsolute7, join as join25, relative as relative3, resolve as resolve13 } from "node:path";
15420
15660
  function isRecord10(value) {
15421
15661
  return typeof value === "object" && value !== null && !Array.isArray(value);
15422
15662
  }
@@ -15546,7 +15786,7 @@ function normalizeJikjiAnswerPath(rawPath, sourceRoots) {
15546
15786
  } else {
15547
15787
  if (normalized.split("/").includes(".."))
15548
15788
  continue;
15549
- candidate = resolve12(realRoot, normalized);
15789
+ candidate = resolve13(realRoot, normalized);
15550
15790
  }
15551
15791
  let realCandidate;
15552
15792
  try {
@@ -15983,7 +16223,7 @@ var init_client9 = __esm(() => {
15983
16223
  });
15984
16224
 
15985
16225
  // src/jikji/path-map.ts
15986
- import { isAbsolute as isAbsolute8, relative as relative4, resolve as resolve13, sep as sep5 } from "node:path";
16226
+ import { isAbsolute as isAbsolute8, relative as relative4, resolve as resolve14, sep as sep5 } from "node:path";
15987
16227
  function planJikjiSourceRoots(searchPaths) {
15988
16228
  return planSourceRoots(searchPaths).map((sourceRoot) => ({
15989
16229
  ...sourceRoot,
@@ -15991,7 +16231,7 @@ function planJikjiSourceRoots(searchPaths) {
15991
16231
  }));
15992
16232
  }
15993
16233
  function outermostJikjiRoots(searchPaths) {
15994
- const unique = [...new Set(searchPaths.map((searchPath) => resolve13(searchPath)))];
16234
+ const unique = [...new Set(searchPaths.map((searchPath) => resolve14(searchPath)))];
15995
16235
  return unique.filter((candidate) => !unique.some((other) => {
15996
16236
  if (other === candidate)
15997
16237
  return false;
@@ -16012,13 +16252,13 @@ function resolveReturnedPath(rootPath, returnedPath) {
16012
16252
  return;
16013
16253
  if (normalized.split("/").includes(".."))
16014
16254
  return;
16015
- return resolve13(rootPath, normalized);
16255
+ return resolve14(rootPath, normalized);
16016
16256
  }
16017
16257
  function isUnsafeReturnedPath(normalizedPath) {
16018
16258
  return normalizedPath.startsWith("/") || /^[A-Za-z]:\//.test(normalizedPath) || normalizedPath.startsWith("//") || /^[A-Za-z][A-Za-z0-9+.-]*:\/\//.test(normalizedPath);
16019
16259
  }
16020
16260
  function isWithinRoot(rootPath, sourcePath) {
16021
- const rel = relative4(resolve13(rootPath), resolve13(sourcePath));
16261
+ const rel = relative4(resolve14(rootPath), resolve14(sourcePath));
16022
16262
  return rel === "" || !rel.startsWith("..") && !rel.includes(`..${sep5}`) && !isAbsolute8(rel);
16023
16263
  }
16024
16264
  var init_path_map = __esm(() => {
@@ -17388,7 +17628,8 @@ class MinSyncClient {
17388
17628
  restoreConfig();
17389
17629
  return this.lockHeldResult(result.stderr);
17390
17630
  }
17391
- if (!result.ok) {
17631
+ const partial = !result.ok && result.code === MINSYNC_PARTIAL_SYNC_EXIT_CODE ? readPartialSync(result.stdout) : undefined;
17632
+ if (!result.ok && partial === undefined) {
17392
17633
  restoreConfig();
17393
17634
  return {
17394
17635
  ok: false,
@@ -17408,10 +17649,12 @@ class MinSyncClient {
17408
17649
  }
17409
17650
  if (effective.identity)
17410
17651
  this.writeIdentity(effective.identity);
17652
+ const filesFailed = partial?.filesFailed ?? [];
17411
17653
  return {
17412
17654
  ok: true,
17413
17655
  synced: readSyncedCount(result.stdout),
17414
17656
  workspacePath: this.workspacePath,
17657
+ ...filesFailed.length > 0 ? { filesFailed } : {},
17415
17658
  ...identityChanged ? {
17416
17659
  diagnostic: {
17417
17660
  code: "embedding-identity-mismatch",
@@ -17443,6 +17686,19 @@ function readConfigSnapshot(configPath) {
17443
17686
  throw error;
17444
17687
  }
17445
17688
  }
17689
+ function readPartialSync(stdout) {
17690
+ const parsed = parseJson7(stdout);
17691
+ if (!isRecord11(parsed) || parsed.partial !== true)
17692
+ return;
17693
+ const failures = Array.isArray(parsed.files_failed) ? parsed.files_failed : [];
17694
+ return {
17695
+ filesFailed: failures.filter(isRecord11).map((failure) => ({
17696
+ path: typeof failure.path === "string" ? failure.path : "(unknown)",
17697
+ stage: typeof failure.stage === "string" ? failure.stage : "unknown",
17698
+ error: typeof failure.error === "string" ? failure.error : "unknown error"
17699
+ }))
17700
+ };
17701
+ }
17446
17702
  function readSyncedCount(stdout) {
17447
17703
  const parsed = parseJson7(stdout);
17448
17704
  if (!isRecord11(parsed))
@@ -17498,7 +17754,7 @@ function isIdentity(value) {
17498
17754
  function isRecord11(value) {
17499
17755
  return typeof value === "object" && value !== null;
17500
17756
  }
17501
- var MINSYNC_OLLAMA_MIGRATION_MESSAGE = "This workspace uses the legacy 768-dimensional Ollama/TEI embedding path. Reindex explicitly, or pin an explicit Ollama/TEI embedder config before using the new default runtime.", API_KEY_ENV_PATTERN2, MINSYNC_LOCK_HELD_EXIT_CODE = 3, MinSyncQueryError;
17757
+ var MINSYNC_OLLAMA_MIGRATION_MESSAGE = "This workspace uses the legacy 768-dimensional Ollama/TEI embedding path. Reindex explicitly, or pin an explicit Ollama/TEI embedder config before using the new default runtime.", API_KEY_ENV_PATTERN2, MINSYNC_LOCK_HELD_EXIT_CODE = 3, MINSYNC_PARTIAL_SYNC_EXIT_CODE = 6, MinSyncQueryError;
17502
17758
  var init_client10 = __esm(() => {
17503
17759
  init_embedder_config();
17504
17760
  init_errors4();
@@ -17742,7 +17998,7 @@ function isGitHubAsset(value) {
17742
17998
  function isRecord12(value) {
17743
17999
  return typeof value === "object" && value !== null;
17744
18000
  }
17745
- var LATEST_RELEASE_URL = "https://api.github.com/repos/NomaDamas/MinSync/releases/latest", SHA256_HEX_PATTERN, MINSYNC_VERSION = "0.4.7", MINSYNC_CARGO_INSTALL_TIMEOUT_MS, CARGO_PROBE_TIMEOUT_MS = 5000, MinSyncReleaseError;
18001
+ 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;
17746
18002
  var init_installer2 = __esm(() => {
17747
18003
  init_process();
17748
18004
  SHA256_HEX_PATTERN = /^[a-f0-9]{64}$/;
@@ -18298,7 +18554,7 @@ var init_isolated = __esm(() => {
18298
18554
  // src/mirror/sync.ts
18299
18555
  import { existsSync as existsSync24, rmSync as rmSync8 } from "node:fs";
18300
18556
  import { opendir, readFile as readFile5, stat as stat7 } from "node:fs/promises";
18301
- import { extname as extname5, resolve as resolve14, sep as sep7 } from "node:path";
18557
+ import { extname as extname5, resolve as resolve15, sep as sep7 } from "node:path";
18302
18558
  import { setTimeout as sleep3 } from "node:timers/promises";
18303
18559
  function errorCode(error) {
18304
18560
  if (!(error instanceof Error))
@@ -18816,7 +19072,7 @@ async function collectFiles(state, directory, depth) {
18816
19072
  continue;
18817
19073
  if (PRODUCT_ARTIFACT_FILE_NAMES.has(entry.name))
18818
19074
  continue;
18819
- const sourcePath = resolve14(directory, entry.name);
19075
+ const sourcePath = resolve15(directory, entry.name);
18820
19076
  if (entry.isDirectory()) {
18821
19077
  if (depth + 1 > state.limits.maxDepth) {
18822
19078
  listing.unreliablePrefixes.push(sourceIdentifier(sourceRoot, sourcePath));
@@ -19999,12 +20255,43 @@ function digitsEndAt(text, from) {
19999
20255
  end += 1;
20000
20256
  return end;
20001
20257
  }
20002
- function continuesAsProse(answer, from) {
20003
- if (answer[from] !== " " && answer[from] !== " ")
20258
+ function continuesAsProse(answer, openIndex, from) {
20259
+ let cursor = from;
20260
+ let leadingEmphasis = 0;
20261
+ while (openIndex - 1 - leadingEmphasis >= 0 && (answer[openIndex - 1 - leadingEmphasis] === "*" || answer[openIndex - 1 - leadingEmphasis] === "_")) {
20262
+ leadingEmphasis += 1;
20263
+ }
20264
+ if (leadingEmphasis > 0) {
20265
+ let trailingEmphasis = 0;
20266
+ while (cursor + trailingEmphasis < answer.length && trailingEmphasis < leadingEmphasis && answer[cursor + trailingEmphasis] === answer[openIndex - 1 - trailingEmphasis]) {
20267
+ trailingEmphasis += 1;
20268
+ }
20269
+ if (trailingEmphasis === leadingEmphasis)
20270
+ cursor += trailingEmphasis;
20271
+ }
20272
+ const tokenStart = openIndex - leadingEmphasis;
20273
+ const linePrefix = answer.slice(answer.lastIndexOf(`
20274
+ `, tokenStart - 1) + 1, tokenStart);
20275
+ const atHeadingStart = /^[ \t>*-]*$/u.test(linePrefix);
20276
+ if (atHeadingStart && answer[cursor] === ":")
20277
+ cursor += 1;
20278
+ if (answer[cursor] !== " " && answer[cursor] !== " ")
20004
20279
  return false;
20005
- const next = answer.codePointAt(from + 1);
20280
+ cursor += 1;
20281
+ while (cursor < answer.length && (answer[cursor] === "*" || answer[cursor] === "_" || answer[cursor] === '"' || answer[cursor] === "'" || answer[cursor] === "“" || answer[cursor] === "‘")) {
20282
+ cursor += 1;
20283
+ }
20284
+ const next = answer.codePointAt(cursor);
20006
20285
  return next !== undefined && /[\p{L}\p{N}]/u.test(String.fromCodePoint(next));
20007
20286
  }
20287
+ function isLegalPinpoint(answer, openIndex, afterCloseIndex) {
20288
+ if (PINPOINT_RANGE_AHEAD.test(answer.slice(afterCloseIndex, afterCloseIndex + 16)))
20289
+ return true;
20290
+ if (PINPOINT_RANGE_BEHIND.test(answer.slice(Math.max(0, openIndex - 16), openIndex)))
20291
+ return true;
20292
+ const prefixWindow = answer.slice(Math.max(0, openIndex - 120), openIndex);
20293
+ return PARA_PINPOINT_PREFIX.test(prefixWindow) || CLAUSE_START_AT_PREFIX.test(prefixWindow) || CASE_CITATION_AT_PREFIX.test(prefixWindow);
20294
+ }
20008
20295
  function evidenceMarkerAt(answer, index) {
20009
20296
  const ids = [];
20010
20297
  let cursor = index + 1;
@@ -20070,7 +20357,7 @@ function answerMarkers(answer, isFile = () => false) {
20070
20357
  }
20071
20358
  if (char === "[") {
20072
20359
  const digitsEnd = digitsEndAt(answer, index + 1);
20073
- if (digitsEnd > index + 1 && answer[digitsEnd] === "]" && answer[digitsEnd + 1] !== "(" && !continuesAsProse(answer, digitsEnd + 1)) {
20360
+ if (digitsEnd > index + 1 && answer[digitsEnd] === "]" && answer[digitsEnd + 1] !== "(" && !continuesAsProse(answer, index, digitsEnd + 1) && !isLegalPinpoint(answer, index, digitsEnd + 1)) {
20074
20361
  markers.push({
20075
20362
  kind: "number",
20076
20363
  start: leadingStart(index),
@@ -20142,7 +20429,14 @@ function assertResultsMappingOneToOne(label, results, mapping) {
20142
20429
  ].filter((note) => note !== undefined);
20143
20430
  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.");
20144
20431
  }
20145
- var MAX_FILE_MARKER_CLOSERS = 8, FILE_MARKER_PREFIX = "[file:";
20432
+ 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;
20433
+ var init_citations = __esm(() => {
20434
+ 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;
20435
+ 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;
20436
+ 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;
20437
+ PINPOINT_RANGE_AHEAD = /^[ \t\u00a0]*[-–—][ \t\u00a0]*\[\d+\]/u;
20438
+ PINPOINT_RANGE_BEHIND = /\[\d+\][ \t\u00a0]*[-–—][ \t\u00a0]*$/u;
20439
+ });
20146
20440
 
20147
20441
  // src/agent/evidence-ledger.ts
20148
20442
  import { createHash as createHash15 } from "node:crypto";
@@ -20426,6 +20720,7 @@ function deriveResultsFromAnswer(text, ledger, options) {
20426
20720
  }
20427
20721
  var MAX_EXCERPT_CHARS = 400, MAX_CLAIM_CHARS = 280, CLAIM_LOOKBACK_CHARS = 600, SENTENCE_END, LEADING_LIST_MARK, EVIDENCE_MARKER_TEXT, LEADING_BLANK;
20428
20722
  var init_answer_citations = __esm(() => {
20723
+ init_citations();
20429
20724
  init_evidence_ledger();
20430
20725
  SENTENCE_END = /[.!?。!?]/u;
20431
20726
  LEADING_LIST_MARK = /^[\s\-*•>]+/u;
@@ -20434,7 +20729,7 @@ var init_answer_citations = __esm(() => {
20434
20729
  });
20435
20730
 
20436
20731
  // src/agent/answer-guidelines.ts
20437
- 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.";
20732
+ 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.";
20438
20733
 
20439
20734
  // src/agent/datasource-skill.ts
20440
20735
  import { Type as Type3 } from "typebox";
@@ -22635,7 +22930,7 @@ var init_egress_gate = __esm(() => {
22635
22930
 
22636
22931
  // src/p2p/policy.ts
22637
22932
  import { existsSync as existsSync26, mkdirSync as mkdirSync21, readFileSync as readFileSync23, writeFileSync as writeFileSync17 } from "node:fs";
22638
- import { basename as basename13, dirname as dirname23, join as join36, resolve as resolve15 } from "node:path";
22933
+ import { basename as basename13, dirname as dirname23, join as join36, resolve as resolve16 } from "node:path";
22639
22934
  import { parse as parseToml2 } from "smol-toml";
22640
22935
  function isRecord17(value) {
22641
22936
  return typeof value === "object" && value !== null && !Array.isArray(value);
@@ -22918,7 +23213,7 @@ class PolicyStore {
22918
23213
  if (typeof normalizedOptions.workspacePath !== "string" || normalizedOptions.workspacePath.length === 0) {
22919
23214
  throw new PolicyError("workspacePath must be a non-empty path");
22920
23215
  }
22921
- this.workspacePath = resolve15(normalizedOptions.workspacePath);
23216
+ this.workspacePath = resolve16(normalizedOptions.workspacePath);
22922
23217
  this.sourceRoots = normalizedOptions.sourceRoots;
22923
23218
  const globalConfigPath = normalizedOptions.globalConfigPath ?? resolveGlobalConfigPath(normalizedOptions.homePath);
22924
23219
  const workspacePolicyPath = normalizedOptions.workspacePolicyPath ?? join36(this.workspacePath, ".autorag", "p2p", "policy.toml");
@@ -24349,6 +24644,7 @@ function recordStructuredResultsSession(sessionId, query, details, memory, compo
24349
24644
  }
24350
24645
  var init_search_documents = __esm(() => {
24351
24646
  init_memory();
24647
+ init_citations();
24352
24648
  });
24353
24649
 
24354
24650
  // src/agent/search-all-tool.ts
@@ -24652,6 +24948,10 @@ function buildSystemPrompt(config) {
24652
24948
  const modelId = config.modelId ?? "the configured model";
24653
24949
  const toolLines = [
24654
24950
  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)"),
24951
+ ...config.remoteSession ? [] : [
24952
+ 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"),
24953
+ toolLine(config, "edit", "surgically update an existing deliverable or config file when the user asks (local sessions only)")
24954
+ ],
24655
24955
  toolLine(config, "jikji_find", "local discovery through Jikji answer packs"),
24656
24956
  toolLine(config, "everything_search", "instant Windows file/folder name search (voidtools Everything) across the configured search folders"),
24657
24957
  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"),
@@ -24668,6 +24968,12 @@ function buildSystemPrompt(config) {
24668
24968
  ...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`),
24669
24969
  ...config.toolNames.filter((name) => ![
24670
24970
  "bash",
24971
+ "read",
24972
+ "write",
24973
+ "edit",
24974
+ "grep",
24975
+ "find",
24976
+ "ls",
24671
24977
  "jikji_find",
24672
24978
  "everything_search",
24673
24979
  "fsearch_search",
@@ -24800,8 +25106,9 @@ Your answer is your final reply, written as plain text, never as a tool call. Ci
24800
25106
  ## Answer Guidelines
24801
25107
 
24802
25108
  - **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.
24803
- - **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.
24804
- - **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.
25109
+ - **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.
25110
+ - **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.
25111
+ - ${ANSWER_DELIVERABLE_RULE} ${ANSWER_DELIVERABLE_DELTA_RULE}
24805
25112
  - ${ANSWER_CITATION_RULE}
24806
25113
  - ${ANSWER_IMAGE_EMBED_RULE} ${ANSWER_IMAGE_DELTA_RULE}
24807
25114
  - **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.
@@ -30457,7 +30764,7 @@ var init_web_search_tool = __esm(() => {
30457
30764
  // src/agent/agent.ts
30458
30765
  import { randomUUID as randomUUID8 } from "node:crypto";
30459
30766
  import { existsSync as existsSync31, watch as fsWatch, mkdirSync as mkdirSync25, realpathSync as realpathSync5, statSync as statSync3, writeFileSync as writeFileSync22 } from "node:fs";
30460
- import { basename as basename14, dirname as dirname27, join as join41, resolve as resolve16 } from "node:path";
30767
+ import { basename as basename14, dirname as dirname27, join as join41, resolve as resolve17 } from "node:path";
30461
30768
  import { clampThinkingLevel } from "@earendil-works/pi-ai/compat";
30462
30769
  import { getAgentDir as getAgentDir4 } from "@earendil-works/pi-coding-agent";
30463
30770
  function positiveLimit(value, fallback, path) {
@@ -30617,7 +30924,7 @@ class AutoRAGAgent {
30617
30924
  this.datasourceVirtualScopePrefixes = this.datasourceSkills.map((skill) => normalizeVirtualPath2(`/${skill.describe().name}`));
30618
30925
  this.startupDiagnostics = options.startupDiagnostics ?? [];
30619
30926
  this.datasourceAgentSkills = this.buildDatasourceAgentSkills();
30620
- this.configuredSearchPaths = options.searchPaths.map((searchPath) => resolve16(searchPath));
30927
+ this.configuredSearchPaths = options.searchPaths.map((searchPath) => resolve17(searchPath));
30621
30928
  this.languages = options.languages ?? DEFAULT_LANGUAGES;
30622
30929
  this.searchPaths = options.searchPaths.map(pinSearchRoot);
30623
30930
  this.jikjiRoots = outermostJikjiRoots(this.searchPaths);
@@ -30664,7 +30971,8 @@ class AutoRAGAgent {
30664
30971
  const everythingClient = new EverythingClient({
30665
30972
  ...options.everything ?? {},
30666
30973
  root: this.workspaceProjectRoot,
30667
- folders: this.searchPaths
30974
+ folders: this.searchPaths,
30975
+ excludeFolders: this.excludePaths
30668
30976
  });
30669
30977
  if (everythingClient.isSupported())
30670
30978
  this.everythingClient = everythingClient;
@@ -30770,8 +31078,9 @@ class AutoRAGAgent {
30770
31078
  return true;
30771
31079
  });
30772
31080
  this.tools = tools;
31081
+ const builtinToolNames = this.remoteSession ? PI_BUILTIN_TOOL_NAMES.filter((name) => name !== "write" && name !== "edit") : PI_BUILTIN_TOOL_NAMES;
30773
31082
  const toolNames = [
30774
- ...PI_BUILTIN_TOOL_NAMES,
31083
+ ...builtinToolNames,
30775
31084
  ...tools.map((tool) => tool.name),
30776
31085
  ...this.jevExtension !== undefined ? [JEV_TOOL_NAME] : []
30777
31086
  ];
@@ -30787,7 +31096,7 @@ class AutoRAGAgent {
30787
31096
  this.innerAgent = {
30788
31097
  state: {
30789
31098
  systemPrompt,
30790
- tools: [...PI_BUILTIN_TOOL_NAMES.map((name) => ({ name })), ...tools],
31099
+ tools: [...builtinToolNames.map((name) => ({ name })), ...tools],
30791
31100
  messages: []
30792
31101
  },
30793
31102
  transformContext: (messages) => this.withMemoryContext(messages)
@@ -31104,6 +31413,7 @@ ${context.text}
31104
31413
  ...this.jevExtension !== undefined ? [this.jevExtension] : []
31105
31414
  ],
31106
31415
  ...this.jevExtension !== undefined ? { extensionToolNames: [JEV_TOOL_NAME] } : {},
31416
+ remoteSession: this.remoteSession,
31107
31417
  ...this.updateNotice === undefined ? {} : { updateNotice: this.updateNotice }
31108
31418
  });
31109
31419
  this.boundPiRuntime = runtime.runtime;
@@ -31558,7 +31868,7 @@ Warning: the edited config did not validate, so the previous config was restored
31558
31868
  result.summary,
31559
31869
  ...result.evidence.map((evidence) => evidence.excerpt)
31560
31870
  ])
31561
- ], [this.workspaceProjectRoot, ...this.searchPaths].map((root) => resolve16(root)));
31871
+ ], [this.workspaceProjectRoot, ...this.searchPaths].map((root) => resolve17(root)));
31562
31872
  if (!scan.ok)
31563
31873
  throw new RemoteSessionRejectedError(scan.code);
31564
31874
  }
@@ -32030,8 +32340,9 @@ ${baseline}
32030
32340
  ` + `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.
32031
32341
 
32032
32342
  ` + `Formatting and content rules for the answer:
32033
- ` + `- 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.
32034
- ` + `- Answer the question directly. Do not include specific file paths, datasource descriptions, or retrieval mechanics in the answer text.
32343
+ ` + `- 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.
32344
+ ` + `- 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.
32345
+ ` + `- ${ANSWER_DELIVERABLE_RULE}
32035
32346
  ` + `- ${ANSWER_CITATION_RULE}
32036
32347
  ` + `- ${ANSWER_IMAGE_EMBED_RULE}
32037
32348
  ` + `- Do not report per-source negative findings (e.g. "no information found in Slack" or "checked Drive but found nothing").
@@ -32070,12 +32381,14 @@ ${baseline}
32070
32381
  ` + `- NEVER restate or re-list first-answer facts that remain correct, and never repeat its bullet list.
32071
32382
  ` + `- Mark each item clearly as a correction or as a new finding.
32072
32383
  ` + `- 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.
32384
+ ` + `- ${ANSWER_DELIVERABLE_RULE} ${ANSWER_DELIVERABLE_DELTA_RULE}
32073
32385
  ` + `- ${ANSWER_CITATION_RULE} A correction or new finding that relies on evidence the first answer cited must cite that evidence's id again.
32074
32386
  ` + `- ${ANSWER_IMAGE_EMBED_RULE} ${ANSWER_IMAGE_DELTA_RULE}
32075
32387
  ` + `- Do not report per-source negative findings (e.g. "no information found in Slack").
32076
32388
  ` + `- 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):
32077
- ` + `- 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.
32078
- ` + `- Answer the question directly. Do not include specific file paths, datasource descriptions, or retrieval mechanics in the answer text.
32389
+ ` + `- 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.
32390
+ ` + `- 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.
32391
+ ` + `- ${ANSWER_DELIVERABLE_RULE}${fastAnswer !== undefined ? ` ${ANSWER_DELIVERABLE_DELTA_RULE}` : ""}
32079
32392
  ` + `- ${ANSWER_CITATION_RULE}
32080
32393
  ` + `- ${ANSWER_IMAGE_EMBED_RULE}
32081
32394
  ` + `- Do not report per-source negative findings (e.g. "no information found in Slack").
@@ -32103,8 +32416,9 @@ ${initialRetrievalContext ?? "Pending; continue only with a brief progress state
32103
32416
  ` + `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.
32104
32417
 
32105
32418
  ` + `Formatting and content rules for the answer:
32106
- ` + `- 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.
32107
- ` + `- Answer the question directly. Do not include specific file paths, datasource descriptions, or retrieval mechanics in the answer text.
32419
+ ` + `- 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.
32420
+ ` + `- 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.
32421
+ ` + `- ${ANSWER_DELIVERABLE_RULE}
32108
32422
  ` + `- ${ANSWER_CITATION_RULE}
32109
32423
  ` + `- ${ANSWER_IMAGE_EMBED_RULE}
32110
32424
  ` + `- Do not report per-source negative findings (e.g. "no information found in Slack").
@@ -32259,6 +32573,7 @@ ${initialRetrievalContext ?? "Pending; continue only with a brief progress state
32259
32573
  ...summary.diagnostics,
32260
32574
  ...minsyncDiagnostics,
32261
32575
  ...stagingExcludedDiagnostics(minsync?.stagingExcluded),
32576
+ ...fileSkippedDiagnostics(minsync?.filesFailed),
32262
32577
  ...everythingDiagnostics,
32263
32578
  ...fsearchDiagnostics
32264
32579
  ],
@@ -32388,7 +32703,7 @@ ${initialRetrievalContext ?? "Pending; continue only with a brief progress state
32388
32703
  }
32389
32704
  startWatchRefresh(options = {}) {
32390
32705
  this.refreshState = { ...this.refreshState, watchLimited: false, watchFailed: false };
32391
- const dirs = this.searchPaths.map((searchPath) => resolve16(searchPath));
32706
+ const dirs = this.searchPaths.map((searchPath) => resolve17(searchPath));
32392
32707
  const watcherFactory = options.watcherFactory ?? this.defaultWatcherFactory();
32393
32708
  return createWatchRefresh({
32394
32709
  dirs,
@@ -32565,7 +32880,7 @@ ${initialRetrievalContext ?? "Pending; continue only with a brief progress state
32565
32880
  const evidencePack = [];
32566
32881
  const allPaths = [];
32567
32882
  for (const entry of entries) {
32568
- const originRoot = sourceRoots.find((sr) => sr.rootPath === resolve16(entry.root));
32883
+ const originRoot = sourceRoots.find((sr) => sr.rootPath === resolve17(entry.root));
32569
32884
  if (originRoot === undefined)
32570
32885
  continue;
32571
32886
  const originRoots = [originRoot];
@@ -32895,6 +33210,14 @@ function stagingExcludedDiagnostics(excluded) {
32895
33210
  source
32896
33211
  }));
32897
33212
  }
33213
+ function fileSkippedDiagnostics(failed) {
33214
+ return (failed ?? []).map((failure) => ({
33215
+ code: "minsync-file-skipped",
33216
+ severity: "warning",
33217
+ message: `MinSync skipped this file at the ${failure.stage} stage and will retry it on the next sync: ${sanitizeDiagnosticMessage(failure.error)}`,
33218
+ source: failure.path
33219
+ }));
33220
+ }
32898
33221
  function toSearchDiagnostic(diagnostic) {
32899
33222
  return {
32900
33223
  code: diagnostic.code,
@@ -32970,7 +33293,7 @@ function toMinSyncReasonDiagnostic(reason) {
32970
33293
  };
32971
33294
  }
32972
33295
  function pinExcludedPath(path) {
32973
- const resolvedPath = resolve16(path);
33296
+ const resolvedPath = resolve17(path);
32974
33297
  try {
32975
33298
  return join41(realpathSync5(dirname27(resolvedPath)), basename14(resolvedPath));
32976
33299
  } catch {
@@ -32978,7 +33301,7 @@ function pinExcludedPath(path) {
32978
33301
  }
32979
33302
  }
32980
33303
  function pinSearchRoot(searchPath) {
32981
- const resolvedPath = resolve16(searchPath);
33304
+ const resolvedPath = resolve17(searchPath);
32982
33305
  let canonicalPath;
32983
33306
  try {
32984
33307
  canonicalPath = realpathSync5(resolvedPath);
@@ -33431,7 +33754,7 @@ var init_memory2 = __esm(() => {
33431
33754
 
33432
33755
  // src/cli/commands/index.ts
33433
33756
  import { existsSync as existsSync32, lstatSync as lstatSync3, rmSync as rmSync11 } from "node:fs";
33434
- import { join as join42, resolve as resolve17, sep as sep8 } from "node:path";
33757
+ import { join as join42, resolve as resolve18, sep as sep8 } from "node:path";
33435
33758
  async function runIndex2(ctx) {
33436
33759
  const sub = ctx.positionals[0];
33437
33760
  if (sub !== "reset" && sub !== "rebuild") {
@@ -33447,8 +33770,8 @@ async function runIndex2(ctx) {
33447
33770
  }
33448
33771
  const methods = parseMethodFlag(ctx.flags.method);
33449
33772
  const { targetNames, targetSubdirs, refreshMethods } = resolveResetScope(methods);
33450
- const autoragDir = resolve17(config.workspacePath, ".autorag");
33451
- const targets = targetSubdirs.map((subdir) => resolve17(config.workspacePath, subdir));
33773
+ const autoragDir = resolve18(config.workspacePath, ".autorag");
33774
+ const targets = targetSubdirs.map((subdir) => resolve18(config.workspacePath, subdir));
33452
33775
  for (const dir of [autoragDir, ...targets]) {
33453
33776
  if (existsSync32(dir) && lstatSync3(dir).isSymbolicLink()) {
33454
33777
  ctx.stderr(renderError(new Error("Refusing to reset: an index path is a symlink, not a real directory."), {
@@ -33810,12 +34133,12 @@ var init_health = __esm(() => {
33810
34133
  });
33811
34134
 
33812
34135
  // src/cli/commands/duplicates.ts
33813
- import { resolve as resolve18 } from "node:path";
34136
+ import { resolve as resolve19 } from "node:path";
33814
34137
  async function runDuplicates(ctx, scanner = (path) => scanWithDupey(path)) {
33815
34138
  try {
33816
34139
  const config = resolveConfig({ flags: ctx.flags, cwd: ctx.cwd });
33817
34140
  const requested = ctx.positionals[0];
33818
- const roots = requested ? [resolve18(ctx.cwd, requested)] : config.searchPaths.map((path) => resolve18(path));
34141
+ const roots = requested ? [resolve19(ctx.cwd, requested)] : config.searchPaths.map((path) => resolve19(path));
33819
34142
  const scans = await Promise.all(roots.map((root) => scanner(root)));
33820
34143
  const exactGroups = scans.flatMap(exactGroupsFromScan);
33821
34144
  const extractionErrors = scans.flatMap((scan) => scan.errors);
@@ -35111,7 +35434,7 @@ var init_results = __esm(() => {
35111
35434
  confidence: Type18.Optional(Type18.Number({ description: "Evidence confidence, 0..1", minimum: 0, maximum: 1 }))
35112
35435
  });
35113
35436
  answerSchema = Type18.String({
35114
- description: `Answer for the caller. At most 5 bullet points (plus optional explanation); reference results by bracketed number (e.g. [1], [2]) without file paths or raw chunk text, except the image-embed exception below. ${ANSWER_CITATION_RULE} ${ANSWER_IMAGE_EMBED_RULE}`
35437
+ description: `Answer for the caller. Unless the user explicitly requests otherwise, provide at most 5 bullet points (plus optional explanation) and answer directly; reference results by bracketed number (e.g. [1], [2]) without file paths or raw chunk text, except the image-embed exception below. ${ANSWER_DELIVERABLE_RULE} ${ANSWER_CITATION_RULE} ${ANSWER_IMAGE_EMBED_RULE}`
35115
35438
  });
35116
35439
  warningsSchema = Type18.Optional(Type18.Array(Type18.String(), { description: "Optional warnings about this result set" }));
35117
35440
  curatedResultFields = {
@@ -35288,7 +35611,8 @@ Usage: autorag lite <subcommand> [args] [flags]
35288
35611
  Subcommands:
35289
35612
  autorag lite init
35290
35613
  Write the configured model-free lifecycle config
35291
- (--search-paths PATHS --workspace DIR --memory-path FILE --force)
35614
+ (--search-paths PATHS --exclude PATH --workspace DIR --memory-path FILE --force)
35615
+ --exclude may be repeated; relative paths use the current directory
35292
35616
  autorag lite refresh Run an incremental index refresh (no model required)
35293
35617
  (--full --force --method minsync,parsed,datasources,jikji,everything,fsearch,all)
35294
35618
  autorag lite watch Watch configured roots (or --once for one refresh tick)
@@ -35364,6 +35688,7 @@ var COMMAND_USAGE = {
35364
35688
  "",
35365
35689
  "Flags:",
35366
35690
  " --search-paths <csv> Folders to index/search",
35691
+ " --exclude <path> Exclude a file or folder (repeatable; relative to the current directory)",
35367
35692
  " --languages <csv> OCR/parser languages",
35368
35693
  " --workspace <dir> Workspace that owns .autorag state",
35369
35694
  " --memory-path <file> Retrieval memory file",
@@ -35632,6 +35957,7 @@ var BOOLEAN_FLAGS = new Set([
35632
35957
  var VALUE_FLAGS = new Set([
35633
35958
  "config",
35634
35959
  "search-paths",
35960
+ "exclude",
35635
35961
  "languages",
35636
35962
  "workspace",
35637
35963
  "memory-path",
@@ -35761,6 +36087,7 @@ Run: autorag <command> --help for command-specific usage.
35761
36087
  function parseArgs(argv) {
35762
36088
  const positionals = [];
35763
36089
  const flags = {};
36090
+ const repeatedFlags = {};
35764
36091
  for (let i = 0;i < argv.length; i++) {
35765
36092
  const token = argv[i];
35766
36093
  if (token === "-h") {
@@ -35781,22 +36108,27 @@ function parseArgs(argv) {
35781
36108
  if (!BOOLEAN_FLAGS.has(key) && !VALUE_FLAGS.has(key)) {
35782
36109
  return { error: `Unknown flag: --${key}` };
35783
36110
  }
35784
- if (eq !== -1) {
35785
- flags[key] = body.slice(eq + 1);
35786
- continue;
35787
- }
35788
- if (BOOLEAN_FLAGS.has(key)) {
36111
+ if (eq === -1 && BOOLEAN_FLAGS.has(key)) {
35789
36112
  flags[key] = true;
35790
36113
  continue;
35791
36114
  }
35792
- const next = argv[i + 1];
35793
- if (next === undefined || next.startsWith("--")) {
36115
+ const value = eq === -1 ? argv[++i] : body.slice(eq + 1);
36116
+ if (value === undefined || eq === -1 && value.startsWith("--")) {
35794
36117
  return { error: `Flag --${key} requires a value` };
35795
36118
  }
35796
- flags[key] = next;
35797
- i++;
36119
+ if (key === "exclude") {
36120
+ if (value.trim().length === 0)
36121
+ return { error: "Flag --exclude requires a non-empty path" };
36122
+ repeatedFlags[key] ??= [];
36123
+ repeatedFlags[key].push(value);
36124
+ }
36125
+ flags[key] = value;
35798
36126
  }
35799
- return { positionals, flags };
36127
+ return {
36128
+ positionals,
36129
+ flags,
36130
+ ...Object.keys(repeatedFlags).length > 0 ? { repeatedFlags } : {}
36131
+ };
35800
36132
  }
35801
36133
  function promptYesNo(question) {
35802
36134
  const rl = createInterface({ input: process.stdin, output: process.stdout });
@@ -35918,6 +36250,7 @@ ${USAGE2}`);
35918
36250
  const ctx = {
35919
36251
  positionals: positionals.slice(1),
35920
36252
  flags,
36253
+ ...parsed.repeatedFlags === undefined ? {} : { repeatedFlags: parsed.repeatedFlags },
35921
36254
  json,
35922
36255
  debug,
35923
36256
  cwd: process.cwd(),