@hasna/search 0.0.8 → 0.0.9

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.
Files changed (65) hide show
  1. package/LICENSE +2 -1
  2. package/README.md +78 -9
  3. package/dist/cli/index.js +1761 -198
  4. package/dist/cli/local.d.ts +3 -0
  5. package/dist/cli/local.d.ts.map +1 -0
  6. package/dist/cli/storage.d.ts +3 -0
  7. package/dist/cli/storage.d.ts.map +1 -0
  8. package/dist/db/database.d.ts.map +1 -1
  9. package/dist/db/index-db.d.ts +6 -0
  10. package/dist/db/index-db.d.ts.map +1 -0
  11. package/dist/db/index-migrations.d.ts +3 -0
  12. package/dist/db/index-migrations.d.ts.map +1 -0
  13. package/dist/db/migrations.d.ts.map +1 -1
  14. package/dist/db/pg-migrations.d.ts +1 -1
  15. package/dist/db/providers.d.ts.map +1 -1
  16. package/dist/db/storage-config.d.ts +26 -0
  17. package/dist/db/storage-config.d.ts.map +1 -0
  18. package/dist/db/storage-sync.d.ts +35 -0
  19. package/dist/db/storage-sync.d.ts.map +1 -0
  20. package/dist/index.d.ts +9 -3
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +2459 -118
  23. package/dist/lib/config.d.ts.map +1 -1
  24. package/dist/lib/dedup.d.ts.map +1 -1
  25. package/dist/lib/local/find.d.ts +38 -0
  26. package/dist/lib/local/find.d.ts.map +1 -0
  27. package/dist/lib/local/ignore.d.ts +38 -0
  28. package/dist/lib/local/ignore.d.ts.map +1 -0
  29. package/dist/lib/local/indexer.d.ts +57 -0
  30. package/dist/lib/local/indexer.d.ts.map +1 -0
  31. package/dist/lib/local/query.d.ts +60 -0
  32. package/dist/lib/local/query.d.ts.map +1 -0
  33. package/dist/lib/local/regex.d.ts +26 -0
  34. package/dist/lib/local/regex.d.ts.map +1 -0
  35. package/dist/lib/local/walker.d.ts +30 -0
  36. package/dist/lib/local/walker.d.ts.map +1 -0
  37. package/dist/lib/providers/content.d.ts +9 -0
  38. package/dist/lib/providers/content.d.ts.map +1 -0
  39. package/dist/lib/providers/files.d.ts +9 -0
  40. package/dist/lib/providers/files.d.ts.map +1 -0
  41. package/dist/lib/providers/index.d.ts.map +1 -1
  42. package/dist/lib/search.d.ts.map +1 -1
  43. package/dist/mcp/http.d.ts +15 -0
  44. package/dist/mcp/http.d.ts.map +1 -0
  45. package/dist/mcp/index.js +14323 -11632
  46. package/dist/mcp/server.d.ts +5 -0
  47. package/dist/mcp/server.d.ts.map +1 -0
  48. package/dist/mcp/storage-tools.d.ts +3 -0
  49. package/dist/mcp/storage-tools.d.ts.map +1 -0
  50. package/dist/server/index.js +28499 -4976
  51. package/dist/server/serve.d.ts.map +1 -1
  52. package/dist/storage.d.ts +7 -0
  53. package/dist/storage.d.ts.map +1 -0
  54. package/dist/storage.js +5584 -0
  55. package/dist/types/index.d.ts +10 -2
  56. package/dist/types/index.d.ts.map +1 -1
  57. package/package.json +16 -4
  58. package/dist/cli/cloud.d.ts +0 -3
  59. package/dist/cli/cloud.d.ts.map +0 -1
  60. package/dist/db/cloud-config.d.ts +0 -14
  61. package/dist/db/cloud-config.d.ts.map +0 -1
  62. package/dist/db/cloud-sync.d.ts +0 -30
  63. package/dist/db/cloud-sync.d.ts.map +0 -1
  64. package/dist/mcp/cloud-tools.d.ts +0 -3
  65. package/dist/mcp/cloud-tools.d.ts.map +0 -1
package/dist/cli/index.js CHANGED
@@ -993,7 +993,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
993
993
  this._exitCallback = (err) => {
994
994
  if (err.code !== "commander.executeSubCommandAsync") {
995
995
  throw err;
996
- } else {}
996
+ }
997
997
  };
998
998
  }
999
999
  return this;
