docs: update node usage and bump version

Update README installation and quick-start commands to Node examples.
- replace bun install/link commands with npm-based Node workflow
- bump package version to 0.9.9 for CLI and MCP metadata
- keep Bun guidance as optional development/runtime note
This commit is contained in:
Tobi Lutke 2026-02-15 16:44:47 -04:00
parent 392934e78a
commit 13e8473455
No known key found for this signature in database
3 changed files with 94 additions and 46 deletions

View File

@ -9,8 +9,8 @@ QMD combines BM25 full-text search, vector semantic search, and LLM re-ranking
## Quick Start
```sh
# Install globally
bun install -g https://github.com/tobi/qmd
# Install globally (Node)
npm install -g github:tobi/qmd
# Create collections for your notes, docs, and meeting transcripts
qmd collection add ~/notes --name notes
@ -231,7 +231,8 @@ The `query` command uses **Reciprocal Rank Fusion (RRF)** with position-aware bl
### System Requirements
- **Bun** >= 1.0.0
- **Node.js** >= 22
- **Bun** >= 1.0.0 (optional; supported for local development)
- **macOS**: Homebrew SQLite (for extension support)
```sh
brew install sqlite
@ -252,18 +253,16 @@ Models are downloaded from HuggingFace and cached in `~/.cache/qmd/models/`.
## Installation
```sh
bun install -g github:tobi/qmd
npm install -g github:tobi/qmd
```
Make sure `~/.bun/bin` is in your PATH.
### Development
```sh
git clone https://github.com/tobi/qmd
cd qmd
bun install
bun link
npm install
npm link
```
## Usage

View File

