Skip to content
Draft
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@
### Fixes

- Fix paths in nix flake
- Configure SQLite connection pragmas before probing `sqlite-vec`, avoid
resetting `PRAGMA journal_mode = WAL` on every query startup, and tolerate
another process winning the WAL transition race so parallel readers don't
fail during initialization with transient `database is locked` errors.
- Sync stale `bun.lock` (`better-sqlite3` 11.x → 12.x). CI and release
script now use `--frozen-lockfile` to prevent recurrence. #386
(thanks @Mic92)
Expand Down
235 changes: 235 additions & 0 deletions install.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
#!/usr/bin/env bash
set -euo pipefail
umask 022
shopt -s lastpipe 2>/dev/null || true

REPO_URL="${QMD_REPO_URL:-https://github.com/chidev/qmd.git}"
DEFAULT_REF="${QMD_INSTALL_REF:-feature/stabilize_qmd}"
INSTALL_DIR_DEFAULT="${QMD_INSTALL_DIR:-$HOME/.local/share/chidev-qmd}"
BIN_DIR_DEFAULT="${QMD_BIN_DIR:-$HOME/.local/bin}"
BIN_NAME="${QMD_BIN_NAME:-qmd}"
NO_PATH_UPDATE=0
FORCE=0
REF="$DEFAULT_REF"
INSTALL_DIR="$INSTALL_DIR_DEFAULT"
BIN_DIR="$BIN_DIR_DEFAULT"

blue=$'\033[34m'
green=$'\033[32m'
yellow=$'\033[33m'
red=$'\033[31m'
bold=$'\033[1m'
reset=$'\033[0m'

info() { printf "%s->%s %s\n" "$blue" "$reset" "$*"; }
ok() { printf "%sOK%s %s\n" "$green" "$reset" "$*"; }
warn() { printf "%sWARN%s %s\n" "$yellow" "$reset" "$*" >&2; }
err() { printf "%sERR%s %s\n" "$red" "$reset" "$*" >&2; }

usage() {
cat <<EOF
${bold}chidev qmd installer${reset}

Installs the managed qmd fork and a stable wrapper executable.

Usage:
install.sh [options]

Options:
--ref REF Git ref to install (default: ${DEFAULT_REF})
--install-dir PATH Managed clone path (default: ${INSTALL_DIR_DEFAULT})
--bin-dir PATH Wrapper install dir (default: ${BIN_DIR_DEFAULT})
--bin-name NAME Wrapper name (default: ${BIN_NAME})
--force Reset managed clone if it is dirty
--no-path-update Do not try to add bin dir to shell rc files
-h, --help Show this help

Environment:
QMD_REPO_URL Override git remote URL
QMD_INSTALL_REF Override default ref
QMD_INSTALL_DIR Override default install dir
QMD_BIN_DIR Override default bin dir
QMD_BIN_NAME Override wrapper name
EOF
}

while [ $# -gt 0 ]; do
case "$1" in
--ref)
REF="${2:?missing value for --ref}"
shift 2
;;
--install-dir)
INSTALL_DIR="${2:?missing value for --install-dir}"
shift 2
;;
--bin-dir)
BIN_DIR="${2:?missing value for --bin-dir}"
shift 2
;;
--bin-name)
BIN_NAME="${2:?missing value for --bin-name}"
shift 2
;;
--force)
FORCE=1
shift
;;
--no-path-update)
NO_PATH_UPDATE=1
shift
;;
-h|--help)
usage
exit 0
;;
*)
err "unknown option: $1"
usage
exit 1
;;
esac
done

need_cmd() {
if ! command -v "$1" >/dev/null 2>&1; then
err "required command not found: $1"
exit 1
fi
}

ensure_node() {
need_cmd node
local major
major="$(node -p 'process.versions.node.split(".")[0]')"
if [ "${major:-0}" -lt 22 ]; then
err "node >= 22 is required"
exit 1
fi
}

pick_package_manager() {
if command -v bun >/dev/null 2>&1; then
echo "bun"
elif command -v npm >/dev/null 2>&1; then
echo "npm"
else
err "bun or npm is required"
exit 1
fi
}

