@grumppie/ownsearch 0.1.3

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/dist/cli.d.ts ADDED
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
package/dist/cli.js ADDED
@@ -0,0 +1,229 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ OwnSearchError,
4
+ buildContextBundle,
5
+ createStore,
6
+ deleteRootDefinition,
7
+ embedQuery,
8
+ findRoot,
9
+ getConfigPath,
10
+ getEnvPath,
11
+ indexPath,
12
+ listRoots,
13
+ loadConfig,
14
+ loadOwnSearchEnv,
15
+ saveGeminiApiKey
16
+ } from "./chunk-LGXCBOO4.js";
17
+
18
+ // src/cli.ts
19
+ import path from "path";
20
+ import { spawn } from "child_process";
21
+ import readline from "readline/promises";
22
+ import { fileURLToPath } from "url";
23
+ import { Command } from "commander";
24
+
25
+ // src/docker.ts
26
+ import { execFile } from "child_process";
27
+ import { promisify } from "util";
28
+ var execFileAsync = promisify(execFile);
29
+ async function runDocker(args) {
30
+ try {
31
+ const { stdout } = await execFileAsync("docker", args, { windowsHide: true });
32
+ return stdout.trim();
33
+ } catch (error) {
34
+ throw new OwnSearchError("Docker is required for Qdrant setup. Install Docker and ensure `docker` is on PATH.");
35
+ }
36
+ }
37
+ async function ensureQdrantDocker() {
38
+ const config = await loadConfig();
39
+ const containerName = config.qdrantContainerName;
40
+ const volumeName = config.qdrantVolumeName;
41
+ const existing = await runDocker(["ps", "-a", "--filter", `name=^/${containerName}$`, "--format", "{{.Names}}"]);
42
+ if (existing === containerName) {
43
+ const running = await runDocker(["inspect", "-f", "{{.State.Running}}", containerName]);
44
+ if (running === "true") {
45
+ return { started: false, url: config.qdrantUrl };
46
+ }
47
+ await runDocker(["start", containerName]);
48
+ return { started: true, url: config.qdrantUrl };
49
+ }
50
+ await runDocker([
51
+ "run",
52
+ "-d",
53
+ "--name",
54
+ containerName,
55
+ "-p",
56
+ "6333:6333",
57
+ "-p",
58
+ "6334:6334",
59
+ "-v",
60
+ `${volumeName}:/qdrant/storage`,
61
+ "qdrant/qdrant:latest"
62
+ ]);
63
+ return { started: true, url: config.qdrantUrl };
64
+ }
65
+
66
+ // src/cli.ts
67
+ loadOwnSearchEnv();
68
+ var program = new Command();
69
+ var PACKAGE_NAME = "@grumppie/ownsearch";
70
+ function requireGeminiKey() {
71
+ if (!process.env.GEMINI_API_KEY) {
72
+ throw new OwnSearchError("Set GEMINI_API_KEY before running OwnSearch.");
73
+ }
74
+ }
75
+ async function promptForGeminiKey() {
76
+ if (process.env.GEMINI_API_KEY || !process.stdin.isTTY || !process.stdout.isTTY) {
77
+ return Boolean(process.env.GEMINI_API_KEY);
78
+ }
79
+ const rl = readline.createInterface({
80
+ input: process.stdin,
81
+ output: process.stdout
82
+ });
83
+ try {
84
+ const apiKey = (await rl.question(
85
+ `Enter GEMINI_API_KEY to save in ${getEnvPath()} (leave blank to skip): `
86
+ )).trim();
87
+ if (!apiKey) {
88
+ return false;
89
+ }
90
+ await saveGeminiApiKey(apiKey);
91
+ process.env.GEMINI_API_KEY = apiKey;
92
+ return true;
93
+ } finally {
94
+ rl.close();
95
+ }
96
+ }
97
+ program.name("ownsearch").description("Gemini-powered local search MCP server backed by Qdrant.").version("0.1.0");
98
+ program.command("setup").description("Create config and start a local Qdrant Docker container.").action(async () => {
99
+ const config = await loadConfig();
100
+ const result = await ensureQdrantDocker();
101
+ const geminiApiKeyPresent = await promptForGeminiKey();
102
+ console.log(JSON.stringify({
103
+ configPath: getConfigPath(),
104
+ envPath: getEnvPath(),
105
+ qdrantUrl: config.qdrantUrl,
106
+ qdrantStarted: result.started,
107
+ geminiApiKeyPresent
108
+ }, null, 2));
109
+ if (!geminiApiKeyPresent) {
110
+ console.log(`GEMINI_API_KEY is not set. Re-run setup or add it to ${getEnvPath()} before indexing or search.`);
111
+ }
112
+ });
113
+ program.command("index").argument("<folder>", "Folder path to index").option("-n, --name <name>", "Display name for the indexed root").option("--max-file-bytes <n>", "Override the file size limit for this run", (value) => Number(value)).description("Index a local folder into Qdrant using Gemini embeddings.").action(async (folder, options) => {
114
+ requireGeminiKey();
115
+ const result = await indexPath(folder, {
116
+ name: options.name,
117
+ maxFileBytes: options.maxFileBytes
118
+ });
119
+ console.log(JSON.stringify(result, null, 2));
120
+ });
121
+ program.command("search").argument("<query>", "Natural language query").option("--root-id <rootId...>", "Restrict search to one or more root IDs (repeatable)").option("--limit <n>", "Max results (default 5)", (value) => Number(value), 5).option("--path <substr>", "Filter results to files whose relative path contains this substring").description("Embed a query with Gemini and search the local Qdrant store.").action(
122
+ async (query, options) => {
123
+ requireGeminiKey();
124
+ const store = await createStore();
125
+ const vector = await embedQuery(query);
126
+ const hits = await store.search(
127
+ vector,
128
+ {
129
+ rootIds: options.rootId,
130
+ pathSubstring: options.path
131
+ },
132
+ Math.max(1, Math.min(options.limit ?? 5, 50))
133
+ );
134
+ console.log(JSON.stringify({ query, hits }, null, 2));
135
+ }
136
+ );
137
+ program.command("search-context").argument("<query>", "Natural language query").option("--root-id <rootId...>", "Restrict search to one or more root IDs (repeatable)").option("--limit <n>", "Max search hits to consider (default 8)", (value) => Number(value), 8).option("--max-chars <n>", "Max context characters to return (default 12000)", (value) => Number(value), 12e3).option("--path <substr>", "Filter results to files whose relative path contains this substring").description("Search the local Qdrant store and return a bundled context payload for agent use.").action(
138
+ async (query, options) => {
139
+ requireGeminiKey();
140
+ const store = await createStore();
141
+ const vector = await embedQuery(query);
142
+ const hits = await store.search(
143
+ vector,
144
+ {
145
+ rootIds: options.rootId,
146
+ pathSubstring: options.path
147
+ },
148
+ Math.max(1, Math.min(options.limit ?? 8, 20))
149
+ );
150
+ console.log(JSON.stringify(buildContextBundle(query, hits, Math.max(500, options.maxChars ?? 12e3)), null, 2));
151
+ }
152
+ );
153
+ program.command("list-roots").description("List indexed roots registered in local config.").action(async () => {
154
+ console.log(JSON.stringify({ roots: await listRoots() }, null, 2));
155
+ });
156
+ program.command("delete-root").argument("<rootId>", "Root identifier to delete").description("Delete one indexed root from local config and Qdrant.").action(async (rootId) => {
157
+ const root = await findRoot(rootId);
158
+ if (!root) {
159
+ throw new OwnSearchError(`Unknown root: ${rootId}`);
160
+ }
161
+ const store = await createStore();
162
+ await store.deleteRoot(root.id);
163
+ await deleteRootDefinition(root.id);
164
+ console.log(JSON.stringify({ deleted: true, root }, null, 2));
165
+ });
166
+ program.command("store-status").description("Show Qdrant collection status for this package.").action(async () => {
167
+ const store = await createStore();
168
+ console.log(JSON.stringify(await store.getStatus(), null, 2));
169
+ });
170
+ program.command("doctor").description("Check local prerequisites and package configuration.").action(async () => {
171
+ const config = await loadConfig();
172
+ const roots = await listRoots();
173
+ let qdrantReachable = false;
174
+ try {
175
+ const store = await createStore();
176
+ await store.getStatus();
177
+ qdrantReachable = true;
178
+ } catch (error) {
179
+ qdrantReachable = false;
180
+ }
181
+ console.log(JSON.stringify({
182
+ configPath: getConfigPath(),
183
+ envPath: getEnvPath(),
184
+ geminiApiKeyPresent: Boolean(process.env.GEMINI_API_KEY),
185
+ qdrantUrl: config.qdrantUrl,
186
+ qdrantReachable,
187
+ collection: config.qdrantCollection,
188
+ embeddingModel: config.embeddingModel,
189
+ vectorSize: config.vectorSize,
190
+ chunkSize: config.chunkSize,
191
+ chunkOverlap: config.chunkOverlap,
192
+ maxFileBytes: config.maxFileBytes,
193
+ rootCount: roots.length
194
+ }, null, 2));
195
+ });
196
+ program.command("serve-mcp").description("Start the stdio MCP server.").action(async () => {
197
+ const currentFilePath = fileURLToPath(import.meta.url);
198
+ const serverPath = path.join(path.dirname(currentFilePath), "mcp", "server.js");
199
+ const child = spawn(process.execPath, [serverPath], {
200
+ stdio: "inherit",
201
+ env: process.env
202
+ });
203
+ child.on("exit", (code) => {
204
+ process.exitCode = code ?? 0;
205
+ });
206
+ });
207
+ program.command("print-agent-config").argument("<agent>", "codex | claude-desktop | cursor").description("Print an MCP config snippet for a supported agent.").action(async (agent) => {
208
+ const config = {
209
+ command: "npx",
210
+ args: ["-y", PACKAGE_NAME, "serve-mcp"],
211
+ env: {
212
+ GEMINI_API_KEY: "${GEMINI_API_KEY}"
213
+ }
214
+ };
215
+ switch (agent) {
216
+ case "codex":
217
+ case "claude-desktop":
218
+ case "cursor":
219
+ console.log(JSON.stringify({ ownsearch: config }, null, 2));
220
+ return;
221
+ default:
222
+ throw new OwnSearchError(`Unsupported agent: ${agent}`);
223
+ }
224
+ });
225
+ program.parseAsync(process.argv).catch((error) => {
226
+ const message = error instanceof Error ? error.message : String(error);
227
+ console.error(message);
228
+ process.exitCode = 1;
229
+ });
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
@@ -0,0 +1,243 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ OwnSearchError,
4
+ buildContextBundle,
5
+ createStore,
6
+ deleteRootDefinition,
7
+ embedQuery,
8
+ findRoot,
9
+ indexPath,
10
+ loadConfig,
11
+ loadOwnSearchEnv
12
+ } from "../chunk-LGXCBOO4.js";
13
+
14
+ // src/mcp/server.ts
15
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
16
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
17
+ import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
18
+ loadOwnSearchEnv();
19
+ function asText(result) {
20
+ return {
21
+ content: [
22
+ {
23
+ type: "text",
24
+ text: JSON.stringify(result, null, 2)
25
+ }
26
+ ]
27
+ };
28
+ }
29
+ function asError(error) {
30
+ const message = error instanceof Error ? error.message : String(error);
31
+ return {
32
+ isError: true,
33
+ content: [
34
+ {
35
+ type: "text",
36
+ text: message
37
+ }
38
+ ]
39
+ };
40
+ }
41
+ var server = new Server(
42
+ {
43
+ name: "ownsearch",
44
+ version: "0.1.0"
45
+ },
46
+ {
47
+ capabilities: {
48
+ tools: {}
49
+ }
50
+ }
51
+ );
52
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
53
+ tools: [
54
+ {
55
+ name: "index_path",
56
+ description: "Register a local folder and sync its Gemini embedding index into Qdrant.",
57
+ inputSchema: {
58
+ type: "object",
59
+ properties: {
60
+ path: { type: "string", description: "Absolute or relative folder path to index." },
61
+ name: { type: "string", description: "Optional display name for this indexed root." }
62
+ },
63
+ required: ["path"]
64
+ }
65
+ },
66
+ {
67
+ name: "search",
68
+ description: "Semantic search over one root or the full local Qdrant store.",
69
+ inputSchema: {
70
+ type: "object",
71
+ properties: {
72
+ query: { type: "string", description: "Natural language search query." },
73
+ rootIds: {
74
+ type: "array",
75
+ items: { type: "string" },
76
+ description: "Optional list of root IDs to restrict search."
77
+ },
78
+ limit: { type: "number", description: "Maximum result count. Default 5." },
79
+ pathSubstring: { type: "string", description: "Optional file path substring filter." }
80
+ },
81
+ required: ["query"]
82
+ }
83
+ },
84
+ {
85
+ name: "search_context",
86
+ description: "Search and return a bundled context payload with top chunks for direct agent grounding.",
87
+ inputSchema: {
88
+ type: "object",
89
+ properties: {
90
+ query: { type: "string", description: "Natural language search query." },
91
+ rootIds: {
92
+ type: "array",
93
+ items: { type: "string" },
94
+ description: "Optional list of root IDs to restrict search."
95
+ },
96
+ limit: { type: "number", description: "Maximum search hits to consider. Default 8." },
97
+ maxChars: { type: "number", description: "Maximum total characters of bundled context. Default 12000." },
98
+ pathSubstring: { type: "string", description: "Optional file path substring filter." }
99
+ },
100
+ required: ["query"]
101
+ }
102
+ },
103
+ {
104
+ name: "get_chunks",
105
+ description: "Fetch exact indexed chunks by id after a search step.",
106
+ inputSchema: {
107
+ type: "object",
108
+ properties: {
109
+ ids: {
110
+ type: "array",
111
+ items: { type: "string" },
112
+ description: "Chunk ids returned by search."
113
+ }
114
+ },
115
+ required: ["ids"]
116
+ }
117
+ },
118
+ {
119
+ name: "list_roots",
120
+ description: "List approved indexed roots.",
121
+ inputSchema: {
122
+ type: "object",
123
+ properties: {}
124
+ }
125
+ },
126
+ {
127
+ name: "delete_root",
128
+ description: "Delete one indexed root from config and vector storage.",
129
+ inputSchema: {
130
+ type: "object",
131
+ properties: {
132
+ rootId: { type: "string", description: "Root identifier returned by list_roots." }
133
+ },
134
+ required: ["rootId"]
135
+ }
136
+ },
137
+ {
138
+ name: "store_status",
139
+ description: "Inspect Qdrant collection status for the local index.",
140
+ inputSchema: {
141
+ type: "object",
142
+ properties: {}
143
+ }
144
+ }
145
+ ]
146
+ }));
147
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
148
+ try {
149
+ switch (request.params.name) {
150
+ case "index_path": {
151
+ const args = request.params.arguments;
152
+ if (!args?.path) {
153
+ throw new OwnSearchError("`path` is required.");
154
+ }
155
+ const result = await indexPath(args.path, { name: args.name });
156
+ return asText(result);
157
+ }
158
+ case "search": {
159
+ const args = request.params.arguments;
160
+ if (!args?.query) {
161
+ throw new OwnSearchError("`query` is required.");
162
+ }
163
+ const vector = await embedQuery(args.query);
164
+ const store = await createStore();
165
+ const hits = await store.search(
166
+ vector,
167
+ {
168
+ rootIds: args.rootIds,
169
+ pathSubstring: args.pathSubstring
170
+ },
171
+ Math.max(1, Math.min(args.limit ?? 5, 20))
172
+ );
173
+ return asText({
174
+ query: args.query,
175
+ hits
176
+ });
177
+ }
178
+ case "search_context": {
179
+ const args = request.params.arguments;
180
+ if (!args?.query) {
181
+ throw new OwnSearchError("`query` is required.");
182
+ }
183
+ const vector = await embedQuery(args.query);
184
+ const store = await createStore();
185
+ const hits = await store.search(
186
+ vector,
187
+ {
188
+ rootIds: args.rootIds,
189
+ pathSubstring: args.pathSubstring
190
+ },
191
+ Math.max(1, Math.min(args.limit ?? 8, 20))
192
+ );
193
+ return asText(buildContextBundle(args.query, hits, Math.max(500, args.maxChars ?? 12e3)));
194
+ }
195
+ case "get_chunks": {
196
+ const args = request.params.arguments;
197
+ if (!args?.ids?.length) {
198
+ throw new OwnSearchError("`ids` is required.");
199
+ }
200
+ const store = await createStore();
201
+ const chunks = await store.getChunks(args.ids);
202
+ return asText({ chunks });
203
+ }
204
+ case "list_roots": {
205
+ const config = await loadConfig();
206
+ return asText({ roots: config.roots });
207
+ }
208
+ case "delete_root": {
209
+ const args = request.params.arguments;
210
+ if (!args?.rootId) {
211
+ throw new OwnSearchError("`rootId` is required.");
212
+ }
213
+ const root = await findRoot(args.rootId);
214
+ if (!root) {
215
+ throw new OwnSearchError(`Unknown root: ${args.rootId}`);
216
+ }
217
+ const store = await createStore();
218
+ await store.deleteRoot(root.id);
219
+ await deleteRootDefinition(root.id);
220
+ return asText({
221
+ deleted: true,
222
+ root
223
+ });
224
+ }
225
+ case "store_status": {
226
+ const store = await createStore();
227
+ return asText(await store.getStatus());
228
+ }
229
+ default:
230
+ throw new OwnSearchError(`Unknown tool: ${request.params.name}`);
231
+ }
232
+ } catch (error) {
233
+ return asError(error);
234
+ }
235
+ });
236
+ async function main() {
237
+ const transport = new StdioServerTransport();
238
+ await server.connect(transport);
239
+ }
240
+ main().catch((error) => {
241
+ console.error(error);
242
+ process.exitCode = 1;
243
+ });
package/package.json ADDED
@@ -0,0 +1,61 @@
1
+ {
2
+ "name": "@grumppie/ownsearch",
3
+ "version": "0.1.3",
4
+ "description": "Text-first local document search MCP server backed by Gemini embeddings and Qdrant.",
5
+ "type": "module",
6
+ "bin": {
7
+ "ownsearch": "./dist/cli.js"
8
+ },
9
+ "files": [
10
+ "dist",
11
+ "README.md"
12
+ ],
13
+ "scripts": {
14
+ "build": "tsup src/cli.ts src/mcp/server.ts --format esm --dts --clean --external pdf-parse",
15
+ "dev": "tsx src/cli.ts",
16
+ "prepare": "npm run build",
17
+ "prepublishOnly": "npm run typecheck && npm run build",
18
+ "serve-mcp": "tsx src/mcp/server.ts",
19
+ "typecheck": "tsc --noEmit"
20
+ },
21
+ "keywords": [
22
+ "mcp",
23
+ "search",
24
+ "embeddings",
25
+ "gemini",
26
+ "qdrant",
27
+ "rag"
28
+ ],
29
+ "author": "OwnSearch contributors",
30
+ "license": "MIT",
31
+ "repository": {
32
+ "type": "git",
33
+ "url": "https://github.com/Grumppie/OwnSearch.git"
34
+ },
35
+ "homepage": "https://github.com/Grumppie/OwnSearch#readme",
36
+ "bugs": {
37
+ "url": "https://github.com/Grumppie/OwnSearch/issues"
38
+ },
39
+ "publishConfig": {
40
+ "access": "public"
41
+ },
42
+ "engines": {
43
+ "node": ">=20"
44
+ },
45
+ "dependencies": {
46
+ "@google/genai": "^1.46.0",
47
+ "@modelcontextprotocol/sdk": "^1.27.1",
48
+ "@qdrant/js-client-rest": "^1.17.0",
49
+ "commander": "^14.0.1",
50
+ "dotenv": "^17.3.1",
51
+ "pdf-parse": "^2.4.5",
52
+ "zod": "^3.25.76"
53
+ },
54
+ "devDependencies": {
55
+ "@types/node": "^24.6.0",
56
+ "@types/pdf-parse": "^1.1.5",
57
+ "tsup": "^8.5.0",
58
+ "tsx": "^4.20.6",
59
+ "typescript": "^5.9.3"
60
+ }
61
+ }