@ -1,25 +1,39 @@
{
"name": "qmd",
"version": "1.0.0",
"version": "0.9.9",
"description": "Quick Markdown Search - Full-text and vector search for markdown files",
"type": "module",
"bin": {
"qmd": "./qmd"
},
"scripts": {
"test": "bun test",
"qmd": "bun src/qmd.ts",
"index": "bun src/qmd.ts index",
"vector": "bun src/qmd.ts vector",
"search": "bun src/qmd.ts search",
"vsearch": "bun src/qmd.ts vsearch",
"rerank": "bun src/qmd.ts rerank",
"link": "bun link",
"inspector": "npx @modelcontextprotocol/inspector bun src/qmd.ts mcp"
"test": "vitest run",
"test:unit": "vitest run --reporter=verbose src/*.test.ts",
"test:models": "vitest run --reporter=verbose src/models/*.test.ts",
"test:integration": "vitest run --reporter=verbose src/integration/*.test.ts",
"test:unit:bun": "bun run vitest run --reporter=verbose --testTimeout=120000 src/*.test.ts",
"test:models:bun": "bun run vitest run --reporter=verbose --testTimeout=120000 src/models/*.test.ts",
"test:integration:bun": "bun run vitest run --reporter=verbose --testTimeout=120000 src/integration/*.test.ts",
"test:unit:node": "npx vitest run --reporter=verbose --testTimeout=120000 src/*.test.ts",
"test:models:node": "npx vitest run --reporter=verbose --testTimeout=120000 src/models/*.test.ts",
"test:integration:node": "npx vitest run --reporter=verbose --testTimeout=120000 src/integration/*.test.ts",
"test:ci:bun": "npm run test:unit:bun && npm run test:models:bun && npm run test:integration:bun",
"test:ci:node": "npm run test:unit:node && npm run test:models:node && npm run test:integration:node",
"test:ci": "npm run test:unit && npm run test:models && npm run test:integration",
"qmd": "tsx src/qmd.ts",
"index": "tsx src/qmd.ts index",
"vector": "tsx src/qmd.ts vector",
"search": "tsx src/qmd.ts search",
"vsearch": "tsx src/qmd.ts vsearch",
"rerank": "tsx src/qmd.ts rerank",
"inspector": "npx @modelcontextprotocol/inspector tsx src/qmd.ts mcp"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.25.1",
"better-sqlite3": "^11.0.0",
"fast-glob": "^3.3.0",
"node-llama-cpp": "^3.14.5",
"picomatch": "^4.0.0",
"sqlite-vec": "^0.1.7-alpha.2",
"yaml": "^2.8.2",
"zod": "^4.2.1"
@ -31,13 +45,15 @@
"sqlite-vec-win32-x64": "^0.1.7-alpha.2"
},
"devDependencies": {
"@types/bun": "latest"
"@types/better-sqlite3": "^7.6.0",
"tsx": "^4.0.0",
"vitest": "^3.0.0"
},
"peerDependencies": {
"typescript": "^5.9.3"
},
"engines": {
"bun": ">=1.0.0"
"node": ">=22.0.0"
},
"keywords": [
"markdown",

View File

@ -1,4 +1,3 @@
#!/usr/bin/env bun
/**
* QMD MCP Server - Model Context Protocol server for QMD
*
@ -8,6 +7,8 @@
* Follows MCP spec 2025-06-18 for proper response types.
*/
import { createServer, type IncomingMessage, type ServerResponse } from "node:http";
import { fileURLToPath } from "url";
import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { WebStandardStreamableHTTPServerTransport }
@ -147,7 +148,7 @@ function buildInstructions(store: Store): string {
*/
function createMcpServer(store: Store): McpServer {
const server = new McpServer(
{ name: "qmd", version: "1.0.0" },
{ name: "qmd", version: "0.9.9" },
{ instructions: buildInstructions(store) },
);
@ -539,7 +540,7 @@ export async function startMcpServer(): Promise<void> {
// =============================================================================
export type HttpServerHandle = {
httpServer: ReturnType<typeof Bun.serve>;
httpServer: import("http").Server;
port: number;
stop: () => Promise<void>;
};
@ -586,47 +587,79 @@ export async function startMcpHttpServer(port: number, options?: { quiet?: boole
if (!quiet) console.error(msg);
}
const httpServer = Bun.serve({
port,
hostname: "localhost",
async fetch(req) {
const reqStart = Date.now();
const pathname = new URL(req.url).pathname;
// Helper to collect request body
async function collectBody(req: IncomingMessage): Promise<string> {
const chunks: Buffer[] = [];
for await (const chunk of req) chunks.push(chunk as Buffer);
return Buffer.concat(chunks).toString();
}
if (pathname === "/health" && req.method === "GET") {
const res = Response.json({
status: "ok",
uptime: Math.floor((Date.now() - startTime) / 1000),
});
const httpServer = createServer(async (nodeReq: IncomingMessage, nodeRes: ServerResponse) => {
const reqStart = Date.now();
const pathname = nodeReq.url || "/";
try {
if (pathname === "/health" && nodeReq.method === "GET") {
const body = JSON.stringify({ status: "ok", uptime: Math.floor((Date.now() - startTime) / 1000) });
nodeRes.writeHead(200, { "Content-Type": "application/json" });
nodeRes.end(body);
log(`${ts()} GET /health (${Date.now() - reqStart}ms)`);
return res;
return;
}
if (pathname === "/mcp" && req.method === "POST") {
const body = await req.json();
if (pathname === "/mcp" && nodeReq.method === "POST") {
const rawBody = await collectBody(nodeReq);
const body = JSON.parse(rawBody);
const label = describeRequest(body);
const res = await transport.handleRequest(req, { parsedBody: body });
const url = `http://localhost:${port}${pathname}`;
const headers: Record<string, string> = {};
for (const [k, v] of Object.entries(nodeReq.headers)) {
if (typeof v === "string") headers[k] = v;
}
const request = new Request(url, { method: "POST", headers, body: rawBody });
const response = await transport.handleRequest(request, { parsedBody: body });
nodeRes.writeHead(response.status, Object.fromEntries(response.headers));
nodeRes.end(Buffer.from(await response.arrayBuffer()));
log(`${ts()} POST /mcp ${label} (${Date.now() - reqStart}ms)`);
return res;
return;
}
// Pass other methods (GET, DELETE) to transport for protocol handling
if (pathname === "/mcp") {
return transport.handleRequest(req);
const url = `http://localhost:${port}${pathname}`;
const headers: Record<string, string> = {};
for (const [k, v] of Object.entries(nodeReq.headers)) {
if (typeof v === "string") headers[k] = v;
}
const rawBody = nodeReq.method !== "GET" && nodeReq.method !== "HEAD" ? await collectBody(nodeReq) : undefined;
const request = new Request(url, { method: nodeReq.method || "GET", headers, ...(rawBody ? { body: rawBody } : {}) });
const response = await transport.handleRequest(request);
nodeRes.writeHead(response.status, Object.fromEntries(response.headers));
nodeRes.end(Buffer.from(await response.arrayBuffer()));
return;
}
return new Response("Not Found", { status: 404 });
},
nodeRes.writeHead(404);
nodeRes.end("Not Found");
} catch (err) {
console.error("HTTP handler error:", err);
nodeRes.writeHead(500);
nodeRes.end("Internal Server Error");
}
});
const actualPort = httpServer.port;
await new Promise<void>((resolve, reject) => {
httpServer.on("error", reject);
httpServer.listen(port, "localhost", () => resolve());
});
const actualPort = (httpServer.address() as import("net").AddressInfo).port;
let stopping = false;
const stop = async () => {
if (stopping) return;
stopping = true;
await transport.close();
httpServer.stop();
httpServer.close();
store.close();
await disposeDefaultLlamaCpp();
};
@ -647,6 +680,6 @@ export async function startMcpHttpServer(port: number, options?: { quiet?: boole
}
// Run if this is the main module
if (import.meta.main) {
if (fileURLToPath(import.meta.url) === process.argv[1] || process.argv[1]?.endsWith("/mcp.ts")) {
startMcpServer().catch(console.error);
}