ensure_path_entry() {
[ "$NO_PATH_UPDATE" -eq 1 ] && return 0
case ":$PATH:" in
*":$BIN_DIR:"*) return 0 ;;
esac

local line="export PATH=\"$BIN_DIR:\$PATH\""
local shell_name rc
shell_name="$(basename "${SHELL:-}")"
case "$shell_name" in
zsh) rc="$HOME/.zshrc" ;;
bash) rc="$HOME/.bashrc" ;;
*) rc="$HOME/.profile" ;;
esac

if [ -e "$rc" ] && ! [ -w "$rc" ]; then
warn "cannot update $rc; add $BIN_DIR to PATH manually"
return 0
fi

mkdir -p "$(dirname "$rc")"
touch "$rc"
if ! grep -F "$line" "$rc" >/dev/null 2>&1; then
printf "\n%s\n" "$line" >>"$rc"
ok "updated PATH in $rc"
fi
}

sync_repo() {
if [ -d "$INSTALL_DIR/.git" ]; then
info "updating managed clone in $INSTALL_DIR"
git -C "$INSTALL_DIR" remote set-url origin "$REPO_URL"
if [ "$FORCE" -eq 1 ]; then
git -C "$INSTALL_DIR" reset --hard HEAD
git -C "$INSTALL_DIR" clean -fd
elif [ -n "$(git -C "$INSTALL_DIR" status --porcelain)" ]; then
err "managed clone is dirty: $INSTALL_DIR (re-run with --force)"
exit 1
fi
git -C "$INSTALL_DIR" fetch origin "$REF" --depth 1
git -C "$INSTALL_DIR" checkout -B "$REF" FETCH_HEAD
else
info "cloning $REPO_URL to $INSTALL_DIR"
mkdir -p "$(dirname "$INSTALL_DIR")"
git clone --depth 1 --branch "$REF" "$REPO_URL" "$INSTALL_DIR"
fi
}

build_repo() {
local pm="$1"
info "building qmd with $pm"
case "$pm" in
bun)
(cd "$INSTALL_DIR" && bun install --frozen-lockfile || bun install)
(cd "$INSTALL_DIR" && bun run build)
;;
npm)
(cd "$INSTALL_DIR" && npm install)
(cd "$INSTALL_DIR" && npm run build)
;;
esac
}

clean_managed_clone() {
if [ -d "$INSTALL_DIR/.git" ]; then
git -C "$INSTALL_DIR" restore bun.lock 2>/dev/null || true
fi
rm -f "$INSTALL_DIR/package-lock.json"
}

install_wrapper() {
local wrapper_path="$BIN_DIR/$BIN_NAME"
info "installing wrapper to $wrapper_path"
mkdir -p "$BIN_DIR"
cat >"$wrapper_path" <<EOF
#!/usr/bin/env bash
set -euo pipefail
QMD_HOME="${INSTALL_DIR}"
exec node "\$QMD_HOME/dist/cli/qmd.js" "\$@"
EOF
chmod 0755 "$wrapper_path"
}

verify_install() {
local wrapper_path="$BIN_DIR/$BIN_NAME"
info "verifying wrapper"
"$wrapper_path" --version >/dev/null
ok "verified $wrapper_path"
}

main() {
info "installing managed qmd fork"
need_cmd git
ensure_node
local pm
pm="$(pick_package_manager)"
sync_repo
build_repo "$pm"
clean_managed_clone
install_wrapper
ensure_path_entry
verify_install
ok "qmd installed from $REPO_URL @ $REF"
printf "\n"
printf "Managed clone: %s\n" "$INSTALL_DIR"
printf "Wrapper: %s/%s\n" "$BIN_DIR" "$BIN_NAME"
printf "Ref: %s\n" "$REF"
case ":$PATH:" in
*":$BIN_DIR:"*) printf "PATH: ready\n" ;;
*) printf "PATH: add %s to PATH or open a new shell\n" "$BIN_DIR" ;;
esac
}

