Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"description": "Quick Markdown Search - Full-text and vector search for markdown files",
"type": "module",
"bin": {
"qmd": "./qmd"
"qmd": "./src/qmd.ts"
},
"scripts": {
"test": "bun test",
Expand Down
41 changes: 24 additions & 17 deletions src/qmd.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import {
getRealPath,
homedir,
resolve,
normalizePathSeparators,
isAbsolutePath,
enableProductionMode,
searchFTS,
searchVec,
Expand Down Expand Up @@ -204,16 +206,17 @@ function computeDisplayPath(
existingPaths: Set<string>
): string {
// Get path relative to collection (include collection dir name)
const collectionDir = collectionPath.replace(/\/$/, '');
const collectionDir = normalizePathSeparators(collectionPath).replace(/\/$/, '');
const collectionName = collectionDir.split('/').pop() || '';
const normalizedFilepath = normalizePathSeparators(filepath);

let relativePath: string;
if (filepath.startsWith(collectionDir + '/')) {
if (normalizedFilepath.startsWith(collectionDir + '/')) {
// filepath is under collection: use collection name + relative path
relativePath = collectionName + filepath.slice(collectionDir.length);
relativePath = collectionName + normalizedFilepath.slice(collectionDir.length);
} else {
// Fallback: just use the filepath
relativePath = filepath;
relativePath = normalizedFilepath;
}

const parts = relativePath.split('/').filter(p => p.length > 0);
Expand Down Expand Up @@ -400,7 +403,10 @@ async function updateCollections(): Promise<void> {
if (yamlCol?.update) {
console.log(`${c.dim} Running update command: ${yamlCol.update}${c.reset}`);
try {
const proc = Bun.spawn(["/usr/bin/env", "bash", "-c", yamlCol.update], {
const shellCmd = process.platform === "win32"
? ["cmd", "/c", yamlCol.update]
: ["/usr/bin/env", "bash", "-c", yamlCol.update];
const proc = Bun.spawn(shellCmd, {
cwd: col.pwd,
stdout: "pipe",
stderr: "pipe",
Expand Down Expand Up @@ -447,17 +453,18 @@ async function updateCollections(): Promise<void> {
* Returns { collectionId, collectionName, relativePath } or null if not in any collection.
*/
function detectCollectionFromPath(db: Database, fsPath: string): { collectionName: string; relativePath: string } | null {
const realPath = getRealPath(fsPath);
const realPath = normalizePathSeparators(getRealPath(fsPath));

// Find collections that this path is under from YAML
const allCollections = yamlListCollections();

// Find longest matching path
let bestMatch: { name: string; path: string } | null = null;
for (const coll of allCollections) {
if (realPath.startsWith(coll.path + '/') || realPath === coll.path) {
if (!bestMatch || coll.path.length > bestMatch.path.length) {
bestMatch = { name: coll.name, path: coll.path };
const collPath = normalizePathSeparators(coll.path);
if (realPath.startsWith(collPath + '/') || realPath === collPath) {
if (!bestMatch || collPath.length > bestMatch.path.length) {
bestMatch = { name: coll.name, path: collPath };
}
}
}
Expand Down Expand Up @@ -496,7 +503,7 @@ async function contextAdd(pathArg: string | undefined, contextText: string): Pro
fsPath = getPwd();
} else if (fsPath.startsWith('~/')) {
fsPath = homedir() + fsPath.slice(1);
} else if (!fsPath.startsWith('/') && !fsPath.startsWith('qmd://')) {
} else if (!isAbsolutePath(fsPath) && !fsPath.startsWith('qmd://')) {
fsPath = resolve(getPwd(), fsPath);
}

Expand Down Expand Up @@ -608,7 +615,7 @@ function contextRemove(pathArg: string): void {
fsPath = getPwd();
} else if (fsPath.startsWith('~/')) {
fsPath = homedir() + fsPath.slice(1);
} else if (!fsPath.startsWith('/')) {
} else if (!isAbsolutePath(fsPath)) {
fsPath = resolve(getPwd(), fsPath);
}

Expand Down Expand Up @@ -749,8 +756,8 @@ function getDocument(filename: string, fromLine?: number, maxLines?: number, lin
} else {
// Try to interpret as collection/path format first (before filesystem path)
// If path is relative (no / or ~ prefix), check if first component is a collection name
if (!inputPath.startsWith('/') && !inputPath.startsWith('~')) {
const parts = inputPath.split('/');
if (!isAbsolutePath(inputPath) && !inputPath.startsWith('~')) {
const parts = normalizePathSeparators(inputPath).split('/');
if (parts.length >= 2) {
const possibleCollection = parts[0];
const possiblePath = parts.slice(1).join('/');
Expand Down Expand Up @@ -795,7 +802,7 @@ function getDocument(filename: string, fromLine?: number, maxLines?: number, lin
// Expand ~ to home directory
if (fsPath.startsWith('~/')) {
fsPath = homedir() + fsPath.slice(1);
} else if (!fsPath.startsWith('/')) {
} else if (!isAbsolutePath(fsPath)) {
// Relative path - resolve from current directory
fsPath = resolve(getPwd(), fsPath);
}
Expand All @@ -816,7 +823,7 @@ function getDocument(filename: string, fromLine?: number, maxLines?: number, lin

// Fuzzy match by filename (last component of path)
if (!doc) {
const filename = inputPath.split('/').pop() || inputPath;
const filename = normalizePathSeparators(inputPath).split('/').pop() || inputPath;
doc = db.prepare(`
SELECT d.collection as collectionName, d.path, content.doc as body
FROM documents d
Expand Down Expand Up @@ -1154,7 +1161,7 @@ function listFiles(pathArg?: string): void {
pathPrefix = parsed.path;
} else {
// Just collection name or collection/path
const parts = pathArg.split('/');
const parts = normalizePathSeparators(pathArg).split('/');
collectionName = parts[0] || '';
if (parts.length > 1) {
pathPrefix = parts.slice(1).join('/');
Expand Down Expand Up @@ -1275,7 +1282,7 @@ async function collectionAdd(pwd: string, globPattern: string, name?: string): P
// If name not provided, generate from pwd basename
let collName = name;
if (!collName) {
const parts = pwd.split('/').filter(Boolean);
const parts = normalizePathSeparators(pwd).split('/').filter(Boolean);
collName = parts[parts.length - 1] || 'root';
}

Expand Down
23 changes: 18 additions & 5 deletions src/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@

import { Database } from "bun:sqlite";
import { Glob } from "bun";
import { realpathSync, statSync } from "node:fs";
import { mkdirSync, realpathSync, statSync } from "node:fs";
import { fileURLToPath } from "node:url";
import * as sqliteVec from "sqlite-vec";
import {
LlamaCpp,
Expand Down Expand Up @@ -42,7 +43,7 @@ import {
// Configuration
// =============================================================================

const HOME = Bun.env.HOME || "/tmp";
const HOME = Bun.env.HOME || Bun.env.USERPROFILE || "/tmp";
export const DEFAULT_EMBED_MODEL = "embeddinggemma";
export const DEFAULT_RERANK_MODEL = "ExpedientFalcon/qwen3-reranker:0.6b-q8_0";
export const DEFAULT_QUERY_MODEL = "Qwen/Qwen3-1.7B";
Expand Down Expand Up @@ -254,7 +255,7 @@ export function getDefaultDbPath(indexName: string = "index"): string {

const cacheDir = Bun.env.XDG_CACHE_HOME || resolve(homedir(), ".cache");
const qmdCacheDir = resolve(cacheDir, "qmd");
try { Bun.spawnSync(["mkdir", "-p", qmdCacheDir]); } catch { }
try { mkdirSync(qmdCacheDir, { recursive: true }); } catch { }
return resolve(qmdCacheDir, `${indexName}.sqlite`);
}

Expand Down Expand Up @@ -441,14 +442,26 @@ function initializeDatabase(db: Database): void {
try {
sqliteVec.load(db);
} catch (err) {
if (err instanceof Error && err.message.includes("does not support dynamic extension loading")) {
// On Windows, sqlite-vec may fail to resolve the native DLL when running
// from outside the project directory. Fall back to loading it directly
// from this project's node_modules using an absolute path.
if (process.platform === "win32") {
try {
const scriptDir = resolve(fileURLToPath(import.meta.url), "..");
const vecDll = resolve(scriptDir, "..", "node_modules", "sqlite-vec-windows-x64", "vec0");
db.loadExtension(vecDll);
} catch (fallbackErr) {
throw err; // throw original error if fallback also fails
}
} else if (err instanceof Error && err.message.includes("does not support dynamic extension loading")) {
throw new Error(
"SQLite build does not support dynamic extension loading. " +
"Install Homebrew SQLite so the sqlite-vec extension can be loaded, " +
"and set BREW_PREFIX if Homebrew is installed in a non-standard location."
);
} else {
throw err;
}
throw err;
}
db.exec("PRAGMA journal_mode = WAL");
db.exec("PRAGMA foreign_keys = ON");
Expand Down