@hasna/search 0.0.8 → 0.0.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +2 -1
- package/README.md +78 -9
- package/dist/cli/index.js +1761 -198
- package/dist/cli/local.d.ts +3 -0
- package/dist/cli/local.d.ts.map +1 -0
- package/dist/cli/storage.d.ts +3 -0
- package/dist/cli/storage.d.ts.map +1 -0
- package/dist/db/database.d.ts.map +1 -1
- package/dist/db/index-db.d.ts +6 -0
- package/dist/db/index-db.d.ts.map +1 -0
- package/dist/db/index-migrations.d.ts +3 -0
- package/dist/db/index-migrations.d.ts.map +1 -0
- package/dist/db/migrations.d.ts.map +1 -1
- package/dist/db/pg-migrations.d.ts +1 -1
- package/dist/db/providers.d.ts.map +1 -1
- package/dist/db/storage-config.d.ts +26 -0
- package/dist/db/storage-config.d.ts.map +1 -0
- package/dist/db/storage-sync.d.ts +35 -0
- package/dist/db/storage-sync.d.ts.map +1 -0
- package/dist/index.d.ts +9 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2459 -118
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/dedup.d.ts.map +1 -1
- package/dist/lib/local/find.d.ts +38 -0
- package/dist/lib/local/find.d.ts.map +1 -0
- package/dist/lib/local/ignore.d.ts +38 -0
- package/dist/lib/local/ignore.d.ts.map +1 -0
- package/dist/lib/local/indexer.d.ts +62 -0
- package/dist/lib/local/indexer.d.ts.map +1 -0
- package/dist/lib/local/query.d.ts +60 -0
- package/dist/lib/local/query.d.ts.map +1 -0
- package/dist/lib/local/regex.d.ts +26 -0
- package/dist/lib/local/regex.d.ts.map +1 -0
- package/dist/lib/local/walker.d.ts +30 -0
- package/dist/lib/local/walker.d.ts.map +1 -0
- package/dist/lib/providers/content.d.ts +9 -0
- package/dist/lib/providers/content.d.ts.map +1 -0
- package/dist/lib/providers/files.d.ts +9 -0
- package/dist/lib/providers/files.d.ts.map +1 -0
- package/dist/lib/providers/index.d.ts.map +1 -1
- package/dist/lib/search.d.ts.map +1 -1
- package/dist/mcp/http.d.ts +15 -0
- package/dist/mcp/http.d.ts.map +1 -0
- package/dist/mcp/index.js +14334 -11630
- package/dist/mcp/server.d.ts +5 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/mcp/storage-tools.d.ts +3 -0
- package/dist/mcp/storage-tools.d.ts.map +1 -0
- package/dist/server/index.js +28445 -4917
- package/dist/server/serve.d.ts.map +1 -1
- package/dist/storage.d.ts +7 -0
- package/dist/storage.d.ts.map +1 -0
- package/dist/storage.js +5584 -0
- package/dist/types/index.d.ts +10 -2
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +16 -4
- package/dist/cli/cloud.d.ts +0 -3
- package/dist/cli/cloud.d.ts.map +0 -1
- package/dist/db/cloud-config.d.ts +0 -14
- package/dist/db/cloud-config.d.ts.map +0 -1
- package/dist/db/cloud-sync.d.ts +0 -30
- package/dist/db/cloud-sync.d.ts.map +0 -1
- package/dist/mcp/cloud-tools.d.ts +0 -3
- 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
|
-
}
|
|
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 {
|
|
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
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
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.
|
|
7397
|
-
description: "Unified search
|
|
7414
|
+
version: "0.0.10",
|
|
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
|
-
|
|
7419
|
-
|
|
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
|
|
7531
|
+
import chalk3 from "chalk";
|
|
7502
7532
|
|
|
7503
|
-
// src/cli/
|
|
7533
|
+
// src/cli/storage.ts
|
|
7504
7534
|
import chalk from "chalk";
|
|
7505
7535
|
|
|
7506
|
-
// src/db/
|
|
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
|
|
7511
|
-
|
|
7512
|
-
|
|
7513
|
-
|
|
7514
|
-
|
|
7515
|
-
|
|
7516
|
-
|
|
7517
|
-
|
|
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: "
|
|
7578
|
+
password_env: "SEARCH_DATABASE_PASSWORD",
|
|
7525
7579
|
ssl: true
|
|
7526
7580
|
}
|
|
7527
7581
|
};
|
|
7528
|
-
if (existsSync(
|
|
7582
|
+
if (existsSync(STORAGE_CONFIG_PATH)) {
|
|
7529
7583
|
try {
|
|
7530
|
-
const raw = JSON.parse(readFileSync(
|
|
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 =
|
|
7536
|
-
|
|
7537
|
-
|
|
7538
|
-
|
|
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
|
|
7544
|
-
const direct =
|
|
7598
|
+
function getStorageConnectionString(dbName = "search") {
|
|
7599
|
+
const direct = getStorageDatabaseUrl();
|
|
7545
7600
|
if (direct)
|
|
7546
7601
|
return direct;
|
|
7547
|
-
const config =
|
|
7602
|
+
const config = getStorageConfig();
|
|
7548
7603
|
const { host, port, username, password_env, ssl } = config.rds;
|
|
7549
7604
|
if (!host || !username) {
|
|
7550
|
-
throw new Error("
|
|
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 =
|
|
7607
|
+
const password = process.env[password_env];
|
|
7553
7608
|
if (!password) {
|
|
7554
|
-
throw new Error(`
|
|
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/
|
|
7615
|
+
// src/db/storage-sync.ts
|
|
7561
7616
|
init_database();
|
|
7562
7617
|
init_remote_storage();
|
|
7563
7618
|
init_pg_migrations();
|
|
7564
|
-
var
|
|
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
|
|
7652
|
-
return new PgAdapterAsync(
|
|
7706
|
+
async function getStoragePg() {
|
|
7707
|
+
return new PgAdapterAsync(getStorageConnectionString("search"));
|
|
7653
7708
|
}
|
|
7654
|
-
async function
|
|
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
|
|
7660
|
-
const config =
|
|
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 === "
|
|
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:
|
|
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
|
|
7735
|
+
async function pushStorageChanges(tables = [...STORAGE_TABLES]) {
|
|
7676
7736
|
const db = getDb();
|
|
7677
|
-
const remote = await
|
|
7737
|
+
const remote = await getStoragePg();
|
|
7678
7738
|
const results = [];
|
|
7679
7739
|
try {
|
|
7680
|
-
await
|
|
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
|
|
7757
|
+
async function pullStorageChanges(tables = [...STORAGE_TABLES]) {
|
|
7698
7758
|
const db = getDb();
|
|
7699
|
-
const remote = await
|
|
7759
|
+
const remote = await getStoragePg();
|
|
7700
7760
|
const results = [];
|
|
7701
7761
|
try {
|
|
7702
|
-
await
|
|
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
|
|
7779
|
+
async function syncStorageChanges(tables = [...STORAGE_TABLES]) {
|
|
7720
7780
|
return {
|
|
7721
|
-
push: await
|
|
7722
|
-
pull: await
|
|
7781
|
+
push: await pushStorageChanges(tables),
|
|
7782
|
+
pull: await pullStorageChanges(tables)
|
|
7723
7783
|
};
|
|
7724
7784
|
}
|
|
7725
|
-
function
|
|
7785
|
+
function parseStorageTables(raw) {
|
|
7726
7786
|
if (!raw)
|
|
7727
|
-
return [...
|
|
7787
|
+
return [...STORAGE_TABLES];
|
|
7728
7788
|
const requested = raw.split(",").map((table) => table.trim()).filter(Boolean);
|
|
7729
|
-
|
|
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/
|
|
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
|
|
7746
|
-
const
|
|
7747
|
-
|
|
7748
|
-
const status =
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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 ||
|
|
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
|
-
|
|
12896
|
-
|
|
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:
|
|
14518
|
+
resultCount: persistable.length,
|
|
12957
14519
|
duration
|
|
12958
14520
|
}, db);
|
|
12959
|
-
if (
|
|
12960
|
-
|
|
12961
|
-
|
|
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,
|
|
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(
|
|
13256
|
-
|
|
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(
|
|
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(
|
|
14848
|
+
console.log(chalk3.cyan(`
|
|
13289
14849
|
--- Transcript Matches ---`));
|
|
13290
14850
|
for (const m of deep.transcriptMatches) {
|
|
13291
|
-
console.log(
|
|
13292
|
-
console.log(
|
|
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(
|
|
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(
|
|
14876
|
+
console.log(chalk3.bold(`Search History (${total} total)`));
|
|
13317
14877
|
console.log();
|
|
13318
14878
|
for (const s of searches) {
|
|
13319
|
-
console.log(`${
|
|
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(
|
|
14885
|
+
console.error(chalk3.red(`Search not found: ${id}`));
|
|
13326
14886
|
return;
|
|
13327
14887
|
}
|
|
13328
|
-
console.log(
|
|
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(
|
|
14897
|
+
console.log(chalk3.green("Search deleted"));
|
|
13338
14898
|
} else {
|
|
13339
|
-
console.error(
|
|
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(
|
|
14906
|
+
console.log(chalk3.dim("No saved searches"));
|
|
13347
14907
|
return;
|
|
13348
14908
|
}
|
|
13349
14909
|
for (const s of items) {
|
|
13350
|
-
console.log(`${
|
|
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(
|
|
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(
|
|
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(
|
|
14934
|
+
console.log(chalk3.green("Saved search deleted"));
|
|
13375
14935
|
} else {
|
|
13376
|
-
console.error(
|
|
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(
|
|
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 ?
|
|
13387
|
-
const keyInfo = p.apiKeyEnv ?
|
|
13388
|
-
console.log(` ${
|
|
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(
|
|
14953
|
+
console.log(chalk3.green(`Provider ${name} enabled`));
|
|
13394
14954
|
} else {
|
|
13395
|
-
console.error(
|
|
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(
|
|
14960
|
+
console.log(chalk3.green(`Provider ${name} disabled`));
|
|
13401
14961
|
} else {
|
|
13402
|
-
console.error(
|
|
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(
|
|
14972
|
+
console.log(chalk3.green(`Provider ${name} updated`));
|
|
13413
14973
|
} else {
|
|
13414
|
-
console.error(
|
|
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(`${
|
|
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(
|
|
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(
|
|
14991
|
+
console.log(chalk3.green("Profile deleted"));
|
|
13432
14992
|
} else {
|
|
13433
|
-
console.error(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
15036
|
+
console.log(chalk3.green(`Config ${key} updated`));
|
|
13477
15037
|
});
|
|
13478
15038
|
config.command("reset").action(() => {
|
|
13479
15039
|
resetConfig();
|
|
13480
|
-
console.log(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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 =
|
|
13504
|
-
console.log(`${
|
|
13505
|
-
console.log(` ${
|
|
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(` ${
|
|
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(
|
|
15078
|
+
console.log(chalk3.yellow("Errors:"));
|
|
13516
15079
|
for (const e of errors2) {
|
|
13517
|
-
console.log(` ${
|
|
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(
|
|
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
|
|
13540
|
-
db.run("INSERT INTO feedback (message, email, category, version) VALUES (?, ?, ?, ?)", [message, opts.email || null, opts.category || "general",
|
|
13541
|
-
console.log(
|
|
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();
|