Sfoglia il codice sorgente

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
Tobi Lutke 3 mesi fa
parent
commit
13e8473455
3 ha cambiato i file con 95 aggiunte e 47 eliminazioni
  1. 7 8
      README.md
  2. 28 12
      package.json
  3. 60 27
      src/mcp.ts

+ 7 - 8
README.md

@@ -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

+ 28 - 12
package.json

@@ -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",

+ 60 - 27
src/mcp.ts

@@ -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;
-
-      if (pathname === "/health" && req.method === "GET") {
-        const res = Response.json({
-          status: "ok",
-          uptime: Math.floor((Date.now() - startTime) / 1000),
-        });
+  // 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();
+  }
+
+  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");
+    }
+  });
+
+  await new Promise<void>((resolve, reject) => {
+    httpServer.on("error", reject);
+    httpServer.listen(port, "localhost", () => resolve());
   });
 
-  const actualPort = httpServer.port;
+  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);
 }