main "$@"
22 changes: 18 additions & 4 deletions src/cli/qmd.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2183,7 +2183,7 @@ async function vectorSearch(query: string, opts: OutputOptions, _model: string =

checkIndexHealth(store.db);

await withLLMSession(async () => {
const runSearch = async () => {
let results = await vectorSearchQuery(store, query, {
collection: singleCollection,
limit: opts.all ? 500 : (opts.limit || 10),
Expand Down Expand Up @@ -2221,7 +2221,14 @@ async function vectorSearch(query: string, opts: OutputOptions, _model: string =
context: r.context,
docid: r.docid,
})), query, { ...opts, limit: results.length });
}, { maxDuration: 10 * 60 * 1000, name: 'vectorSearch' });
};

// Skip local LLM session when using remote Ollama for embeddings
if (process.env.OLLAMA_EMBED_URL) {
await runSearch();
} else {
await withLLMSession(runSearch, { maxDuration: 10 * 60 * 1000, name: 'vectorSearch' });
}
}

async function querySearch(query: string, opts: OutputOptions, _embedModel: string = DEFAULT_EMBED_MODEL, _rerankModel: string = DEFAULT_RERANK_MODEL): Promise<void> {
Expand All @@ -2239,7 +2246,7 @@ async function querySearch(query: string, opts: OutputOptions, _embedModel: stri
// Intent can come from --intent flag or from intent: line in query document
const intent = opts.intent || parsed?.intent;

await withLLMSession(async () => {
const runQuery = async () => {
let results;

if (parsed) {
Expand Down Expand Up @@ -2359,7 +2366,14 @@ async function querySearch(query: string, opts: OutputOptions, _embedModel: stri
docid: r.docid,
explain: r.explain,
})), displayQuery, { ...opts, limit: results.length });
}, { maxDuration: 10 * 60 * 1000, name: 'querySearch' });
};

// Skip local LLM session when using remote Ollama for embeddings
if (process.env.OLLAMA_EMBED_URL) {
await runQuery();
} else {
await withLLMSession(runQuery, { maxDuration: 10 * 60 * 1000, name: 'querySearch' });
}
}

// Parse CLI arguments using util.parseArgs
Expand Down
3 changes: 3 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,8 @@ export interface SearchOptions {
limit?: number;
/** Minimum score threshold */
minScore?: number;
/** Maximum candidates to rerank (default: 40) */
candidateLimit?: number;
/** Include explain traces */
explain?: boolean;
/** Chunk strategy: "auto" (default, uses AST for code files) or "regex" (legacy) */
Expand Down Expand Up @@ -393,6 +395,7 @@ export async function createStore(options: StoreOptions): Promise<QMDStore> {
collections: collections.length > 0 ? collections : undefined,
limit: opts.limit,
minScore: opts.minScore,
candidateLimit: opts.candidateLimit,
explain: opts.explain,
intent: opts.intent,
skipRerank,
Expand Down
8 changes: 6 additions & 2 deletions src/mcp/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,9 @@ Intent-aware lex (C++ performance, not sports):
candidateLimit: z.number().optional().describe(
"Maximum candidates to rerank (default: 40, lower = faster but may miss results)"
),
skipRerank: z.boolean().optional().describe(
"Skip LLM reranking and use RRF fusion scores only. Much faster on CPU-only servers."
),
collections: z.array(z.string()).optional().describe("Filter to collections (OR match)"),
intent: z.string().optional().describe(
"Background context to disambiguate the query. Example: query='performance', intent='web page load times and Core Web Vitals'. Does not search on its own."
Expand All @@ -301,7 +304,7 @@ Intent-aware lex (C++ performance, not sports):
),
},
},
async ({ searches, limit, minScore, candidateLimit, collections, intent, rerank }) => {
async ({ searches, limit, minScore, candidateLimit, skipRerank, collections, intent, rerank }) => {
// Map to internal format
const queries: ExpandedQuery[] = searches.map(s => ({
type: s.type,
Expand All @@ -316,8 +319,9 @@ Intent-aware lex (C++ performance, not sports):
collections: effectiveCollections.length > 0 ? effectiveCollections : undefined,
limit,
minScore,
rerank,
candidateLimit,
intent,
rerank: skipRerank ? false : rerank,
});

// Use first lex or vec query for snippet extraction
Expand Down
Loading