@@ -2097,7 +2097,7 @@ function runMigrations(db) {
2097
2097
  db.exec("BEGIN");
2098
2098
  try {
2099
2099
  migration.up(db);
2100
- db.prepare("INSERT INTO _migrations (version, description) VALUES (?, ?)").run(migration.version, migration.description);
2100
+ db.prepare("INSERT OR IGNORE INTO _migrations (version, description) VALUES (?, ?)").run(migration.version, migration.description);
2101
2101
  db.exec("COMMIT");
2102
2102
  } catch (err) {
2103
2103
  db.exec("ROLLBACK");
@@ -2234,6 +2234,34 @@ var init_migrations = __esm(() => {
2234
2234
  ('prof-all', 'all', 'All enabled providers', '["google","serpapi","exa","perplexity","brave","bing","twitter","reddit","youtube","hackernews","github","arxiv"]', '{}', datetime('now'));
2235
2235
  `);
2236
2236
  }
2237
+ },
2238
+ {
2239
+ version: 4,
2240
+ description: "Local search providers (files, content) and local profile",
2241
+ up: (db) => {
2242
+ db.exec(`
2243
+ INSERT OR IGNORE INTO providers (name, enabled, api_key_env, rate_limit, metadata) VALUES
2244
+ ('files', 1, '', 0, '{}'),
2245
+ ('content', 1, '', 0, '{}');
2246
+
2247
+ INSERT OR IGNORE INTO search_profiles (id, name, description, providers, options, created_at) VALUES
2248
+ ('prof-local', 'local', 'Local filesystem: file paths + content', '["files","content"]', '{}', datetime('now'));
2249
+ `);
2250
+ const row = db.query("SELECT id, providers FROM search_profiles WHERE name = 'all'").get();
2251
+ if (row) {
2252
+ let providers;
2253
+ try {
2254
+ providers = JSON.parse(row.providers);
2255
+ } catch {
2256
+ providers = [];
2257
+ }
2258
+ for (const name of ["files", "content"]) {
2259
+ if (!providers.includes(name))
2260
+ providers.push(name);
2261
+ }
2262
+ db.prepare("UPDATE search_profiles SET providers = ? WHERE id = ?").run(JSON.stringify(providers), row.id);
2263
+ }
2264
+ }
2237
2265
  }
2238
2266
  ];
2239
2267
  });
@@ -2247,18 +2275,7 @@ __export(exports_database, {
2247
2275
  closeDb: () => closeDb
2248
2276
  });
2249
2277
  import { Database } from "bun:sqlite";
2250
- import { cpSync, existsSync as existsSync2, mkdirSync } from "fs";
2251
- function migrateDotfile() {
2252
- const home = Bun.env.HOME ?? "/tmp";
2253
- const oldDir = `${home}/.search`;
2254
- const newDir = `${home}/.hasna/search`;
2255
- if (existsSync2(newDir) || !existsSync2(oldDir))
2256
- return;
2257
- try {
2258
- mkdirSync(`${home}/.hasna`, { recursive: true });
2259
- cpSync(oldDir, newDir, { recursive: true });
2260
- } catch {}
2261
- }
2278
+ import { mkdirSync } from "fs";
2262
2279
  function ensureFeedbackTable(db) {
2263
2280
  db.exec(`
2264
2281
  CREATE TABLE IF NOT EXISTS feedback (
@@ -2277,18 +2294,19 @@ function getDbPath() {
2277
2294
  if (envPath)
2278
2295
  return envPath;
2279
2296
  const home = Bun.env.HOME ?? "/tmp";
2280
- migrateDotfile();
2281
- const newDir = `${home}/.hasna/search`;
2282
- mkdirSync(newDir, { recursive: true });
2283
- return `${newDir}/data.db`;
2297
+ const dir = `${home}/.hasna/search`;
2298
+ mkdirSync(dir, { recursive: true });
2299
+ return `${dir}/data.db`;
2284
2300
  }
2285
2301
  function getDb() {
2286
2302
  if (instance)
2287
2303
  return instance;
2288
2304
  const path = getDbPath();
2289
2305
  const db = new Database(path);
2290
- db.exec("PRAGMA synchronous = NORMAL");
2291
2306
  db.exec("PRAGMA busy_timeout = 5000");
2307
+ db.exec("PRAGMA journal_mode = WAL");
2308
+ db.exec("PRAGMA foreign_keys = ON");
2309
+ db.exec("PRAGMA synchronous = NORMAL");
2292
2310
  runMigrations(db);
2293
2311
  ensureFeedbackTable(db);
2294
2312
  instance = db;
@@ -7393,8 +7411,8 @@ var init_pg_migrate = __esm(() => {
7393
7411
  var require_package = __commonJS((exports, module) => {
7394
7412
  module.exports = {
7395
7413
  name: "@hasna/search",
7396
- version: "0.0.8",
7397
- description: "Unified search aggregator \u2014 12 providers (Google, SerpAPI, Exa, Perplexity, Twitter, Reddit, YouTube, Brave, Bing, Hacker News, GitHub, arXiv) + YouTube transcription. CLI + MCP + REST API + Dashboard.",
7414
+ version: "0.0.9",
7415
+ description: "Unified search \u2014 local file index (find files by name/path/content/regex in ms, trigram FTS) + 12 web providers (Google, SerpAPI, Exa, Perplexity, Twitter, Reddit, YouTube, Brave, Bing, Hacker News, GitHub, arXiv) + YouTube transcription. CLI + MCP + REST API + Dashboard.",
7398
7416
  type: "module",
7399
7417
  main: "dist/index.js",
7400
7418
  types: "dist/index.d.ts",
@@ -7407,6 +7425,10 @@ var require_package = __commonJS((exports, module) => {
7407
7425
  ".": {
7408
7426
  types: "./dist/index.d.ts",
7409
7427
  import: "./dist/index.js"
7428
+ },
7429
+ "./storage": {
7430
+ types: "./dist/storage.d.ts",
7431
+ import: "./dist/storage.js"
7410
7432
  }
7411
7433
  },
7412
7434
  files: [
@@ -7415,8 +7437,9 @@ var require_package = __commonJS((exports, module) => {
7415
7437
  "README.md"
7416
7438
  ],
7417
7439
  scripts: {
7418
- build: "cd dashboard && bun run build && cd .. && bun build src/cli/index.tsx --outdir dist/cli --target bun --external ink --external react --external chalk && bun build src/mcp/index.ts --outdir dist/mcp --target bun && bun build src/server/index.ts --outdir dist/server --target bun && bun build src/index.ts --outdir dist --target bun && tsc --emitDeclarationOnly --outDir dist",
7419
- "build:no-dashboard": "bun build src/cli/index.tsx --outdir dist/cli --target bun --external ink --external react --external chalk && bun build src/mcp/index.ts --outdir dist/mcp --target bun && bun build src/server/index.ts --outdir dist/server --target bun && bun build src/index.ts --outdir dist --target bun && tsc --emitDeclarationOnly --outDir dist",
7440
+ clean: "rm -rf dist",
7441
+ build: "bun run clean && cd dashboard && bun run build && cd .. && bun build src/cli/index.tsx --outdir dist/cli --target bun --external ink --external react --external chalk && bun build src/mcp/index.ts --outdir dist/mcp --target bun && bun build src/server/index.ts --outdir dist/server --target bun && bun build src/index.ts src/storage.ts --outdir dist --target bun && tsc --emitDeclarationOnly --outDir dist",
7442
+ "build:no-dashboard": "bun run clean && bun build src/cli/index.tsx --outdir dist/cli --target bun --external ink --external react --external chalk && bun build src/mcp/index.ts --outdir dist/mcp --target bun && bun build src/server/index.ts --outdir dist/server --target bun && bun build src/index.ts src/storage.ts --outdir dist --target bun && tsc --emitDeclarationOnly --outDir dist",
7420
7443
  typecheck: "tsc --noEmit",
7421
7444
  test: "bun test",
7422
7445
  "dev:cli": "bun run src/cli/index.tsx",
@@ -7427,6 +7450,13 @@ var require_package = __commonJS((exports, module) => {
7427
7450
  },
7428
7451
  keywords: [
7429
7452
  "search",
7453
+ "local-search",
7454
+ "file-search",
7455
+ "code-search",
7456
+ "grep",
7457
+ "trigram",
7458
+ "fts5",
7459
+ "indexing",
7430
7460
  "meta-search",
7431
7461
  "aggregator",
7432
7462
  "google",
@@ -7498,70 +7528,95 @@ var {
7498
7528
  } = import__.default;
7499
7529
 
7500
7530
  // src/cli/index.tsx
7501
- import chalk2 from "chalk";
7531
+ import chalk3 from "chalk";
7502
7532
 
7503
- // src/cli/cloud.ts
7533
+ // src/cli/storage.ts
7504
7534
  import chalk from "chalk";
7505
7535
 
7506
- // src/db/cloud-config.ts
7536
+ // src/db/storage-config.ts
7507
7537
  import { existsSync, readFileSync } from "fs";
7508
7538
  import { homedir } from "os";
7509
7539
  import { join } from "path";
7510
- var CONFIG_PATH = join(homedir(), ".hasna", "search", "cloud", "config.json");
7511
- function isMode(value) {
7512
- return value === "local" || value === "hybrid" || value === "cloud";
7513
- }
7514
- function envConnectionString() {
7515
- return Bun.env.HASNA_SEARCH_CLOUD_DATABASE_URL ?? Bun.env.OPEN_SEARCH_CLOUD_DATABASE_URL ?? Bun.env.SEARCH_CLOUD_DATABASE_URL;
7516
- }
7517
- function getCloudConfig() {
7540
+ var STORAGE_CONFIG_PATH = join(homedir(), ".hasna", "search", "storage", "config.json");
7541
+ var SEARCH_STORAGE_ENV = "HASNA_SEARCH_DATABASE_URL";
7542
+ var SEARCH_STORAGE_FALLBACK_ENV = "SEARCH_DATABASE_URL";
7543
+ var SEARCH_STORAGE_MODE_ENV = "HASNA_SEARCH_STORAGE_MODE";
7544
+ var SEARCH_STORAGE_MODE_FALLBACK_ENV = "SEARCH_STORAGE_MODE";
7545
+ var STORAGE_DATABASE_ENV = [SEARCH_STORAGE_ENV, SEARCH_STORAGE_FALLBACK_ENV];
7546
+ function readEnv(name) {
7547
+ const value = process.env[name]?.trim();
7548
+ return value || undefined;
7549
+ }
7550
+ function normalizeMode(value) {
7551
+ const normalized = value?.trim().toLowerCase();
7552
+ if (normalized === "local" || normalized === "hybrid" || normalized === "remote")
7553
+ return normalized;
7554
+ return;
7555
+ }
7556
+ function getStorageDatabaseEnvName() {
7557
+ for (const name of STORAGE_DATABASE_ENV) {
7558
+ if (readEnv(name))
7559
+ return name;
7560
+ }
7561
+ return null;
7562
+ }
7563
+ function getStorageDatabaseEnv() {
7564
+ const name = getStorageDatabaseEnvName();
7565
+ return name ? { name } : null;
7566
+ }
7567
+ function getStorageDatabaseUrl() {
7568
+ const env = getStorageDatabaseEnv();
7569
+ return env ? readEnv(env.name) : undefined;
7570
+ }
7571
+ function getStorageConfig() {
7518
7572
  const config = {
7519
7573
  mode: "local",
7520
7574
  rds: {
7521
7575
  host: "",
7522
7576
  port: 5432,
7523
7577
  username: "",
7524
- password_env: "SEARCH_CLOUD_DATABASE_PASSWORD",
7578
+ password_env: "SEARCH_DATABASE_PASSWORD",
7525
7579
  ssl: true
7526
7580
  }
7527
7581
  };
7528
- if (existsSync(CONFIG_PATH)) {
7582
+ if (existsSync(STORAGE_CONFIG_PATH)) {
7529
7583
  try {
7530
- const raw = JSON.parse(readFileSync(CONFIG_PATH, "utf-8"));
7531
- config.mode = raw.mode ?? config.mode;
7584
+ const raw = JSON.parse(readFileSync(STORAGE_CONFIG_PATH, "utf-8"));
7585
+ config.mode = normalizeMode(raw.mode) ?? config.mode;
7532
7586
  config.rds = { ...config.rds, ...raw.rds ?? {} };
7533
7587
  } catch {}
7534
7588
  }
7535
- const modeOverride = Bun.env.HASNA_SEARCH_CLOUD_MODE ?? Bun.env.OPEN_SEARCH_CLOUD_MODE ?? Bun.env.SEARCH_CLOUD_MODE;
7536
- if (isMode(modeOverride)) {
7537
- config.mode = modeOverride;
7538
- } else if (envConnectionString() && config.mode === "local") {
7589
+ const modeOverride = readEnv(SEARCH_STORAGE_MODE_ENV) ?? readEnv(SEARCH_STORAGE_MODE_FALLBACK_ENV);
7590
+ const normalizedMode = normalizeMode(modeOverride);
7591
+ if (normalizedMode) {
7592
+ config.mode = normalizedMode;
7593
+ } else if (getStorageDatabaseUrl() && config.mode === "local") {
7539
7594
  config.mode = "hybrid";
7540
7595
  }
7541
7596
  return config;
7542
7597
  }
7543
- function getConnectionString(dbName = "search") {
7544
- const direct = envConnectionString();
7598
+ function getStorageConnectionString(dbName = "search") {
7599
+ const direct = getStorageDatabaseUrl();
7545
7600
  if (direct)
7546
7601
  return direct;
7547
- const config = getCloudConfig();
7602
+ const config = getStorageConfig();
7548
7603
  const { host, port, username, password_env, ssl } = config.rds;
7549
7604
  if (!host || !username) {
7550
- throw new Error("Cloud database is not configured. Set HASNA_SEARCH_CLOUD_DATABASE_URL or configure ~/.hasna/search/cloud/config.json.");
7605
+ throw new Error("Storage database is not configured. Set HASNA_SEARCH_DATABASE_URL or configure ~/.hasna/search/storage/config.json.");
7551
7606
  }
7552
- const password = Bun.env[password_env];
7607
+ const password = process.env[password_env];
7553
7608
  if (!password) {
7554
- throw new Error(`Cloud database password is not set. Export ${password_env}.`);
7609
+ throw new Error(`Storage database password is not set. Export ${password_env}.`);
7555
7610
  }
7556
7611
  const sslParam = ssl ? "?sslmode=require" : "";
7557
7612
  return `postgres://${username}:${encodeURIComponent(password)}@${host}:${port}/${dbName}${sslParam}`;
7558
7613
  }
7559
7614
 
7560
- // src/db/cloud-sync.ts
7615
+ // src/db/storage-sync.ts
7561
7616
  init_database();
7562
7617
  init_remote_storage();
7563
7618
  init_pg_migrations();
7564
- var CLOUD_TABLES = [
7619
+ var STORAGE_TABLES = [
7565
7620
  "searches",
7566
7621
  "search_results",
7567
7622
  "saved_searches",
@@ -7648,21 +7703,26 @@ function upsertSqlite(db, table, rows) {
7648
7703
  }
7649
7704
  return written;
7650
7705
  }
7651
- async function getCloudPg() {
7652
- return new PgAdapterAsync(getConnectionString("search"));
7706
+ async function getStoragePg() {
7707
+ return new PgAdapterAsync(getStorageConnectionString("search"));
7653
7708
  }
7654
- async function runCloudMigrations(remote) {
7709
+ async function runStorageMigrations(remote) {
7655
7710
  for (const migration of PG_MIGRATIONS) {
7656
7711
  await remote.exec(migration);
7657
7712
  }
7658
7713
  }
7659
- function getCloudStatus(db = getDb()) {
7660
- const config = getCloudConfig();
7714
+ function getStorageStatus(db = getDb()) {
7715
+ const config = getStorageConfig();
7716
+ const activeEnv = getStorageDatabaseEnv();
7661
7717
  return {
7718
+ configured: Boolean(activeEnv) || Boolean(config.rds.host && config.rds.username),
7662
7719
  mode: config.mode,
7663
- enabled: config.mode === "hybrid" || config.mode === "cloud",
7720
+ enabled: config.mode === "hybrid" || config.mode === "remote",
7721
+ env: STORAGE_DATABASE_ENV,
7722
+ activeEnv: activeEnv?.name ?? null,
7723
+ service: "search",
7664
7724
  db_path: getDbPath(),
7665
- tables: CLOUD_TABLES.map((table) => {
7725
+ tables: STORAGE_TABLES.map((table) => {
7666
7726
  try {
7667
7727
  const row = db.query(`SELECT COUNT(*) as count FROM ${quoteId(table)}`).get();
7668
7728
  return { table, rows: row.count };
@@ -7672,12 +7732,12 @@ function getCloudStatus(db = getDb()) {
7672
7732
  })
7673
7733
  };
7674
7734
  }
7675
- async function pushCloudChanges(tables = [...CLOUD_TABLES]) {
7735
+ async function pushStorageChanges(tables = [...STORAGE_TABLES]) {
7676
7736
  const db = getDb();
7677
- const remote = await getCloudPg();
7737
+ const remote = await getStoragePg();
7678
7738
  const results = [];
7679
7739
  try {
7680
- await runCloudMigrations(remote);
7740
+ await runStorageMigrations(remote);
7681
7741
  for (const table of tables) {
7682
7742
  const result = { table, direction: "push", rowsRead: 0, rowsWritten: 0, errors: [] };
7683
7743
  try {
@@ -7694,12 +7754,12 @@ async function pushCloudChanges(tables = [...CLOUD_TABLES]) {
7694
7754
  }
7695
7755
  return results;
7696
7756
  }
7697
- async function pullCloudChanges(tables = [...CLOUD_TABLES]) {
7757
+ async function pullStorageChanges(tables = [...STORAGE_TABLES]) {
7698
7758
  const db = getDb();
7699
- const remote = await getCloudPg();
7759
+ const remote = await getStoragePg();
7700
7760
  const results = [];
7701
7761
  try {
7702
- await runCloudMigrations(remote);
7762
+ await runStorageMigrations(remote);
7703
7763
  for (const table of tables) {
7704
7764
  const result = { table, direction: "pull", rowsRead: 0, rowsWritten: 0, errors: [] };
7705
7765
  try {
@@ -7716,20 +7776,24 @@ async function pullCloudChanges(tables = [...CLOUD_TABLES]) {
7716
7776
  }
7717
7777
  return results;
7718
7778
  }
7719
- async function syncCloudChanges(tables = [...CLOUD_TABLES]) {
7779
+ async function syncStorageChanges(tables = [...STORAGE_TABLES]) {
7720
7780
  return {
7721
- push: await pushCloudChanges(tables),
7722
- pull: await pullCloudChanges(tables)
7781
+ push: await pushStorageChanges(tables),
7782
+ pull: await pullStorageChanges(tables)
7723
7783
  };
7724
7784
  }
7725
- function parseCloudTables(raw) {
7785
+ function parseStorageTables(raw) {
7726
7786
  if (!raw)
7727
- return [...CLOUD_TABLES];
7787
+ return [...STORAGE_TABLES];
7728
7788
  const requested = raw.split(",").map((table) => table.trim()).filter(Boolean);
7729
- return requested.length > 0 ? requested : [...CLOUD_TABLES];
7789
+ const allowed = new Set(STORAGE_TABLES);
7790
+ const invalid = requested.filter((table) => !allowed.has(table));
7791
+ if (invalid.length > 0)
7792
+ throw new Error(`Unknown search storage table(s): ${invalid.join(", ")}`);
7793
+ return requested.length > 0 ? requested : [...STORAGE_TABLES];
7730
7794
  }
7731
7795
 
7732
- // src/cli/cloud.ts
7796
+ // src/cli/storage.ts
7733
7797
  function printJson(value) {
7734
7798
  console.log(JSON.stringify(value, null, 2));
7735
7799
  }
@@ -7742,10 +7806,10 @@ function printResults(results) {
7742
7806
  }
7743
7807
  }
7744
7808
  }
7745
- function registerCloudCommands(program2) {
7746
- const cloud = program2.command("cloud").description("Manage search local/remote cloud sync");
7747
- cloud.command("status").description("Show local database and cloud sync status").option("--json", "Output as JSON").action((opts) => {
7748
- const status = getCloudStatus();
7809
+ function registerStorageCommands(program2) {
7810
+ const storage = program2.command("storage").description("Manage search local/remote storage sync");
7811
+ storage.command("status").description("Show local database and storage sync status").option("--json", "Output as JSON").action((opts) => {
7812
+ const status = getStorageStatus();
7749
7813
  if (opts.json) {
7750
7814
  printJson(status);
7751
7815
  return;
@@ -7757,9 +7821,9 @@ function registerCloudCommands(program2) {
7757
7821
  console.log(` ${table.table}: ${table.rows}`);
7758
7822
  }
7759
7823
  });
7760
- cloud.command("push").description("Push local search data to PostgreSQL").option("--tables <tables>", "Comma-separated table names").option("--json", "Output as JSON").action(async (opts) => {
7824
+ storage.command("push").description("Push local search data to PostgreSQL").option("--tables <tables>", "Comma-separated table names").option("--json", "Output as JSON").action(async (opts) => {
7761
7825
  try {
7762
- const results = await pushCloudChanges(parseCloudTables(opts.tables));
7826
+ const results = await pushStorageChanges(parseStorageTables(opts.tables));
7763
7827
  if (opts.json)
7764
7828
  printJson(results);
7765
7829
  else
@@ -7772,9 +7836,9 @@ function registerCloudCommands(program2) {
7772
7836
  process.exitCode = 1;
7773
7837
  }
7774
7838
  });
7775
- cloud.command("pull").description("Pull PostgreSQL search data into the local database").option("--tables <tables>", "Comma-separated table names").option("--json", "Output as JSON").action(async (opts) => {
7839
+ storage.command("pull").description("Pull PostgreSQL search data into the local database").option("--tables <tables>", "Comma-separated table names").option("--json", "Output as JSON").action(async (opts) => {
7776
7840
  try {
7777
- const results = await pullCloudChanges(parseCloudTables(opts.tables));
7841
+ const results = await pullStorageChanges(parseStorageTables(opts.tables));
7778
7842
  if (opts.json)
7779
7843
  printJson(results);
7780
7844
  else
@@ -7787,9 +7851,9 @@ function registerCloudCommands(program2) {
7787
7851
  process.exitCode = 1;
7788
7852
  }
7789
7853
  });
7790
- cloud.command("sync").description("Push local changes, then pull remote changes").option("--tables <tables>", "Comma-separated table names").option("--json", "Output as JSON").action(async (opts) => {
7854
+ storage.command("sync").description("Push local changes, then pull remote changes").option("--tables <tables>", "Comma-separated table names").option("--json", "Output as JSON").action(async (opts) => {
7791
7855
  try {
7792
- const result = await syncCloudChanges(parseCloudTables(opts.tables));
7856
+ const result = await syncStorageChanges(parseStorageTables(opts.tables));
7793
7857
  if (opts.json) {
7794
7858
  printJson(result);
7795
7859
  return;
@@ -7806,10 +7870,10 @@ function registerCloudCommands(program2) {
7806
7870
  process.exitCode = 1;
7807
7871
  }
7808
7872
  });
7809
- cloud.command("migrate").description("Apply PostgreSQL migrations").option("--connection-string <url>", "PostgreSQL connection string").option("--json", "Output as JSON").action(async (opts) => {
7873
+ storage.command("migrate").description("Apply PostgreSQL migrations").option("--connection-string <url>", "PostgreSQL connection string").option("--json", "Output as JSON").action(async (opts) => {
7810
7874
  try {
7811
7875
  const { applyPgMigrations: applyPgMigrations2 } = await Promise.resolve().then(() => (init_pg_migrate(), exports_pg_migrate));
7812
- const result = await applyPgMigrations2(opts.connectionString || getConnectionString("search"));
7876
+ const result = await applyPgMigrations2(opts.connectionString || getStorageConnectionString("search"));
7813
7877
  if (opts.json) {
7814
7878
  printJson(result);
7815
7879
  return;
@@ -7833,6 +7897,144 @@ function registerCloudCommands(program2) {
7833
7897
  });
7834
7898
  }
7835
7899
 
7900
+ // src/cli/local.ts
7901
+ import chalk2 from "chalk";
7902
+
7903
+ // src/lib/local/indexer.ts
7904
+ import { readFileSync as readFileSync4, statSync as statSync2 } from "fs";
7905
+ import { homedir as homedir2 } from "os";
7906
+ import { basename, resolve } from "path";
7907
+
7908
+ // src/db/index-db.ts
7909
+ import { Database as Database2 } from "bun:sqlite";
7910
+ import { mkdirSync as mkdirSync2 } from "fs";
7911
+
7912
+ // src/db/index-migrations.ts
7913
+ var migrations2 = [
7914
+ {
7915
+ version: 1,
7916
+ description: "Local file index core",
7917
+ up: (db) => {
7918
+ db.exec(`
7919
+ CREATE TABLE IF NOT EXISTS index_roots (
7920
+ id TEXT PRIMARY KEY,
7921
+ path TEXT NOT NULL UNIQUE,
7922
+ name TEXT NOT NULL,
7923
+ exclude TEXT NOT NULL DEFAULT '[]',
7924
+ content_indexing INTEGER NOT NULL DEFAULT 1,
7925
+ max_file_size INTEGER NOT NULL DEFAULT 524288,
7926
+ status TEXT NOT NULL DEFAULT 'pending',
7927
+ error TEXT,
7928
+ file_count INTEGER NOT NULL DEFAULT 0,
7929
+ last_indexed_at TEXT,
7930
+ last_duration_ms INTEGER,
7931
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
7932
+ );
7933
+
7934
+ CREATE TABLE IF NOT EXISTS files (
7935
+ id INTEGER PRIMARY KEY,
7936
+ root_id TEXT NOT NULL REFERENCES index_roots(id) ON DELETE CASCADE,
7937
+ rel_path TEXT NOT NULL,
7938
+ name TEXT NOT NULL,
7939
+ ext TEXT NOT NULL DEFAULT '',
7940
+ dir TEXT NOT NULL DEFAULT '',
7941
+ size INTEGER NOT NULL,
7942
+ mtime_ms INTEGER NOT NULL,
7943
+ is_binary INTEGER NOT NULL DEFAULT 0,
7944
+ content_indexed INTEGER NOT NULL DEFAULT 0,
7945
+ indexed_at TEXT NOT NULL,
7946
+ UNIQUE(root_id, rel_path)
7947
+ );
7948
+ CREATE INDEX IF NOT EXISTS idx_files_root ON files(root_id);
7949
+ CREATE INDEX IF NOT EXISTS idx_files_name ON files(name);
7950
+
7951
+ CREATE VIRTUAL TABLE IF NOT EXISTS files_fts USING fts5(
7952
+ name, rel_path,
7953
+ content='files',
7954
+ content_rowid='id',
7955
+ tokenize='trigram'
7956
+ );
7957
+
7958
+ CREATE TRIGGER IF NOT EXISTS files_ai AFTER INSERT ON files BEGIN
7959
+ INSERT INTO files_fts(rowid, name, rel_path)
7960
+ VALUES (NEW.id, NEW.name, NEW.rel_path);
7961
+ END;
7962
+
7963
+ CREATE TRIGGER IF NOT EXISTS files_ad AFTER DELETE ON files BEGIN
7964
+ INSERT INTO files_fts(files_fts, rowid, name, rel_path)
7965
+ VALUES ('delete', OLD.id, OLD.name, OLD.rel_path);
7966
+ END;
7967
+
7968
+ CREATE TRIGGER IF NOT EXISTS files_au AFTER UPDATE OF name, rel_path ON files BEGIN
7969
+ INSERT INTO files_fts(files_fts, rowid, name, rel_path)
7970
+ VALUES ('delete', OLD.id, OLD.name, OLD.rel_path);
7971
+ INSERT INTO files_fts(rowid, name, rel_path)
7972
+ VALUES (NEW.id, NEW.name, NEW.rel_path);
7973
+ END;
7974
+
7975
+ CREATE VIRTUAL TABLE IF NOT EXISTS file_content_fts USING fts5(
7976
+ body,
7977
+ content='',
7978
+ contentless_delete=1,
7979
+ tokenize='trigram'
7980
+ );
7981
+ `);
7982
+ }
7983
+ }
7984
+ ];
7985
+ function runIndexMigrations(db) {
7986
+ db.exec(`
7987
+ CREATE TABLE IF NOT EXISTS _migrations (
7988
+ version INTEGER PRIMARY KEY,
7989
+ description TEXT NOT NULL,
7990
+ applied_at TEXT NOT NULL DEFAULT (datetime('now'))
7991
+ );
7992
+ `);
7993
+ const applied = new Set(db.query("SELECT version FROM _migrations").all().map((row) => row.version));
7994
+ for (const migration of migrations2) {
7995
+ if (applied.has(migration.version))
7996
+ continue;
7997
+ db.exec("BEGIN");
7998
+ try {
7999
+ migration.up(db);
8000
+ db.prepare("INSERT OR IGNORE INTO _migrations (version, description) VALUES (?, ?)").run(migration.version, migration.description);
8001
+ db.exec("COMMIT");
8002
+ } catch (err) {
8003
+ db.exec("ROLLBACK");
8004
+ throw err;
8005
+ }
8006
+ }
8007
+ }
8008
+
8009
+ // src/db/index-db.ts
8010
+ var instance2 = null;
8011
+ function getIndexDbPath() {
8012
+ const envPath = Bun.env.HASNA_SEARCH_INDEX_DB_PATH ?? Bun.env.SEARCH_INDEX_DB_PATH;
8013
+ if (envPath)
8014
+ return envPath;
8015
+ const home = Bun.env.HOME ?? "/tmp";
8016
+ const dir = `${home}/.hasna/search`;
8017
+ mkdirSync2(dir, { recursive: true });
8018
+ return `${dir}/index.db`;
8019
+ }
8020
+ function configure(db) {
8021
+ db.exec("PRAGMA busy_timeout = 5000");
8022
+ db.exec("PRAGMA journal_mode = WAL");
8023
+ db.exec("PRAGMA foreign_keys = ON");
8024
+ db.exec("PRAGMA synchronous = NORMAL");
8025
+ runIndexMigrations(db);
8026
+ return db;
8027
+ }
8028
+ function getIndexDb() {
8029
+ if (instance2)
8030
+ return instance2;
8031
+ instance2 = configure(new Database2(getIndexDbPath()));
8032
+ return instance2;
8033
+ }
8034
+
8035
+ // src/lib/config.ts
8036
+ import { mkdirSync as mkdirSync3, readFileSync as readFileSync2, writeFileSync, existsSync as existsSync2 } from "fs";
8037
+
7836
8038
  // node_modules/zod/v3/external.js
7837
8039
  var exports_external = {};
7838
8040
  __export(exports_external, {
@@ -11819,9 +12021,15 @@ var PROVIDER_NAMES = [
11819
12021
  "youtube",
11820
12022
  "hackernews",
11821
12023
  "github",
11822
- "arxiv"
12024
+ "arxiv",
12025
+ "files",
12026
+ "content"
11823
12027
  ];
11824
12028
  var SearchProviderNameSchema = exports_external.enum(PROVIDER_NAMES);
12029
+ var LOCAL_PROVIDER_NAMES = new Set([
12030
+ "files",
12031
+ "content"
12032
+ ]);
11825
12033
  var EXPORT_FORMATS = ["json", "csv", "md"];
11826
12034
  var ExportFormatSchema = exports_external.enum(EXPORT_FORMATS);
11827
12035
  var SearchOptionsSchema = exports_external.object({
@@ -11844,7 +12052,10 @@ var DEFAULT_CONFIG = {
11844
12052
  fallbackCli: "microservice-transcriber"
11845
12053
  },
11846
12054
  dedup: true,
11847
- maxConcurrent: 5
12055
+ maxConcurrent: 5,
12056
+ indexStaleMinutes: 5,
12057
+ indexAutoRefresh: true,
12058
+ recordLocalResults: false
11848
12059
  };
11849
12060
  var counter = 0;
11850
12061
  function generateId() {
@@ -11854,6 +12065,1320 @@ function generateId() {
11854
12065
  return `${timestamp}-${random}-${counter.toString(36)}`;
11855
12066
  }
11856
12067
 
12068
+ // src/lib/config.ts
12069
+ function getConfigDir() {
12070
+ const home = Bun.env.HOME ?? "/tmp";
12071
+ const dir = `${home}/.hasna/search`;
12072
+ mkdirSync3(dir, { recursive: true });
12073
+ return dir;
12074
+ }
12075
+ function getConfigPath() {
12076
+ return `${getConfigDir()}/config.json`;
12077
+ }
12078
+ function getConfig() {
12079
+ const path = getConfigPath();
12080
+ if (!existsSync2(path)) {
12081
+ return { ...DEFAULT_CONFIG };
12082
+ }
12083
+ try {
12084
+ const raw = readFileSync2(path, "utf-8");
12085
+ const parsed = JSON.parse(raw);
12086
+ return { ...DEFAULT_CONFIG, ...parsed };
12087
+ } catch {
12088
+ return { ...DEFAULT_CONFIG };
12089
+ }
12090
+ }
12091
+ function setConfig(updates) {
12092
+ const current = getConfig();
12093
+ const merged = { ...current, ...updates };
12094
+ const path = getConfigPath();
12095
+ writeFileSync(path, JSON.stringify(merged, null, 2), "utf-8");
12096
+ return merged;
12097
+ }
12098
+ function resetConfig() {
12099
+ const path = getConfigPath();
12100
+ writeFileSync(path, JSON.stringify(DEFAULT_CONFIG, null, 2), "utf-8");
12101
+ return { ...DEFAULT_CONFIG };
12102
+ }
12103
+ function setConfigValue(key, value) {
12104
+ const config = getConfig();
12105
+ config[key] = value;
12106
+ return setConfig(config);
12107
+ }
12108
+
12109
+ // src/lib/local/walker.ts
12110
+ import { readdirSync, readFileSync as readFileSync3, statSync, openSync, readSync, closeSync } from "fs";
12111
+ import { join as join2 } from "path";
12112
+
12113
+ // src/lib/local/ignore.ts
12114
+ function classEnd(pattern, open) {
12115
+ let i = open + 1;
12116
+ if (pattern[i] === "!")
12117
+ i++;
12118
+ if (pattern[i] === "]")
12119
+ i++;
12120
+ for (;i < pattern.length; i++) {
12121
+ if (pattern[i] === "\\") {
12122
+ i++;
12123
+ continue;
12124
+ }
12125
+ if (pattern[i] === "]")
12126
+ return i;
12127
+ }
12128
+ return -1;
12129
+ }
12130
+ function globToRegExp(pattern) {
12131
+ let out = "";
12132
+ let i = 0;
12133
+ while (i < pattern.length) {
12134
+ const ch = pattern[i];
12135
+ if (ch === "*") {
12136
+ if (pattern[i + 1] === "*") {
12137
+ const segmentStart = i === 0 || pattern[i - 1] === "/";
12138
+ if (segmentStart && pattern[i + 2] === "/") {
12139
+ out += "(?:[^/]*/)*";
12140
+ i += 3;
12141
+ } else if (segmentStart && i + 2 === pattern.length) {
12142
+ out += ".*";
12143
+ i += 2;
12144
+ } else {
12145
+ out += "[^/]*";
12146
+ i += 2;
12147
+ }
12148
+ } else {
12149
+ out += "[^/]*";
12150
+ i += 1;
12151
+ }
12152
+ } else if (ch === "?") {
12153
+ out += "[^/]";
12154
+ i += 1;
12155
+ } else if (ch === "[") {
12156
+ const close = classEnd(pattern, i);
12157
+ if (close === -1) {
12158
+ out += "\\[";
12159
+ i += 1;
12160
+ } else {
12161
+ let cls = pattern.slice(i + 1, close);
12162
+ if (cls.startsWith("!"))
12163
+ cls = "^" + cls.slice(1);
12164
+ let jsCls = "";
12165
+ for (let k = 0;k < cls.length; k++) {
12166
+ const c = cls[k];
12167
+ if (c === "\\" && k + 1 < cls.length) {
12168
+ jsCls += "\\" + cls[k + 1];
12169
+ k++;
12170
+ } else if (c === "]") {
12171
+ jsCls += "\\]";
12172
+ } else if (c === "\\") {
12173
+ jsCls += "\\\\";
12174
+ } else {
12175
+ jsCls += c;
12176
+ }
12177
+ }
12178
+ out += `[${jsCls}]`;
12179
+ i = close + 1;
12180
+ }
12181
+ } else if (ch === "\\" && i + 1 < pattern.length) {
12182
+ out += pattern[i + 1].replace(/[.+^${}()|[\]\\*?]/g, "\\$&");
12183
+ i += 2;
12184
+ } else {
12185
+ out += ch.replace(/[.+^${}()|[\]\\]/g, "\\$&");
12186
+ i += 1;
12187
+ }
12188
+ }
12189
+ return out;
12190
+ }
12191
+ function compile(raw) {
12192
+ let pattern = raw.replace(/\r$/, "");
12193
+ if (!pattern.trim() || pattern.startsWith("#"))
12194
+ return null;
12195
+ let negated = false;
12196
+ if (pattern.startsWith("!")) {
12197
+ negated = true;
12198
+ pattern = pattern.slice(1);
12199
+ }
12200
+ pattern = pattern.replace(/(?<!\\) +$/, "");
12201
+ if (!pattern)
12202
+ return null;
12203
+ let dirOnly = false;
12204
+ if (pattern.endsWith("/")) {
12205
+ dirOnly = true;
12206
+ pattern = pattern.slice(0, -1);
12207
+ }
12208
+ const anchored = pattern.includes("/");
12209
+ if (pattern.startsWith("/"))
12210
+ pattern = pattern.slice(1);
12211
+ const body = globToRegExp(pattern);
12212
+ const prefix = anchored ? "^" : "^(?:.*/)?";
12213
+ const regex = new RegExp(`${prefix}${body}$`);
12214
+ return { regex, negated, dirOnly };
12215
+ }
12216
+
12217
+ class IgnoreMatcher {
12218
+ patterns;
12219
+ base;
12220
+ constructor(lines, base = "") {
12221
+ this.base = base;
12222
+ this.patterns = lines.map(compile).filter((p) => p !== null);
12223
+ }
12224
+ ignores(relPath, isDir) {
12225
+ let local = relPath;
12226
+ if (this.base) {
12227
+ if (!relPath.startsWith(this.base + "/"))
12228
+ return;
12229
+ local = relPath.slice(this.base.length + 1);
12230
+ }
12231
+ let result;
12232
+ for (const p of this.patterns) {
12233
+ if (p.dirOnly && !isDir)
12234
+ continue;
12235
+ if (p.regex.test(local))
12236
+ result = !p.negated;
12237
+ }
12238
+ return result;
12239
+ }
12240
+ }
12241
+
12242
+ class IgnoreStack {
12243
+ hard;
12244
+ stack = [];
12245
+ constructor(hard = null) {
12246
+ this.hard = hard;
12247
+ }
12248
+ push(matcher) {
12249
+ this.stack.push(matcher);
12250
+ }
12251
+ pop() {
12252
+ this.stack.pop();
12253
+ }
12254
+ ignores(relPath, isDir) {
12255
+ const hardResult = this.hard?.ignores(relPath, isDir);
12256
+ if (hardResult !== undefined)
12257
+ return hardResult;
12258
+ for (let i = this.stack.length - 1;i >= 0; i--) {
12259
+ const result = this.stack[i].ignores(relPath, isDir);
12260
+ if (result !== undefined)
12261
+ return result;
12262
+ }
12263
+ return false;
12264
+ }
12265
+ }
12266
+ var DEFAULT_EXCLUDES = [
12267
+ ".git/",
12268
+ "node_modules/",
12269
+ "__pycache__/",
12270
+ ".venv/",
12271
+ "venv/",
12272
+ ".tox/",
12273
+ "dist/",
12274
+ "build/",
12275
+ "out/",
12276
+ ".next/",
12277
+ ".nuxt/",
12278
+ ".cache/",
12279
+ ".turbo/",
12280
+ "target/",
12281
+ "coverage/",
12282
+ ".pnpm-store/",
12283
+ ".bun/",
12284
+ ".DS_Store"
12285
+ ];
12286
+
12287
+ // src/lib/local/walker.ts
12288
+ var BINARY_EXTENSIONS = new Set([
12289
+ "png",
12290
+ "jpg",
12291
+ "jpeg",
12292
+ "gif",
12293
+ "webp",
12294
+ "avif",
12295
+ "ico",
12296
+ "bmp",
12297
+ "tiff",
12298
+ "heic",
12299
+ "pdf",
12300
+ "zip",
12301
+ "gz",
12302
+ "tgz",
12303
+ "tar",
12304
+ "bz2",
12305
+ "xz",
12306
+ "zst",
12307
+ "7z",
12308
+ "rar",
12309
+ "exe",
12310
+ "dll",
12311
+ "so",
12312
+ "dylib",
12313
+ "a",
12314
+ "o",
12315
+ "obj",
12316
+ "class",
12317
+ "jar",
12318
+ "war",
12319
+ "pyc",
12320
+ "wasm",
12321
+ "node",
12322
+ "woff",
12323
+ "woff2",
12324
+ "ttf",
12325
+ "otf",
12326
+ "eot",
12327
+ "mp3",
12328
+ "mp4",
12329
+ "m4a",
12330
+ "avi",
12331
+ "mov",
12332
+ "mkv",
12333
+ "webm",
12334
+ "wav",
12335
+ "flac",
12336
+ "ogg",
12337
+ "sqlite",
12338
+ "db",
12339
+ "bin",
12340
+ "dat",
12341
+ "ds_store"
12342
+ ]);
12343
+ var CONTENT_EXCLUDED_NAMES = [/\.lock$/, /-lock\.(json|yaml)$/, /\.min\.(js|css)$/, /\.map$/, /\.svg$/];
12344
+ function hasBinaryExtension(ext) {
12345
+ return BINARY_EXTENSIONS.has(ext.toLowerCase());
12346
+ }
12347
+ function isContentExcluded(name) {
12348
+ return CONTENT_EXCLUDED_NAMES.some((re) => re.test(name));
12349
+ }
12350
+ function isBinaryFile(absPath) {
12351
+ const buf = Buffer.alloc(8192);
12352
+ let fd;
12353
+ try {
12354
+ fd = openSync(absPath, "r");
12355
+ } catch {
12356
+ return true;
12357
+ }
12358
+ try {
12359
+ const read = readSync(fd, buf, 0, buf.length, 0);
12360
+ for (let i = 0;i < read; i++) {
12361
+ if (buf[i] === 0)
12362
+ return true;
12363
+ }
12364
+ return false;
12365
+ } finally {
12366
+ closeSync(fd);
12367
+ }
12368
+ }
12369
+ function readGitignore(dirAbs) {
12370
+ try {
12371
+ return readFileSync3(join2(dirAbs, ".gitignore"), "utf-8").split(`
12372
+ `);
12373
+ } catch {
12374
+ return null;
12375
+ }
12376
+ }
12377
+ function extOf(name) {
12378
+ const idx = name.lastIndexOf(".");
12379
+ return idx > 0 ? name.slice(idx + 1).toLowerCase() : "";
12380
+ }
12381
+ function scanRoot(rootPath, extraExcludes = []) {
12382
+ const stack = new IgnoreStack(new IgnoreMatcher([...DEFAULT_EXCLUDES, ...extraExcludes]));
12383
+ const files = [];
12384
+ const skippedDirs = [];
12385
+ const walk = (dirAbs, dirRel) => {
12386
+ const gitignore = readGitignore(dirAbs);
12387
+ if (gitignore)
12388
+ stack.push(new IgnoreMatcher(gitignore, dirRel));
12389
+ let entries;
12390
+ try {
12391
+ entries = readdirSync(dirAbs, { withFileTypes: true });
12392
+ } catch (err) {
12393
+ if (gitignore)
12394
+ stack.pop();
12395
+ if (dirRel === "") {
12396
+ throw new Error(`Cannot read index root ${dirAbs}: ${err instanceof Error ? err.message : err}`);
12397
+ }
12398
+ if (err.code !== "ENOENT")
12399
+ skippedDirs.push(dirRel);
12400
+ return;
12401
+ }
12402
+ for (const entry of entries) {
12403
+ if (entry.isSymbolicLink())
12404
+ continue;
12405
+ const relPath = dirRel ? `${dirRel}/${entry.name}` : entry.name;
12406
+ if (entry.isDirectory()) {
12407
+ if (entry.name === ".git")
12408
+ continue;
12409
+ if (stack.ignores(relPath, true))
12410
+ continue;
12411
+ walk(join2(dirAbs, entry.name), relPath);
12412
+ } else if (entry.isFile()) {
12413
+ if (stack.ignores(relPath, false))
12414
+ continue;
12415
+ let stat;
12416
+ try {
12417
+ stat = statSync(join2(dirAbs, entry.name));
12418
+ } catch {
12419
+ continue;
12420
+ }
12421
+ files.push({
12422
+ relPath,
12423
+ name: entry.name,
12424
+ ext: extOf(entry.name),
12425
+ dir: dirRel,
12426
+ size: stat.size,
12427
+ mtimeMs: Math.floor(stat.mtimeMs)
12428
+ });
12429
+ }
12430
+ }
12431
+ if (gitignore)
12432
+ stack.pop();
12433
+ };
12434
+ walk(rootPath, "");
12435
+ files.sort((a, b) => a.relPath < b.relPath ? -1 : a.relPath > b.relPath ? 1 : 0);
12436
+ return { files, skippedDirs };
12437
+ }
12438
+
12439
+ // src/lib/local/indexer.ts
12440
+ function rowToRoot(row) {
12441
+ return {
12442
+ id: row.id,
12443
+ path: row.path,
12444
+ name: row.name,
12445
+ exclude: JSON.parse(row.exclude),
12446
+ contentIndexing: Boolean(row.content_indexing),
12447
+ maxFileSize: row.max_file_size,
12448
+ status: row.status,
12449
+ error: row.error,
12450
+ fileCount: row.file_count,
12451
+ lastIndexedAt: row.last_indexed_at,
12452
+ lastDurationMs: row.last_duration_ms,
12453
+ createdAt: row.created_at
12454
+ };
12455
+ }
12456
+ function normalizeRootPath(path) {
12457
+ const expanded = path === "~" ? homedir2() : path.startsWith("~/") ? `${homedir2()}/${path.slice(2)}` : path;
12458
+ return resolve(expanded.replace(/\/+$/, "") || "/");
12459
+ }
12460
+ function listRoots(db) {
12461
+ const d = db ?? getIndexDb();
12462
+ const rows = d.query("SELECT * FROM index_roots ORDER BY path").all();
12463
+ return rows.map(rowToRoot);
12464
+ }
12465
+ function getRoot(ref, db) {
12466
+ const d = db ?? getIndexDb();
12467
+ const row = d.prepare(`SELECT *, CASE WHEN id = $ref THEN 0 WHEN path = $path THEN 1 ELSE 2 END AS priority
12468
+ FROM index_roots
12469
+ WHERE id = $ref OR path = $path OR name = $ref
12470
+ ORDER BY priority LIMIT 1`).get({ $ref: ref, $path: normalizeRootPath(ref) });
12471
+ return row ? rowToRoot(row) : null;
12472
+ }
12473
+ function hasReadyRoot(db) {
12474
+ const d = db ?? getIndexDb();
12475
+ const row = d.query("SELECT 1 FROM index_roots WHERE status = 'ready' LIMIT 1").get();
12476
+ return row !== null;
12477
+ }
12478
+ function addRoot(path, opts = {}, db) {
12479
+ const d = db ?? getIndexDb();
12480
+ const normalized = normalizeRootPath(path);
12481
+ let stat;
12482
+ try {
12483
+ stat = statSync2(normalized);
12484
+ } catch {
12485
+ throw new Error(`Path does not exist: ${normalized}`);
12486
+ }
12487
+ if (!stat.isDirectory())
12488
+ throw new Error(`Not a directory: ${normalized}`);
12489
+ if (opts.maxFileSize !== undefined && (!Number.isFinite(opts.maxFileSize) || opts.maxFileSize < 1)) {
12490
+ throw new Error("maxFileSize must be a positive number of bytes");
12491
+ }
12492
+ const existing = getRoot(normalized, d);
12493
+ if (existing && existing.path === normalized) {
12494
+ throw new Error(`Root already indexed: ${existing.path} (${existing.id})`);
12495
+ }
12496
+ const name = opts.name ?? basename(normalized);
12497
+ const nameClash = d.prepare("SELECT path FROM index_roots WHERE name = ?").get(name);
12498
+ if (nameClash) {
12499
+ throw new Error(`Root name "${name}" already used by ${nameClash.path} \u2014 pass a different name.`);
12500
+ }
12501
+ const id = generateId();
12502
+ d.prepare(`INSERT INTO index_roots (id, path, name, exclude, content_indexing, max_file_size, status, created_at)
12503
+ VALUES (?, ?, ?, ?, ?, ?, 'pending', ?)`).run(id, normalized, name, JSON.stringify(opts.exclude ?? []), opts.contentIndexing === false ? 0 : 1, opts.maxFileSize ?? 524288, new Date().toISOString());
12504
+ return getRoot(id, d);
12505
+ }
12506
+ function removeRoot(idOrPath, db) {
12507
+ const d = db ?? getIndexDb();
12508
+ const root = getRoot(idOrPath, d);
12509
+ if (!root)
12510
+ return false;
12511
+ d.exec("BEGIN");
12512
+ try {
12513
+ d.prepare("DELETE FROM file_content_fts WHERE rowid IN (SELECT id FROM files WHERE root_id = ? AND content_indexed = 1)").run(root.id);
12514
+ d.prepare("DELETE FROM index_roots WHERE id = ?").run(root.id);
12515
+ d.exec("COMMIT");
12516
+ } catch (err) {
12517
+ d.exec("ROLLBACK");
12518
+ throw err;
12519
+ }
12520
+ return true;
12521
+ }
12522
+ function shouldIndexContent(root, file) {
12523
+ return root.contentIndexing && file.size > 0 && file.size <= root.maxFileSize && !hasBinaryExtension(file.ext) && !isContentExcluded(file.name);
12524
+ }
12525
+ function indexRoot(idOrPath, opts = {}, db) {
12526
+ const d = db ?? getIndexDb();
12527
+ const root = getRoot(idOrPath, d);
12528
+ if (!root)
12529
+ throw new Error(`Index root not found: ${idOrPath}`);
12530
+ const start = Date.now();
12531
+ d.prepare("UPDATE index_roots SET status = 'indexing', error = NULL WHERE id = ?").run(root.id);
12532
+ try {
12533
+ const { files: scanned, skippedDirs } = scanRoot(root.path, root.exclude);
12534
+ const now = new Date().toISOString();
12535
+ const existingRows = d.prepare("SELECT id, rel_path, size, mtime_ms, content_indexed FROM files WHERE root_id = ?").all(root.id);
12536
+ const existing = new Map(existingRows.map((r) => [r.rel_path, r]));
12537
+ const insertFile = d.prepare(`INSERT INTO files (root_id, rel_path, name, ext, dir, size, mtime_ms, is_binary, content_indexed, indexed_at)
12538
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
12539
+ const updateFile = d.prepare("UPDATE files SET size = ?, mtime_ms = ?, is_binary = ?, content_indexed = ?, indexed_at = ? WHERE id = ?");
12540
+ const deleteFile = d.prepare("DELETE FROM files WHERE id = ?");
12541
+ const insertContent = d.prepare("INSERT INTO file_content_fts (rowid, body) VALUES (?, ?)");
12542
+ const deleteContent = d.prepare("DELETE FROM file_content_fts WHERE rowid = ?");
12543
+ const stats = {
12544
+ rootId: root.id,
12545
+ added: 0,
12546
+ updated: 0,
12547
+ deleted: 0,
12548
+ contentIndexed: 0,
12549
+ fileCount: scanned.length,
12550
+ skippedDirs: skippedDirs.length,
12551
+ durationMs: 0
12552
+ };
12553
+ d.exec("BEGIN");
12554
+ try {
12555
+ const seen = new Set;
12556
+ for (const file of scanned) {
12557
+ seen.add(file.relPath);
12558
+ const prev = existing.get(file.relPath);
12559
+ const changed = !prev || prev.size !== file.size || prev.mtime_ms !== file.mtimeMs;
12560
+ if (prev && !changed && !opts.force)
12561
+ continue;
12562
+ const wantContent = shouldIndexContent(root, file);
12563
+ const absPath = `${root.path}/${file.relPath}`;
12564
+ let isBinary = wantContent ? isBinaryFile(absPath) : hasBinaryExtension(file.ext);
12565
+ let body = null;
12566
+ if (wantContent && !isBinary) {
12567
+ try {
12568
+ body = readFileSync4(absPath, "utf-8");
12569
+ } catch {
12570
+ isBinary = true;
12571
+ }
12572
+ }
12573
+ const contentIndexed = body !== null ? 1 : 0;
12574
+ if (prev) {
12575
+ if (prev.content_indexed)
12576
+ deleteContent.run(prev.id);
12577
+ updateFile.run(file.size, file.mtimeMs, isBinary ? 1 : 0, contentIndexed, now, prev.id);
12578
+ if (body !== null)
12579
+ insertContent.run(prev.id, body);
12580
+ stats.updated++;
12581
+ } else {
12582
+ const inserted = insertFile.run(root.id, file.relPath, file.name, file.ext, file.dir, file.size, file.mtimeMs, isBinary ? 1 : 0, contentIndexed, now);
12583
+ if (body !== null)
12584
+ insertContent.run(Number(inserted.lastInsertRowid), body);
12585
+ stats.added++;
12586
+ }
12587
+ if (contentIndexed)
12588
+ stats.contentIndexed++;
12589
+ }
12590
+ for (const [relPath, row] of existing) {
12591
+ if (seen.has(relPath))
12592
+ continue;
12593
+ if (row.content_indexed)
12594
+ deleteContent.run(row.id);
12595
+ deleteFile.run(row.id);
12596
+ stats.deleted++;
12597
+ }
12598
+ stats.durationMs = Date.now() - start;
12599
+ d.prepare("UPDATE index_roots SET status = 'ready', file_count = ?, last_indexed_at = ?, last_duration_ms = ? WHERE id = ?").run(scanned.length, now, stats.durationMs, root.id);
12600
+ d.exec("COMMIT");
12601
+ } catch (err) {
12602
+ d.exec("ROLLBACK");
12603
+ throw err;
12604
+ }
12605
+ return stats;
12606
+ } catch (err) {
12607
+ const message = err instanceof Error ? err.message : String(err);
12608
+ d.prepare("UPDATE index_roots SET status = 'error', error = ? WHERE id = ?").run(message, root.id);
12609
+ throw err;
12610
+ }
12611
+ }
12612
+ function indexAllRoots(opts = {}, db) {
12613
+ return listRoots(db).map((root) => indexRoot(root.id, opts, db));
12614
+ }
12615
+ var refreshing = new Set;
12616
+ function refreshStaleRoots(staleMinutes, db) {
12617
+ const cutoff = Date.now() - staleMinutes * 60000;
12618
+ const stats = [];
12619
+ for (const root of listRoots(db)) {
12620
+ if (root.status === "indexing" || root.status === "pending")
12621
+ continue;
12622
+ if (root.lastIndexedAt && Date.parse(root.lastIndexedAt) > cutoff)
12623
+ continue;
12624
+ if (refreshing.has(root.id))
12625
+ continue;
12626
+ refreshing.add(root.id);
12627
+ try {
12628
+ stats.push(indexRoot(root.id, {}, db));
12629
+ } catch {} finally {
12630
+ refreshing.delete(root.id);
12631
+ }
12632
+ }
12633
+ return stats;
12634
+ }
12635
+ function autoRefreshStaleRoots(db) {
12636
+ const config = getConfig();
12637
+ if (!config.indexAutoRefresh)
12638
+ return [];
12639
+ return refreshStaleRoots(config.indexStaleMinutes, db);
12640
+ }
12641
+
12642
+ // src/lib/local/query.ts
12643
+ import { existsSync as existsSync3, readFileSync as readFileSync5 } from "fs";
12644
+
12645
+ // src/lib/local/regex.ts
12646
+ function extractRegexLiterals(pattern) {
12647
+ const branches = splitTopLevelAlternation(pattern);
12648
+ const result = [];
12649
+ for (const branch of branches) {
12650
+ const literals = extractSequenceLiterals(branch).filter((l) => l.length >= 3);
12651
+ if (literals.length === 0)
12652
+ return null;
12653
+ result.push({ literals });
12654
+ }
12655
+ return result;
12656
+ }
12657
+ function splitTopLevelAlternation(pattern) {
12658
+ const branches = [];
12659
+ let depth = 0;
12660
+ let inClass = false;
12661
+ let current = "";
12662
+ for (let i = 0;i < pattern.length; i++) {
12663
+ const ch = pattern[i];
12664
+ if (ch === "\\") {
12665
+ current += ch + (pattern[i + 1] ?? "");
12666
+ i++;
12667
+ continue;
12668
+ }
12669
+ if (inClass) {
12670
+ if (ch === "]")
12671
+ inClass = false;
12672
+ current += ch;
12673
+ continue;
12674
+ }
12675
+ if (ch === "[") {
12676
+ inClass = true;
12677
+ current += ch;
12678
+ } else if (ch === "(") {
12679
+ depth++;
12680
+ current += ch;
12681
+ } else if (ch === ")") {
12682
+ depth--;
12683
+ current += ch;
12684
+ } else if (ch === "|" && depth === 0) {
12685
+ branches.push(current);
12686
+ current = "";
12687
+ } else {
12688
+ current += ch;
12689
+ }
12690
+ }
12691
+ branches.push(current);
12692
+ return branches;
12693
+ }
12694
+ function extractSequenceLiterals(seq) {
12695
+ const literals = [];
12696
+ let run = "";
12697
+ const flush = () => {
12698
+ if (run.length > 0)
12699
+ literals.push(run.toLowerCase());
12700
+ run = "";
12701
+ };
12702
+ let i = 0;
12703
+ while (i < seq.length) {
12704
+ const ch = seq[i];
12705
+ if (ch === "\\") {
12706
+ const next = seq[i + 1];
12707
+ if (next === undefined)
12708
+ break;
12709
+ if (/[a-zA-Z0-9]/.test(next)) {
12710
+ flush();
12711
+ let len = 2;
12712
+ if (next === "x") {
12713
+ len = 4;
12714
+ } else if (next === "u") {
12715
+ if (seq[i + 2] === "{") {
12716
+ const close = seq.indexOf("}", i + 2);
12717
+ len = close === -1 ? 2 : close - i + 1;
12718
+ } else {
12719
+ len = 6;
12720
+ }
12721
+ } else if (next === "c") {
12722
+ len = 3;
12723
+ } else if (next === "k" && seq[i + 2] === "<") {
12724
+ const close = seq.indexOf(">", i + 2);
12725
+ len = close === -1 ? 2 : close - i + 1;
12726
+ } else if ((next === "p" || next === "P") && seq[i + 2] === "{") {
12727
+ const close = seq.indexOf("}", i + 2);
12728
+ len = close === -1 ? 2 : close - i + 1;
12729
+ }
12730
+ i += len;
12731
+ continue;
12732
+ }
12733
+ const q2 = quantifierAt(seq, i + 2);
12734
+ if (q2.optional) {
12735
+ flush();
12736
+ } else {
12737
+ run += next;
12738
+ if (q2.repeats)
12739
+ flush();
12740
+ }
12741
+ i += 2 + q2.length;
12742
+ continue;
12743
+ }
12744
+ if (ch === "(") {
12745
+ const end = findGroupEnd(seq, i);
12746
+ const inner = seq.slice(i + 1, end);
12747
+ const q2 = quantifierAt(seq, end + 1);
12748
+ flush();
12749
+ const isSpecial = inner.startsWith("?") && !inner.startsWith("?:") && !inner.startsWith("?<");
12750
+ const isNamed = inner.startsWith("?<") && !inner.startsWith("?<=") && !inner.startsWith("?<!");
12751
+ const body = inner.startsWith("?:") ? inner.slice(2) : isNamed ? inner.replace(/^\?<[^>]*>/, "") : inner;
12752
+ if (!isSpecial && !q2.optional && !inner.startsWith("?<=") && !inner.startsWith("?<!")) {
12753
+ const groupBranches = splitTopLevelAlternation(body);
12754
+ if (groupBranches.length === 1) {
12755
+ literals.push(...extractSequenceLiterals(body).filter((l) => l.length >= 3));
12756
+ }
12757
+ }
12758
+ i = end + 1 + q2.length;
12759
+ continue;
12760
+ }
12761
+ if (ch === "[") {
12762
+ const end = findClassEnd(seq, i);
12763
+ const q2 = quantifierAt(seq, end + 1);
12764
+ flush();
12765
+ i = end + 1 + q2.length;
12766
+ continue;
12767
+ }
12768
+ if (ch === "." || ch === "^" || ch === "$") {
12769
+ flush();
12770
+ i++;
12771
+ continue;
12772
+ }
12773
+ if (ch === "*" || ch === "?" || ch === "+" || ch === "{") {
12774
+ const q2 = quantifierAt(seq, i);
12775
+ i += Math.max(1, q2.length);
12776
+ continue;
12777
+ }
12778
+ const q = quantifierAt(seq, i + 1);
12779
+ if (q.optional) {
12780
+ flush();
12781
+ } else {
12782
+ run += ch;
12783
+ if (q.repeats)
12784
+ flush();
12785
+ }
12786
+ i += 1 + q.length;
12787
+ }
12788
+ flush();
12789
+ return literals;
12790
+ }
12791
+ function quantifierAt(seq, pos) {
12792
+ const ch = seq[pos];
12793
+ if (ch === "?")
12794
+ return { length: 1, optional: true, repeats: false };
12795
+ if (ch === "*")
12796
+ return { length: 1, optional: true, repeats: true };
12797
+ if (ch === "+")
12798
+ return { length: 1, optional: false, repeats: true };
12799
+ if (ch === "{") {
12800
+ const end = seq.indexOf("}", pos);
12801
+ if (end === -1)
12802
+ return { length: 0, optional: false, repeats: false };
12803
+ const body = seq.slice(pos + 1, end);
12804
+ const min = parseInt(body, 10);
12805
+ const lazy = seq[end + 1] === "?" ? 1 : 0;
12806
+ return { length: end - pos + 1 + lazy, optional: !(min >= 1), repeats: true };
12807
+ }
12808
+ return { length: 0, optional: false, repeats: false };
12809
+ }
12810
+ function findGroupEnd(seq, start) {
12811
+ let depth = 0;
12812
+ let inClass = false;
12813
+ for (let i = start;i < seq.length; i++) {
12814
+ const ch = seq[i];
12815
+ if (ch === "\\") {
12816
+ i++;
12817
+ continue;
12818
+ }
12819
+ if (inClass) {
12820
+ if (ch === "]")
12821
+ inClass = false;
12822
+ continue;
12823
+ }
12824
+ if (ch === "[")
12825
+ inClass = true;
12826
+ else if (ch === "(")
12827
+ depth++;
12828
+ else if (ch === ")") {
12829
+ depth--;
12830
+ if (depth === 0)
12831
+ return i;
12832
+ }
12833
+ }
12834
+ return seq.length - 1;
12835
+ }
12836
+ function findClassEnd(seq, start) {
12837
+ for (let i = start + 1;i < seq.length; i++) {
12838
+ const ch = seq[i];
12839
+ if (ch === "\\") {
12840
+ i++;
12841
+ continue;
12842
+ }
12843
+ if (ch === "]" && i > start + 1)
12844
+ return i;
12845
+ }
12846
+ return seq.length - 1;
12847
+ }
12848
+ function buildFtsQueryFromRegex(pattern) {
12849
+ const branches = extractRegexLiterals(pattern);
12850
+ if (!branches)
12851
+ return null;
12852
+ const branchExprs = branches.map((b) => {
12853
+ const ands = b.literals.map((l) => l.replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/g, "")).filter((l) => l.length >= 3).map((l) => `"${l.replace(/"/g, '""')}"`).join(" AND ");
12854
+ return branches.length > 1 ? `(${ands})` : ands;
12855
+ });
12856
+ if (branchExprs.some((e) => e.length === 0))
12857
+ return null;
12858
+ return branchExprs.join(" OR ");
12859
+ }
12860
+ function compileSearchRegex(pattern, caseSensitive = false) {
12861
+ return new RegExp(pattern, caseSensitive ? "" : "i");
12862
+ }
12863
+
12864
+ // src/lib/local/query.ts
12865
+ var MAX_LINE_LENGTH = 200;
12866
+ var MAX_MATCHES_PER_FILE = 5;
12867
+ function tokenize(query) {
12868
+ return query.replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/g, "").split(/\s+/).filter(Boolean);
12869
+ }
12870
+ function buildFtsQuery(query) {
12871
+ const tokens = tokenize(query).filter((t) => t.length >= 3);
12872
+ if (tokens.length === 0)
12873
+ return null;
12874
+ return tokens.map((t) => `"${t.replace(/"/g, '""')}"`).join(" AND ");
12875
+ }
12876
+ function clampLimit(limit, fallback = 20) {
12877
+ if (limit === undefined || !Number.isFinite(limit))
12878
+ return fallback;
12879
+ return Math.max(1, Math.min(500, Math.floor(limit)));
12880
+ }
12881
+ function filterClauses(opts, db) {
12882
+ const clauses = [];
12883
+ const params = [];
12884
+ if (opts.root) {
12885
+ const root = getRoot(opts.root, db);
12886
+ if (!root)
12887
+ throw new Error(`Index root not found: ${opts.root}`);
12888
+ clauses.push("r.id = ?");
12889
+ params.push(root.id);
12890
+ }
12891
+ if (opts.ext) {
12892
+ clauses.push("f.ext = ?");
12893
+ params.push(opts.ext.replace(/^\./, "").toLowerCase());
12894
+ }
12895
+ if (opts.dir) {
12896
+ clauses.push("f.dir LIKE ? ESCAPE '\\'");
12897
+ const dir = opts.dir.replace(/^\/|\/$/g, "").replace(/[\\%_]/g, "\\$&");
12898
+ params.push(`%${dir}%`);
12899
+ }
12900
+ return { sql: clauses.length > 0 ? ` AND ${clauses.join(" AND ")}` : "", params };
12901
+ }
12902
+ function rowToHit(row, score) {
12903
+ return {
12904
+ rootId: row.root_id,
12905
+ rootName: row.root_name,
12906
+ rootPath: row.root_path,
12907
+ relPath: row.rel_path,
12908
+ absPath: `${row.root_path}/${row.rel_path}`,
12909
+ name: row.name,
12910
+ ext: row.ext,
12911
+ dir: row.dir,
12912
+ size: row.size,
12913
+ mtimeMs: row.mtime_ms,
12914
+ isBinary: row.is_binary === 1,
12915
+ score
12916
+ };
12917
+ }
12918
+ function normalizeForMatch(s) {
12919
+ return s.toLowerCase().replace(/[-_.]+/g, " ").trim();
12920
+ }
12921
+ var EXACT_NAME_FLOOR = 0.72;
12922
+ var PREFIX_NAME_FLOOR = 0.58;
12923
+ var CONTENT_MAX_SCORE = 0.65;
12924
+ function scoreFileName(query, tokens, row) {
12925
+ const q = query.trim().toLowerCase();
12926
+ const qNorm = normalizeForMatch(query);
12927
+ const name = row.name.toLowerCase();
12928
+ const stem = name.replace(/\.[^.]+$/, "");
12929
+ const nameNorm = normalizeForMatch(row.name);
12930
+ const stemNorm = normalizeForMatch(row.name.replace(/\.[^.]+$/, ""));
12931
+ const relPath = row.rel_path.toLowerCase();
12932
+ let score = 0;
12933
+ let floor = 0;
12934
+ if (name === q || stem === q || nameNorm === qNorm || stemNorm === qNorm) {
12935
+ score += 100;
12936
+ floor = EXACT_NAME_FLOOR;
12937
+ } else if (name.startsWith(q) || stem.startsWith(q) || stemNorm.startsWith(qNorm)) {
12938
+ score += 60;
12939
+ floor = PREFIX_NAME_FLOOR;
12940
+ } else if (name.includes(q) || nameNorm.includes(qNorm)) {
12941
+ score += 40;
12942
+ }
12943
+ for (const token of tokens) {
12944
+ const t = token.toLowerCase();
12945
+ if (name.includes(t))
12946
+ score += 15;
12947
+ else if (relPath.includes(t))
12948
+ score += 5;
12949
+ }
12950
+ const depth = row.rel_path.split("/").length - 1;
12951
+ score -= depth * 2;
12952
+ const age = Date.now() - row.mtime_ms;
12953
+ if (age < 7 * 86400000)
12954
+ score += 10;
12955
+ else if (age < 30 * 86400000)
12956
+ score += 5;
12957
+ return Math.max(floor, Math.max(0, score) / (Math.max(0, score) + 60));
12958
+ }
12959
+ var CANDIDATE_COLUMNS = `
12960
+ f.id, f.root_id, r.name as root_name, r.path as root_path,
12961
+ f.rel_path, f.name, f.ext, f.dir, f.size, f.mtime_ms, f.is_binary
12962
+ `;
12963
+ function searchFilePaths(query, opts = {}, db) {
12964
+ const d = db ?? getIndexDb();
12965
+ const limit = clampLimit(opts.limit);
12966
+ const tokens = tokenize(query);
12967
+ if (tokens.length === 0)
12968
+ return [];
12969
+ const ftsQuery = buildFtsQuery(query);
12970
+ const filters = filterClauses(opts, d);
12971
+ const candidateLimit = Math.max(200, limit * 10);
12972
+ let rows;
12973
+ if (ftsQuery) {
12974
+ rows = d.prepare(`SELECT ${CANDIDATE_COLUMNS}
12975
+ FROM files_fts fts
12976
+ JOIN files f ON f.id = fts.rowid
12977
+ JOIN index_roots r ON r.id = f.root_id
12978
+ WHERE files_fts MATCH ?${filters.sql}
12979
+ ORDER BY bm25(files_fts, 10.0, 1.0)
12980
+ LIMIT ?`).all(ftsQuery, ...filters.params, candidateLimit);
12981
+ const namePattern = `${query.trim().replace(/[\\%_]/g, "\\$&")}%`;
12982
+ const nameRows = d.prepare(`SELECT ${CANDIDATE_COLUMNS}
12983
+ FROM files f
12984
+ JOIN index_roots r ON r.id = f.root_id
12985
+ WHERE f.name LIKE ? ESCAPE '\\'${filters.sql}
12986
+ ORDER BY length(f.name)
12987
+ LIMIT 100`).all(namePattern, ...filters.params);
12988
+ const seen = new Set(rows.map((row) => row.id));
12989
+ for (const row of nameRows) {
12990
+ if (!seen.has(row.id))
12991
+ rows.push(row);
12992
+ }
12993
+ } else {
12994
+ const likeClauses = tokens.map(() => "f.rel_path LIKE ? ESCAPE '\\'").join(" AND ");
12995
+ const likeParams = tokens.map((t) => `%${t.replace(/[\\%_]/g, "\\$&")}%`);
12996
+ rows = d.prepare(`SELECT ${CANDIDATE_COLUMNS}
12997
+ FROM files f
12998
+ JOIN index_roots r ON r.id = f.root_id
12999
+ WHERE ${likeClauses}${filters.sql}
13000
+ LIMIT ?`).all(...likeParams, ...filters.params, candidateLimit);
13001
+ }
13002
+ const shortTokens = tokens.filter((t) => t.length < 3).map((t) => t.toLowerCase());
13003
+ const filtered = shortTokens.length > 0 ? rows.filter((row) => shortTokens.every((t) => row.rel_path.toLowerCase().includes(t))) : rows;
13004
+ return filtered.map((row) => rowToHit(row, scoreFileName(query, tokens, row))).sort((a, b) => b.score - a.score).filter((hit) => existsSync3(hit.absPath)).slice(0, limit);
13005
+ }
13006
+ function findLineMatches(content, query, tokens) {
13007
+ const lines = content.split(`
13008
+ `);
13009
+ const phrase = query.trim().toLowerCase();
13010
+ const lowered = tokens.map((t) => t.toLowerCase());
13011
+ const phraseHits = [];
13012
+ const allTokenHits = [];
13013
+ const anyTokenHits = [];
13014
+ for (let i = 0;i < lines.length; i++) {
13015
+ const text = lines[i];
13016
+ const lower = text.toLowerCase();
13017
+ const match = { line: i + 1, text: text.trim().slice(0, MAX_LINE_LENGTH) };
13018
+ if (phrase.length > 0 && lower.includes(phrase))
13019
+ phraseHits.push(match);
13020
+ else if (lowered.every((t) => lower.includes(t)))
13021
+ allTokenHits.push(match);
13022
+ else if (lowered.some((t) => t.length >= 3 && lower.includes(t)))
13023
+ anyTokenHits.push(match);
13024
+ if (phraseHits.length >= MAX_MATCHES_PER_FILE)
13025
+ break;
13026
+ }
13027
+ const tier = phraseHits.length > 0 ? "phrase" : allTokenHits.length > 0 ? "all" : "any";
13028
+ const combined = [...phraseHits, ...allTokenHits, ...anyTokenHits];
13029
+ return { matches: combined.slice(0, MAX_MATCHES_PER_FILE), tier };
13030
+ }
13031
+ function searchFilePathsRegex(pattern, opts = {}, db) {
13032
+ const d = db ?? getIndexDb();
13033
+ const limit = clampLimit(opts.limit);
13034
+ const regex = compileSearchRegex(pattern, opts.caseSensitive);
13035
+ const ftsQuery = buildFtsQueryFromRegex(pattern);
13036
+ if (!ftsQuery) {
13037
+ throw new Error("Regex pattern needs at least one required literal of 3+ characters (e.g. 'handle.*Click', not '\\w+').");
13038
+ }
13039
+ const filters = filterClauses(opts, d);
13040
+ const rows = d.prepare(`SELECT ${CANDIDATE_COLUMNS}
13041
+ FROM files_fts fts
13042
+ JOIN files f ON f.id = fts.rowid
13043
+ JOIN index_roots r ON r.id = f.root_id
13044
+ WHERE files_fts MATCH ?${filters.sql}
13045
+ ORDER BY fts.rank
13046
+ LIMIT 5000`).all(ftsQuery, ...filters.params);
13047
+ const hits = [];
13048
+ for (const row of rows) {
13049
+ if (!regex.test(row.rel_path) && !regex.test(row.name))
13050
+ continue;
13051
+ const depth = row.rel_path.split("/").length - 1;
13052
+ const score = Math.max(0.05, 0.6 - depth * 0.02);
13053
+ const hit = rowToHit(row, score);
13054
+ if (!existsSync3(hit.absPath))
13055
+ continue;
13056
+ hits.push(hit);
13057
+ if (hits.length >= limit)
13058
+ break;
13059
+ }
13060
+ return hits;
13061
+ }
13062
+ function searchFileContentRegex(pattern, opts = {}, db) {
13063
+ const d = db ?? getIndexDb();
13064
+ const limit = clampLimit(opts.limit);
13065
+ const regex = compileSearchRegex(pattern, opts.caseSensitive);
13066
+ const ftsQuery = buildFtsQueryFromRegex(pattern);
13067
+ if (!ftsQuery) {
13068
+ throw new Error("Regex pattern needs at least one required literal of 3+ characters (e.g. 'export.*function', not '\\d+').");
13069
+ }
13070
+ const filters = filterClauses(opts, d);
13071
+ const rows = d.prepare(`SELECT ${CANDIDATE_COLUMNS}
13072
+ FROM file_content_fts fts
13073
+ JOIN files f ON f.id = fts.rowid
13074
+ JOIN index_roots r ON r.id = f.root_id
13075
+ WHERE file_content_fts MATCH ?${filters.sql}
13076
+ ORDER BY fts.rank
13077
+ LIMIT ?`).all(ftsQuery, ...filters.params, Math.max(200, limit * 10));
13078
+ const hits = [];
13079
+ for (let i = 0;i < rows.length && hits.length < limit; i++) {
13080
+ const row = rows[i];
13081
+ const absPath = `${row.root_path}/${row.rel_path}`;
13082
+ let content;
13083
+ try {
13084
+ content = readFileSync5(absPath, "utf-8");
13085
+ } catch {
13086
+ continue;
13087
+ }
13088
+ const lines = content.split(`
13089
+ `);
13090
+ const matches = [];
13091
+ for (let n = 0;n < lines.length && matches.length < MAX_MATCHES_PER_FILE; n++) {
13092
+ if (regex.test(lines[n])) {
13093
+ matches.push({ line: n + 1, text: lines[n].trim().slice(0, MAX_LINE_LENGTH) });
13094
+ }
13095
+ }
13096
+ if (matches.length === 0)
13097
+ continue;
13098
+ const score = Math.max(0.25, 0.65 - i * 0.05);
13099
+ hits.push({
13100
+ ...rowToHit(row, score),
13101
+ line: matches[0].line,
13102
+ lineText: matches[0].text,
13103
+ matches
13104
+ });
13105
+ }
13106
+ return hits;
13107
+ }
13108
+ function searchFileContent(query, opts = {}, db) {
13109
+ const d = db ?? getIndexDb();
13110
+ const limit = clampLimit(opts.limit);
13111
+ const ftsQuery = buildFtsQuery(query);
13112
+ if (!ftsQuery)
13113
+ return [];
13114
+ const filters = filterClauses(opts, d);
13115
+ const rows = d.prepare(`SELECT ${CANDIDATE_COLUMNS}
13116
+ FROM file_content_fts fts
13117
+ JOIN files f ON f.id = fts.rowid
13118
+ JOIN index_roots r ON r.id = f.root_id
13119
+ WHERE file_content_fts MATCH ?${filters.sql}
13120
+ ORDER BY fts.rank
13121
+ LIMIT ?`).all(ftsQuery, ...filters.params, Math.max(50, limit * 3));
13122
+ const tokens = tokenize(query);
13123
+ const shortTokens = tokens.filter((t) => t.length < 3).map((t) => t.toLowerCase());
13124
+ const scored = [];
13125
+ for (let i = 0;i < rows.length && scored.length < limit * 2; i++) {
13126
+ const row = rows[i];
13127
+ const absPath = `${row.root_path}/${row.rel_path}`;
13128
+ let content;
13129
+ try {
13130
+ content = readFileSync5(absPath, "utf-8");
13131
+ } catch {
13132
+ continue;
13133
+ }
13134
+ if (shortTokens.length > 0) {
13135
+ const lower = content.toLowerCase();
13136
+ if (!shortTokens.every((t) => lower.includes(t)))
13137
+ continue;
13138
+ }
13139
+ const { matches, tier } = findLineMatches(content, query, tokens);
13140
+ if (matches.length === 0)
13141
+ continue;
13142
+ const base = Math.max(0.25, 0.55 - i * 0.04);
13143
+ const tierBoost = tier === "phrase" ? 0.1 : tier === "all" ? 0.05 : 0;
13144
+ const score = Math.min(CONTENT_MAX_SCORE, base + tierBoost);
13145
+ scored.push({
13146
+ ...rowToHit(row, score),
13147
+ line: matches[0].line,
13148
+ lineText: matches[0].text,
13149
+ matches
13150
+ });
13151
+ }
13152
+ return scored.sort((a, b) => b.score - a.score).slice(0, limit);
13153
+ }
13154
+
13155
+ // src/lib/local/find.ts
13156
+ function findLocal(query, opts = {}, db) {
13157
+ const kind = opts.kind ?? "both";
13158
+ if (kind !== "file" && kind !== "content" && kind !== "both") {
13159
+ throw new Error(`Invalid kind "${kind}" \u2014 use file, content, or both.`);
13160
+ }
13161
+ const limit = clampLimit(opts.limit);
13162
+ const roots = listRoots(db);
13163
+ if (!hasReadyRoot(db)) {
13164
+ return { query, kind, indexed: false, roots: roots.length, total: 0, results: [] };
13165
+ }
13166
+ if (opts.refresh !== false)
13167
+ autoRefreshStaleRoots(db);
13168
+ const queryOpts = {
13169
+ root: opts.root,
13170
+ ext: opts.ext,
13171
+ dir: opts.dir,
13172
+ limit
13173
+ };
13174
+ const merged = new Map;
13175
+ const pathSearch = opts.regex ? () => searchFilePathsRegex(query, { ...queryOpts, caseSensitive: opts.caseSensitive }, db) : () => searchFilePaths(query, queryOpts, db);
13176
+ const contentSearch = opts.regex ? () => searchFileContentRegex(query, { ...queryOpts, caseSensitive: opts.caseSensitive }, db) : () => searchFileContent(query, queryOpts, db);
13177
+ if (kind === "file" || kind === "both") {
13178
+ for (const hit of pathSearch()) {
13179
+ merged.set(hit.absPath, {
13180
+ path: hit.absPath,
13181
+ root: hit.rootName,
13182
+ kind: "file",
13183
+ score: hit.score,
13184
+ snippet: hit.relPath
13185
+ });
13186
+ }
13187
+ }
13188
+ if (kind === "content" || kind === "both") {
13189
+ for (const hit of contentSearch()) {
13190
+ const existing = merged.get(hit.absPath);
13191
+ if (existing) {
13192
+ existing.kind = "both";
13193
+ existing.score = Math.min(1, Math.max(existing.score, hit.score) + 0.1);
13194
+ existing.line = hit.line;
13195
+ existing.snippet = hit.lineText;
13196
+ existing.matches = hit.matches;
13197
+ } else {
13198
+ merged.set(hit.absPath, {
13199
+ path: hit.absPath,
13200
+ root: hit.rootName,
13201
+ kind: "content",
13202
+ score: hit.score,
13203
+ line: hit.line,
13204
+ snippet: hit.lineText,
13205
+ matches: hit.matches
13206
+ });
13207
+ }
13208
+ }
13209
+ }
13210
+ const results = [...merged.values()].sort((a, b) => b.score - a.score).slice(0, limit);
13211
+ return {
13212
+ query,
13213
+ kind,
13214
+ indexed: true,
13215
+ roots: roots.length,
13216
+ total: results.length,
13217
+ results
13218
+ };
13219
+ }
13220
+
13221
+ // src/cli/local.ts
13222
+ function printJson2(value) {
13223
+ console.log(JSON.stringify(value, null, 2));
13224
+ }
13225
+ function parsePositiveInt(value, label) {
13226
+ const n = parseInt(value, 10);
13227
+ if (!Number.isFinite(n) || n < 1) {
13228
+ console.error(chalk2.red(`Invalid ${label}: ${value} (expected a positive number)`));
13229
+ process.exit(1);
13230
+ }
13231
+ return n;
13232
+ }
13233
+ function printStats(stats, rootLabel) {
13234
+ console.log(chalk2.green(`\u2713 ${rootLabel}`) + chalk2.dim(` ${stats.fileCount} files (+${stats.added} ~${stats.updated} -${stats.deleted}, ${stats.contentIndexed} content) in ${stats.durationMs}ms`));
13235
+ }
13236
+ function registerLocalCommands(program2) {
13237
+ program2.command("find").description("Find files locally by name, path, or content across indexed roots").argument("<query...>", "What to look for").option("-k, --kind <kind>", "Match kind: file, content, both", "both").option("-r, --root <root>", "Limit to one index root (name, path, or id)").option("-e, --ext <ext>", "Filter by file extension").option("-d, --dir <dir>", "Filter by directory substring").option("-l, --limit <n>", "Max results", "20").option("-x, --regex", "Treat the query as a regular expression (grep-style)").option("--case-sensitive", "Case-sensitive matching (regex mode)").option("--no-refresh", "Skip stale-index refresh before searching").option("--json", "Output as JSON").action((queryParts, opts) => {
13238
+ const query = queryParts.join(" ");
13239
+ let response;
13240
+ try {
13241
+ response = findLocal(query, {
13242
+ kind: opts.kind,
13243
+ root: opts.root,
13244
+ ext: opts.ext,
13245
+ dir: opts.dir,
13246
+ limit: parsePositiveInt(opts.limit, "--limit"),
13247
+ refresh: opts.refresh,
13248
+ regex: opts.regex,
13249
+ caseSensitive: opts.caseSensitive
13250
+ });
13251
+ } catch (err) {
13252
+ console.error(chalk2.red(`Error: ${err instanceof Error ? err.message : err}`));
13253
+ process.exitCode = 1;
13254
+ return;
13255
+ }
13256
+ if (opts.json) {
13257
+ printJson2(response);
13258
+ return;
13259
+ }
13260
+ if (!response.indexed) {
13261
+ console.log(chalk2.yellow("No index roots ready. Add one with: search index add <path>"));
13262
+ process.exitCode = 1;
13263
+ return;
13264
+ }
13265
+ if (response.results.length === 0) {
13266
+ console.log(chalk2.yellow("No matches"));
13267
+ return;
13268
+ }
13269
+ for (const r of response.results) {
13270
+ const loc = r.line ? `:${r.line}` : "";
13271
+ const badge = chalk2.bgCyan.black(` ${r.kind} `);
13272
+ console.log(`${badge} ${chalk2.bold(r.path)}${chalk2.dim(loc)}`);
13273
+ if (r.kind !== "file" && r.snippet)
13274
+ console.log(` ${r.snippet}`);
13275
+ for (const m of r.matches?.slice(1) ?? []) {
13276
+ console.log(chalk2.dim(` :${m.line} ${m.text}`));
13277
+ }
13278
+ }
13279
+ });
13280
+ const index = program2.command("index").description("Manage the local file index");
13281
+ index.command("add <path>").description("Register a directory and index it").option("-n, --name <name>", "Friendly root name (default: directory basename)").option("--no-content", "Index file paths only, skip content").option("--exclude <patterns>", "Comma-separated extra exclude patterns (gitignore syntax)").option("--max-file-size <bytes>", "Max file size for content indexing", "524288").option("--json", "Output as JSON").action((path, opts) => {
13282
+ try {
13283
+ const root = addRoot(path, {
13284
+ name: opts.name,
13285
+ contentIndexing: opts.content,
13286
+ exclude: opts.exclude ? opts.exclude.split(",").map((s) => s.trim()) : [],
13287
+ maxFileSize: parsePositiveInt(opts.maxFileSize, "--max-file-size")
13288
+ });
13289
+ if (!opts.json)
13290
+ console.log(chalk2.dim(`Indexing ${root.path} ...`));
13291
+ const stats = indexRoot(root.id);
13292
+ if (opts.json) {
13293
+ printJson2({ root: getRoot(root.id), stats });
13294
+ return;
13295
+ }
13296
+ printStats(stats, root.name);
13297
+ } catch (err) {
13298
+ console.error(chalk2.red(`Error: ${err instanceof Error ? err.message : err}`));
13299
+ process.exitCode = 1;
13300
+ }
13301
+ });
13302
+ index.command("update [path]").description("Incrementally re-index one root, or all roots").option("--force", "Re-read content for all files, not just changed ones").option("--json", "Output as JSON").action((path, opts) => {
13303
+ try {
13304
+ if (path) {
13305
+ const root = getRoot(path);
13306
+ if (!root) {
13307
+ console.error(chalk2.red(`Index root not found: ${path}`));
13308
+ process.exitCode = 1;
13309
+ return;
13310
+ }
13311
+ const stats = indexRoot(root.id, { force: opts.force });
13312
+ if (opts.json)
13313
+ printJson2(stats);
13314
+ else
13315
+ printStats(stats, root.name);
13316
+ return;
13317
+ }
13318
+ const all = indexAllRoots({ force: opts.force });
13319
+ if (opts.json) {
13320
+ printJson2(all);
13321
+ return;
13322
+ }
13323
+ if (all.length === 0) {
13324
+ console.log(chalk2.yellow("No index roots. Add one with: search index add <path>"));
13325
+ return;
13326
+ }
13327
+ const roots = listRoots();
13328
+ for (const stats of all) {
13329
+ printStats(stats, roots.find((r) => r.id === stats.rootId)?.name ?? stats.rootId);
13330
+ }
13331
+ } catch (err) {
13332
+ console.error(chalk2.red(`Error: ${err instanceof Error ? err.message : err}`));
13333
+ process.exitCode = 1;
13334
+ }
13335
+ });
13336
+ index.command("list").alias("ls").description("List index roots").option("--json", "Output as JSON").action((opts) => {
13337
+ const roots = listRoots();
13338
+ if (opts.json) {
13339
+ printJson2(roots);
13340
+ return;
13341
+ }
13342
+ if (roots.length === 0) {
13343
+ console.log(chalk2.yellow("No index roots. Add one with: search index add <path>"));
13344
+ return;
13345
+ }
13346
+ for (const r of roots) {
13347
+ const status = r.status === "ready" ? chalk2.green(r.status) : r.status === "error" ? chalk2.red(r.status) : chalk2.yellow(r.status);
13348
+ console.log(`${chalk2.dim(r.id.substring(0, 8))} ${chalk2.yellow(r.name.padEnd(20))} ${status.padEnd(8)} ${String(r.fileCount).padStart(7)} files ${chalk2.dim(r.path)}`);
13349
+ if (r.error)
13350
+ console.log(chalk2.red(` ${r.error}`));
13351
+ }
13352
+ });
13353
+ index.command("status").description("Show index status and staleness").option("--json", "Output as JSON").action((opts) => {
13354
+ const roots = listRoots();
13355
+ const status = roots.map((r) => ({
13356
+ ...r,
13357
+ staleMinutes: r.lastIndexedAt ? Math.round((Date.now() - Date.parse(r.lastIndexedAt)) / 60000) : null
13358
+ }));
13359
+ if (opts.json) {
13360
+ printJson2(status);
13361
+ return;
13362
+ }
13363
+ if (status.length === 0) {
13364
+ console.log(chalk2.yellow("No index roots. Add one with: search index add <path>"));
13365
+ return;
13366
+ }
13367
+ for (const r of status) {
13368
+ const age = r.staleMinutes === null ? "never indexed" : `indexed ${r.staleMinutes}m ago`;
13369
+ console.log(`${chalk2.yellow(r.name.padEnd(20))} ${r.status.padEnd(8)} ${String(r.fileCount).padStart(7)} files ${age} ${chalk2.dim(`(${r.lastDurationMs ?? "?"}ms)`)}`);
13370
+ }
13371
+ });
13372
+ index.command("rm <idOrPath>").alias("remove").description("Remove a root and all its indexed data").action((idOrPath) => {
13373
+ if (removeRoot(idOrPath)) {
13374
+ console.log(chalk2.green("Index root removed"));
13375
+ } else {
13376
+ console.error(chalk2.red(`Index root not found: ${idOrPath}`));
13377
+ process.exitCode = 1;
13378
+ }
13379
+ });
13380
+ }
13381
+
11857
13382
  // src/lib/providers/google.ts
11858
13383
  class GoogleProvider {
11859
13384
  name = "google";
@@ -12496,6 +14021,62 @@ class ArxivProvider {
12496
14021
  }
12497
14022
  }
12498
14023
 
14024
+ // src/lib/providers/files.ts
14025
+ class FilesProvider {
14026
+ name = "files";
14027
+ displayName = "Local Files";
14028
+ isConfigured() {
14029
+ return hasReadyRoot();
14030
+ }
14031
+ async search(query, options) {
14032
+ autoRefreshStaleRoots();
14033
+ const hits = searchFilePaths(query, { limit: options?.limit ?? 10 });
14034
+ return hits.map((hit) => ({
14035
+ title: hit.name,
14036
+ url: `file://${hit.absPath}`,
14037
+ snippet: hit.absPath,
14038
+ score: hit.score,
14039
+ publishedAt: new Date(hit.mtimeMs).toISOString(),
14040
+ metadata: {
14041
+ root: hit.rootName,
14042
+ relPath: hit.relPath,
14043
+ dir: hit.dir,
14044
+ ext: hit.ext,
14045
+ size: hit.size,
14046
+ isBinary: hit.isBinary
14047
+ }
14048
+ }));
14049
+ }
14050
+ }
14051
+
14052
+ // src/lib/providers/content.ts
14053
+ class ContentProvider {
14054
+ name = "content";
14055
+ displayName = "Local Content";
14056
+ isConfigured() {
14057
+ return hasReadyRoot();
14058
+ }
14059
+ async search(query, options) {
14060
+ autoRefreshStaleRoots();
14061
+ const hits = searchFileContent(query, { limit: options?.limit ?? 10 });
14062
+ return hits.map((hit) => ({
14063
+ title: hit.name,
14064
+ url: `file://${hit.absPath}`,
14065
+ snippet: `${hit.relPath}:${hit.line}: ${hit.lineText}`,
14066
+ score: hit.score,
14067
+ publishedAt: new Date(hit.mtimeMs).toISOString(),
14068
+ metadata: {
14069
+ root: hit.rootName,
14070
+ relPath: hit.relPath,
14071
+ dir: hit.dir,
14072
+ ext: hit.ext,
14073
+ line: hit.line,
14074
+ matches: hit.matches
14075
+ }
14076
+ }));
14077
+ }
14078
+ }
14079
+
12499
14080
  // src/lib/providers/index.ts
12500
14081
  var providerFactories = {
12501
14082
  google: () => new GoogleProvider,
@@ -12509,7 +14090,9 @@ var providerFactories = {
12509
14090
  youtube: () => new YouTubeProvider,
12510
14091
  hackernews: () => new HackerNewsProvider,
12511
14092
  github: () => new GitHubProvider,
12512
- arxiv: () => new ArxivProvider
14093
+ arxiv: () => new ArxivProvider,
14094
+ files: () => new FilesProvider,
14095
+ content: () => new ContentProvider
12513
14096
  };
12514
14097
  var instanceCache = new Map;
12515
14098
  function getProvider(name) {
@@ -12528,6 +14111,8 @@ function getProvider(name) {
12528
14111
  function normalizeUrl(url) {
12529
14112
  try {
12530
14113
  const u = new URL(url);
14114
+ if (u.protocol === "file:")
14115
+ return url.replace(/\/+$/, "");
12531
14116
  u.hostname = u.hostname.toLowerCase();
12532
14117
  u.pathname = u.pathname.replace(/\/+$/, "") || "/";
12533
14118
  const params = new URLSearchParams(u.search);
@@ -12584,55 +14169,6 @@ function deduplicateResults(results) {
12584
14169
  return deduped;
12585
14170
  }
12586
14171
 
12587
- // src/lib/config.ts
12588
- import { mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync, existsSync as existsSync3, cpSync as cpSync2 } from "fs";
12589
- function getConfigDir() {
12590
- const home = Bun.env.HOME ?? "/tmp";
12591
- const newDir = `${home}/.hasna/search`;
12592
- const oldDir = `${home}/.open-search`;
12593
- if (!existsSync3(newDir) && existsSync3(oldDir)) {
12594
- try {
12595
- mkdirSync2(`${home}/.hasna`, { recursive: true });
12596
- cpSync2(oldDir, newDir, { recursive: true });
12597
- } catch {}
12598
- }
12599
- mkdirSync2(newDir, { recursive: true });
12600
- return newDir;
12601
- }
12602
- function getConfigPath() {
12603
- return `${getConfigDir()}/config.json`;
12604
- }
12605
- function getConfig() {
12606
- const path = getConfigPath();
12607
- if (!existsSync3(path)) {
12608
- return { ...DEFAULT_CONFIG };
12609
- }
12610
- try {
12611
- const raw = readFileSync2(path, "utf-8");
12612
- const parsed = JSON.parse(raw);
12613
- return { ...DEFAULT_CONFIG, ...parsed };
12614
- } catch {
12615
- return { ...DEFAULT_CONFIG };
12616
- }
12617
- }
12618
- function setConfig(updates) {
12619
- const current = getConfig();
12620
- const merged = { ...current, ...updates };
12621
- const path = getConfigPath();
12622
- writeFileSync(path, JSON.stringify(merged, null, 2), "utf-8");
12623
- return merged;
12624
- }
12625
- function resetConfig() {
12626
- const path = getConfigPath();
12627
- writeFileSync(path, JSON.stringify(DEFAULT_CONFIG, null, 2), "utf-8");
12628
- return { ...DEFAULT_CONFIG };
12629
- }
12630
- function setConfigValue(key, value) {
12631
- const config = getConfig();
12632
- config[key] = value;
12633
- return setConfig(config);
12634
- }
12635
-
12636
14172
  // src/db/searches.ts
12637
14173
  init_database();
12638
14174
  function rowToSearch(row) {
@@ -12865,6 +14401,8 @@ function updateProviderLastUsed(name, db) {
12865
14401
  d.prepare("UPDATE providers SET last_used_at = ? WHERE name = ?").run(now, name);
12866
14402
  }
12867
14403
  function isProviderConfigured(provider) {
14404
+ if (LOCAL_PROVIDER_NAMES.has(provider.name))
14405
+ return hasReadyRoot();
12868
14406
  if (!provider.apiKeyEnv)
12869
14407
  return true;
12870
14408
  return !!Bun.env[provider.apiKeyEnv];
@@ -12890,10 +14428,19 @@ async function unifiedSearch(query, opts = {}) {
12890
14428
  providerNames = dbProviders.filter((p) => p.enabled).map((p) => p.name);
12891
14429
  }
12892
14430
  }
14431
+ const errors2 = [];
14432
+ const explicitRequest = (opts.providers?.length ?? 0) > 0 || Boolean(opts.profile);
12893
14433
  const activeProviders = providerNames.filter((name) => {
12894
14434
  try {
12895
- const provider = getProvider(name);
12896
- return provider.isConfigured();
14435
+ if (getProvider(name).isConfigured())
14436
+ return true;
14437
+ if (explicitRequest) {
14438
+ errors2.push({
14439
+ provider: name,
14440
+ error: LOCAL_PROVIDER_NAMES.has(name) ? "no index roots ready \u2014 run `search index add <path>` first" : "not configured (missing API key)"
14441
+ });
14442
+ }
14443
+ return false;
12897
14444
  } catch {
12898
14445
  return false;
12899
14446
  }
@@ -12909,7 +14456,6 @@ async function unifiedSearch(query, opts = {}) {
12909
14456
  return { name, results: rawResults };
12910
14457
  }));
12911
14458
  const allResults = [];
12912
- const errors2 = [];
12913
14459
  const searchId = generateId();
12914
14460
  for (const result of results) {
12915
14461
  if (result.status === "fulfilled") {
@@ -12950,19 +14496,31 @@ async function unifiedSearch(query, opts = {}) {
12950
14496
  });
12951
14497
  }
12952
14498
  const duration = Date.now() - startTime;
14499
+ if (activeProviders.length === 0) {
14500
+ return {
14501
+ search: {
14502
+ id: searchId,
14503
+ query,
14504
+ providers: [],
14505
+ profileId: null,
14506
+ resultCount: 0,
14507
+ duration,
14508
+ createdAt: new Date().toISOString()
14509
+ },
14510
+ results: finalResults,
14511
+ errors: errors2
14512
+ };
14513
+ }
14514
+ const persistable = config.recordLocalResults ? finalResults : finalResults.filter((r) => !LOCAL_PROVIDER_NAMES.has(r.source));
12953
14515
  const search = createSearch({
12954
14516
  query,
12955
14517
  providers: activeProviders,
12956
- resultCount: finalResults.length,
14518
+ resultCount: persistable.length,
12957
14519
  duration
12958
14520
  }, db);
12959
- if (finalResults.length > 0) {
12960
- const resultsToStore = finalResults.map((r) => ({
12961
- ...r,
12962
- searchId: search.id
12963
- }));
12964
- createResults(resultsToStore.map((r) => ({
12965
- searchId: r.searchId,
14521
+ if (persistable.length > 0) {
14522
+ createResults(persistable.map((r) => ({
14523
+ searchId: search.id,
12966
14524
  title: r.title,
12967
14525
  url: r.url,
12968
14526
  snippet: r.snippet,
@@ -12975,7 +14533,7 @@ async function unifiedSearch(query, opts = {}) {
12975
14533
  metadata: r.metadata
12976
14534
  })), db);
12977
14535
  }
12978
- updateSearchResults(search.id, finalResults.length, duration, db);
14536
+ updateSearchResults(search.id, persistable.length, duration, db);
12979
14537
  return {
12980
14538
  search: { ...search, resultCount: finalResults.length, duration },
12981
14539
  results: finalResults,
@@ -13251,9 +14809,11 @@ function updateSavedSearchLastRun(id, db) {
13251
14809
  }
13252
14810
 
13253
14811
  // src/cli/index.tsx
14812
+ var pkg = require_package();
13254
14813
  var program2 = new Command;
13255
- program2.name("search").version("0.0.1").description("Unified search aggregator \u2014 12 providers, one interface");
13256
- registerCloudCommands(program2);
14814
+ program2.name("search").version(pkg.version).description("Unified search \u2014 local file index + 12 web providers, one interface");
14815
+ registerStorageCommands(program2);
14816
+ registerLocalCommands(program2);
13257
14817
  program2.command("query").alias("q").argument("<query...>", "Search query").option("-p, --providers <providers>", "Comma-separated providers").option("--profile <name>", "Use a search profile").option("-l, --limit <n>", "Max results per provider", "10").option("-f, --format <format>", "Output format: table, json", "table").option("--no-dedup", "Disable deduplication").action(async (queryParts, opts) => {
13258
14818
  const query = queryParts.join(" ");
13259
14819
  const providers = opts.providers ? opts.providers.split(",") : undefined;
@@ -13270,7 +14830,7 @@ program2.command("query").alias("q").argument("<query...>", "Search query").opti
13270
14830
  }
13271
14831
  printResults2(response.results, response.search.duration, response.errors);
13272
14832
  } catch (err) {
13273
- console.error(chalk2.red(`Error: ${err instanceof Error ? err.message : err}`));
14833
+ console.error(chalk3.red(`Error: ${err instanceof Error ? err.message : err}`));
13274
14834
  process.exit(1);
13275
14835
  }
13276
14836
  });
@@ -13285,11 +14845,11 @@ for (const providerName of PROVIDER_NAMES) {
13285
14845
  });
13286
14846
  printResults2(deep.videoResults, 0, []);
13287
14847
  if (deep.transcriptMatches.length > 0) {
13288
- console.log(chalk2.cyan(`
14848
+ console.log(chalk3.cyan(`
13289
14849
  --- Transcript Matches ---`));
13290
14850
  for (const m of deep.transcriptMatches) {
13291
- console.log(chalk2.yellow(m.videoTitle));
13292
- console.log(chalk2.dim(m.snippet));
14851
+ console.log(chalk3.yellow(m.videoTitle));
14852
+ console.log(chalk3.dim(m.snippet));
13293
14853
  console.log();
13294
14854
  }
13295
14855
  }
@@ -13302,7 +14862,7 @@ for (const providerName of PROVIDER_NAMES) {
13302
14862
  }
13303
14863
  printResults2(response.results, response.search.duration, response.errors);
13304
14864
  } catch (err) {
13305
- console.error(chalk2.red(`Error: ${err instanceof Error ? err.message : err}`));
14865
+ console.error(chalk3.red(`Error: ${err instanceof Error ? err.message : err}`));
13306
14866
  process.exit(1);
13307
14867
  }
13308
14868
  });
@@ -13313,19 +14873,19 @@ history.command("list").alias("ls").option("-l, --limit <n>", "Max items", "20")
13313
14873
  limit: parseInt(opts.limit),
13314
14874
  query: opts.query
13315
14875
  });
13316
- console.log(chalk2.bold(`Search History (${total} total)`));
14876
+ console.log(chalk3.bold(`Search History (${total} total)`));
13317
14877
  console.log();
13318
14878
  for (const s of searches) {
13319
- console.log(`${chalk2.dim(s.id.substring(0, 8))} ${chalk2.white(s.query)} ${chalk2.cyan(s.providers.join(","))} ${chalk2.green(String(s.resultCount) + " results")} ${chalk2.dim(s.createdAt)}`);
14879
+ console.log(`${chalk3.dim(s.id.substring(0, 8))} ${chalk3.white(s.query)} ${chalk3.cyan(s.providers.join(","))} ${chalk3.green(String(s.resultCount) + " results")} ${chalk3.dim(s.createdAt)}`);
13320
14880
  }
13321
14881
  });
13322
14882
  history.command("show <id>").action((id) => {
13323
14883
  const search = getSearch(id);
13324
14884
  if (!search) {
13325
- console.error(chalk2.red(`Search not found: ${id}`));
14885
+ console.error(chalk3.red(`Search not found: ${id}`));
13326
14886
  return;
13327
14887
  }
13328
- console.log(chalk2.bold(`Query: ${search.query}`));
14888
+ console.log(chalk3.bold(`Query: ${search.query}`));
13329
14889
  console.log(`Providers: ${search.providers.join(", ")}`);
13330
14890
  console.log(`Results: ${search.resultCount} | Duration: ${search.duration}ms`);
13331
14891
  console.log();
@@ -13334,32 +14894,32 @@ history.command("show <id>").action((id) => {
13334
14894
  });
13335
14895
  history.command("delete <id>").action((id) => {
13336
14896
  if (deleteSearch(id)) {
13337
- console.log(chalk2.green("Search deleted"));
14897
+ console.log(chalk3.green("Search deleted"));
13338
14898
  } else {
13339
- console.error(chalk2.red(`Search not found: ${id}`));
14899
+ console.error(chalk3.red(`Search not found: ${id}`));
13340
14900
  }
13341
14901
  });
13342
14902
  var saved = program2.command("saved").description("Saved searches");
13343
14903
  saved.command("list").alias("ls").action(() => {
13344
14904
  const items = listSavedSearches();
13345
14905
  if (items.length === 0) {
13346
- console.log(chalk2.dim("No saved searches"));
14906
+ console.log(chalk3.dim("No saved searches"));
13347
14907
  return;
13348
14908
  }
13349
14909
  for (const s of items) {
13350
- console.log(`${chalk2.dim(s.id.substring(0, 8))} ${chalk2.yellow(s.name)} ${chalk2.white(s.query)} ${chalk2.cyan(s.providers.join(","))} ${chalk2.dim(s.lastRunAt ?? "never run")}`);
14910
+ console.log(`${chalk3.dim(s.id.substring(0, 8))} ${chalk3.yellow(s.name)} ${chalk3.white(s.query)} ${chalk3.cyan(s.providers.join(","))} ${chalk3.dim(s.lastRunAt ?? "never run")}`);
13351
14911
  }
13352
14912
  });
13353
14913
  saved.command("add <name> <query...>").option("-p, --providers <providers>", "Comma-separated providers").option("--profile <name>", "Search profile").action((name, queryParts, opts) => {
13354
14914
  const query = queryParts.join(" ");
13355
14915
  const providers = opts.providers ? opts.providers.split(",") : [];
13356
14916
  const s = createSavedSearch({ name, query, providers, profileId: opts.profile });
13357
- console.log(chalk2.green(`Saved search created: ${s.id}`));
14917
+ console.log(chalk3.green(`Saved search created: ${s.id}`));
13358
14918
  });
13359
14919
  saved.command("run <id>").action(async (id) => {
13360
14920
  const s = getSavedSearch(id);
13361
14921
  if (!s) {
13362
- console.error(chalk2.red(`Saved search not found: ${id}`));
14922
+ console.error(chalk3.red(`Saved search not found: ${id}`));
13363
14923
  return;
13364
14924
  }
13365
14925
  updateSavedSearchLastRun(id);
@@ -13371,35 +14931,35 @@ saved.command("run <id>").action(async (id) => {
13371
14931
  });
13372
14932
  saved.command("delete <id>").action((id) => {
13373
14933
  if (deleteSavedSearch(id)) {
13374
- console.log(chalk2.green("Saved search deleted"));
14934
+ console.log(chalk3.green("Saved search deleted"));
13375
14935
  } else {
13376
- console.error(chalk2.red(`Not found: ${id}`));
14936
+ console.error(chalk3.red(`Not found: ${id}`));
13377
14937
  }
13378
14938
  });
13379
14939
  var providers = program2.command("providers").description("Manage search providers");
13380
14940
  providers.command("list").alias("ls").action(() => {
13381
14941
  const all = listProviders();
13382
- console.log(chalk2.bold("Search Providers"));
14942
+ console.log(chalk3.bold("Search Providers"));
13383
14943
  console.log();
13384
14944
  for (const p of all) {
13385
14945
  const configured = isProviderConfigured(p);
13386
- const status = p.enabled ? configured ? chalk2.green("enabled") : chalk2.yellow("enabled (no key)") : chalk2.dim("disabled");
13387
- const keyInfo = p.apiKeyEnv ? chalk2.dim(` [${p.apiKeyEnv}]`) : chalk2.dim(" [no key needed]");
13388
- console.log(` ${chalk2.white(p.name.padEnd(14))} ${status}${keyInfo} rate: ${p.rateLimit}/min`);
14946
+ const status = p.enabled ? configured ? chalk3.green("enabled") : chalk3.yellow("enabled (no key)") : chalk3.dim("disabled");
14947
+ const keyInfo = p.apiKeyEnv ? chalk3.dim(` [${p.apiKeyEnv}]`) : chalk3.dim(" [no key needed]");
14948
+ console.log(` ${chalk3.white(p.name.padEnd(14))} ${status}${keyInfo} rate: ${p.rateLimit}/min`);
13389
14949
  }
13390
14950
  });
13391
14951
  providers.command("enable <name>").action((name) => {
13392
14952
  if (enableProvider(name)) {
13393
- console.log(chalk2.green(`Provider ${name} enabled`));
14953
+ console.log(chalk3.green(`Provider ${name} enabled`));
13394
14954
  } else {
13395
- console.error(chalk2.red(`Provider not found: ${name}`));
14955
+ console.error(chalk3.red(`Provider not found: ${name}`));
13396
14956
  }
13397
14957
  });
13398
14958
  providers.command("disable <name>").action((name) => {
13399
14959
  if (disableProvider(name)) {
13400
- console.log(chalk2.green(`Provider ${name} disabled`));
14960
+ console.log(chalk3.green(`Provider ${name} disabled`));
13401
14961
  } else {
13402
- console.error(chalk2.red(`Provider not found: ${name}`));
14962
+ console.error(chalk3.red(`Provider not found: ${name}`));
13403
14963
  }
13404
14964
  });
13405
14965
  providers.command("configure <name>").option("--key-env <env>", "API key env var name").option("--rate-limit <n>", "Requests per minute").action((name, opts) => {
@@ -13409,35 +14969,35 @@ providers.command("configure <name>").option("--key-env <env>", "API key env var
13409
14969
  if (opts.rateLimit)
13410
14970
  updates.rateLimit = parseInt(opts.rateLimit);
13411
14971
  if (updateProvider(name, updates)) {
13412
- console.log(chalk2.green(`Provider ${name} updated`));
14972
+ console.log(chalk3.green(`Provider ${name} updated`));
13413
14973
  } else {
13414
- console.error(chalk2.red(`Provider not found: ${name}`));
14974
+ console.error(chalk3.red(`Provider not found: ${name}`));
13415
14975
  }
13416
14976
  });
13417
14977
  var profiles = program2.command("profiles").description("Search profiles");
13418
14978
  profiles.command("list").alias("ls").action(() => {
13419
14979
  const all = listProfiles();
13420
14980
  for (const p of all) {
13421
- console.log(`${chalk2.dim(p.id.substring(0, 12))} ${chalk2.yellow(p.name.padEnd(12))} ${chalk2.white(p.providers.join(", "))} ${chalk2.dim(p.description ?? "")}`);
14981
+ console.log(`${chalk3.dim(p.id.substring(0, 12))} ${chalk3.yellow(p.name.padEnd(12))} ${chalk3.white(p.providers.join(", "))} ${chalk3.dim(p.description ?? "")}`);
13422
14982
  }
13423
14983
  });
13424
14984
  profiles.command("create <name>").option("-p, --providers <providers>", "Comma-separated providers").option("-d, --description <desc>", "Description").action((name, opts) => {
13425
14985
  const providerList = opts.providers ? opts.providers.split(",") : [];
13426
14986
  const p = createProfile({ name, providers: providerList, description: opts.description });
13427
- console.log(chalk2.green(`Profile created: ${p.id}`));
14987
+ console.log(chalk3.green(`Profile created: ${p.id}`));
13428
14988
  });
13429
14989
  profiles.command("delete <id>").action((id) => {
13430
14990
  if (deleteProfile(id)) {
13431
- console.log(chalk2.green("Profile deleted"));
14991
+ console.log(chalk3.green("Profile deleted"));
13432
14992
  } else {
13433
- console.error(chalk2.red(`Profile not found: ${id}`));
14993
+ console.error(chalk3.red(`Profile not found: ${id}`));
13434
14994
  }
13435
14995
  });
13436
14996
  profiles.command("use <name> <query...>").action(async (name, queryParts) => {
13437
14997
  const query = queryParts.join(" ");
13438
14998
  const profile = getProfileByName(name);
13439
14999
  if (!profile) {
13440
- console.error(chalk2.red(`Profile not found: ${name}`));
15000
+ console.error(chalk3.red(`Profile not found: ${name}`));
13441
15001
  return;
13442
15002
  }
13443
15003
  const response = await unifiedSearch(query, { profile: name });
@@ -13448,12 +15008,12 @@ program2.command("export <searchId>").option("-f, --format <format>", "Format: j
13448
15008
  const output = exportResults(searchId, opts.format);
13449
15009
  if (opts.output) {
13450
15010
  Bun.write(opts.output, output);
13451
- console.log(chalk2.green(`Exported to ${opts.output}`));
15011
+ console.log(chalk3.green(`Exported to ${opts.output}`));
13452
15012
  } else {
13453
15013
  console.log(output);
13454
15014
  }
13455
15015
  } catch (err) {
13456
- console.error(chalk2.red(`Error: ${err instanceof Error ? err.message : err}`));
15016
+ console.error(chalk3.red(`Error: ${err instanceof Error ? err.message : err}`));
13457
15017
  }
13458
15018
  });
13459
15019
  var config = program2.command("config").description("Configuration");
@@ -13473,20 +15033,20 @@ config.command("set <key> <value>").action((key, value) => {
13473
15033
  } catch {
13474
15034
  setConfigValue(key, value);
13475
15035
  }
13476
- console.log(chalk2.green(`Config ${key} updated`));
15036
+ console.log(chalk3.green(`Config ${key} updated`));
13477
15037
  });
13478
15038
  config.command("reset").action(() => {
13479
15039
  resetConfig();
13480
- console.log(chalk2.green("Config reset to defaults"));
15040
+ console.log(chalk3.green("Config reset to defaults"));
13481
15041
  });
13482
15042
  program2.command("stats").action(() => {
13483
15043
  const stats = getSearchStats();
13484
- console.log(chalk2.bold("Search Statistics"));
15044
+ console.log(chalk3.bold("Search Statistics"));
13485
15045
  console.log(` Total searches: ${stats.totalSearches}`);
13486
15046
  console.log(` Total results: ${stats.totalResults}`);
13487
15047
  console.log();
13488
15048
  if (Object.keys(stats.providerBreakdown).length > 0) {
13489
- console.log(chalk2.bold(" Results by Provider:"));
15049
+ console.log(chalk3.bold(" Results by Provider:"));
13490
15050
  for (const [provider, count] of Object.entries(stats.providerBreakdown)) {
13491
15051
  console.log(` ${provider.padEnd(14)} ${count}`);
13492
15052
  }
@@ -13494,27 +15054,30 @@ program2.command("stats").action(() => {
13494
15054
  });
13495
15055
  function printResults2(results, duration, errors2) {
13496
15056
  if (results.length === 0) {
13497
- console.log(chalk2.yellow("No results found"));
15057
+ console.log(chalk3.yellow("No results found"));
15058
+ for (const e of errors2) {
15059
+ console.error(` ${chalk3.red(e.provider)}: ${e.error}`);
15060
+ }
13498
15061
  return;
13499
15062
  }
13500
- console.log(chalk2.bold(`${results.length} results`) + chalk2.dim(` (${duration}ms)`));
15063
+ console.log(chalk3.bold(`${results.length} results`) + chalk3.dim(` (${duration}ms)`));
13501
15064
  console.log();
13502
15065
  for (const r of results) {
13503
- const badge = chalk2.bgCyan.black(` ${r.source} `);
13504
- console.log(`${chalk2.dim(String(r.rank).padStart(3))} ${badge} ${chalk2.bold.blue(r.title)}`);
13505
- console.log(` ${chalk2.dim(r.url)}`);
15066
+ const badge = chalk3.bgCyan.black(` ${r.source} `);
15067
+ console.log(`${chalk3.dim(String(r.rank).padStart(3))} ${badge} ${chalk3.bold.blue(r.title)}`);
15068
+ console.log(` ${chalk3.dim(r.url)}`);
13506
15069
  if (r.snippet) {
13507
15070
  console.log(` ${r.snippet.substring(0, 200)}`);
13508
15071
  }
13509
15072
  if (r.score !== null) {
13510
- console.log(` ${chalk2.dim(`score: ${r.score.toFixed(3)}`)}`);
15073
+ console.log(` ${chalk3.dim(`score: ${r.score.toFixed(3)}`)}`);
13511
15074
  }
13512
15075
  console.log();
13513
15076
  }
13514
15077
  if (errors2.length > 0) {
13515
- console.log(chalk2.yellow("Errors:"));
15078
+ console.log(chalk3.yellow("Errors:"));
13516
15079
  for (const e of errors2) {
13517
- console.log(` ${chalk2.red(e.provider)}: ${e.error}`);
15080
+ console.log(` ${chalk3.red(e.provider)}: ${e.error}`);
13518
15081
  }
13519
15082
  }
13520
15083
  }
@@ -13526,7 +15089,7 @@ program2.action(async (_, cmd) => {
13526
15089
  const response = await unifiedSearch(query);
13527
15090
  printResults2(response.results, response.search.duration, response.errors);
13528
15091
  } catch (err) {
13529
- console.error(chalk2.red(`Error: ${err instanceof Error ? err.message : err}`));
15092
+ console.error(chalk3.red(`Error: ${err instanceof Error ? err.message : err}`));
13530
15093
  process.exit(1);
13531
15094
  }
13532
15095
  } else {
@@ -13536,8 +15099,8 @@ program2.action(async (_, cmd) => {
13536
15099
  program2.command("feedback <message>").description("Send feedback about this service").option("-e, --email <email>", "Contact email").option("-c, --category <cat>", "Category: bug, feature, general", "general").action(async (message, opts) => {
13537
15100
  const { getDb: getDb2 } = await Promise.resolve().then(() => (init_database(), exports_database));
13538
15101
  const db = getDb2();
13539
- const pkg = require_package();
13540
- db.run("INSERT INTO feedback (message, email, category, version) VALUES (?, ?, ?, ?)", [message, opts.email || null, opts.category || "general", pkg.version]);
13541
- console.log(chalk2.green("\u2713") + " Feedback saved. Thank you!");
15102
+ const pkg2 = require_package();
15103
+ db.run("INSERT INTO feedback (message, email, category, version) VALUES (?, ?, ?, ?)", [message, opts.email || null, opts.category || "general", pkg2.version]);
15104
+ console.log(chalk3.green("\u2713") + " Feedback saved. Thank you!");
13542
15105
  });
13543
15106
  program2.parse();