embarsy-qdrant-mcp 0.1.4 → 0.1.5
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/indexer.js +27 -1
- package/dist/qdrant.js +14 -0
- package/package.json +2 -2
package/dist/indexer.js
CHANGED
|
@@ -3,7 +3,7 @@ import path from "node:path";
|
|
|
3
3
|
import { embedBatch } from "./embeddings.js";
|
|
4
4
|
import { chunkFile } from "./chunker.js";
|
|
5
5
|
import { discoverFiles } from "./walk.js";
|
|
6
|
-
import { ensureCollection, upsertPoints, deleteByFilePath, scrollFileHashes, } from "./qdrant.js";
|
|
6
|
+
import { ensureCollection, upsertPoints, deleteByFilePath, scrollFileHashes, setWorkspacePayload, } from "./qdrant.js";
|
|
7
7
|
import { sha256, pointId, languageForExtension } from "./util.js";
|
|
8
8
|
/** Crude binary guard: a NUL byte early in the file means it isn't source text. */
|
|
9
9
|
function looksBinary(s) {
|
|
@@ -29,10 +29,31 @@ function maxLineLength(s) {
|
|
|
29
29
|
}
|
|
30
30
|
return cur > max ? cur : max;
|
|
31
31
|
}
|
|
32
|
+
/** Directories that hold source but never name a project. */
|
|
33
|
+
const CONTAINER_DIRS = new Set([
|
|
34
|
+
"src", "source", "sources", "lib", "libs", "app", "apps",
|
|
35
|
+
"internal", "cmd", "pkg", "test", "tests",
|
|
36
|
+
]);
|
|
37
|
+
/** Project name to store with every chunk — the indexed folder's name, except that
|
|
38
|
+
* indexing `~/code/myproj/src` should still say `myproj` and not `src`. Only the NAME
|
|
39
|
+
* travels: the absolute path would put the user's home directory into every point. */
|
|
40
|
+
export function workspaceNameFor(rootAbs) {
|
|
41
|
+
let current = rootAbs;
|
|
42
|
+
for (;;) {
|
|
43
|
+
const name = path.basename(current);
|
|
44
|
+
const parent = path.dirname(current);
|
|
45
|
+
if (!name || parent === current)
|
|
46
|
+
return path.basename(rootAbs);
|
|
47
|
+
if (!CONTAINER_DIRS.has(name.toLowerCase()))
|
|
48
|
+
return name;
|
|
49
|
+
current = parent;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
32
52
|
/** Full/incremental index of a directory into the configured Qdrant collection. */
|
|
33
53
|
export async function indexRepo(root, cfg, opts = {}) {
|
|
34
54
|
const log = opts.onProgress ?? (() => { });
|
|
35
55
|
const rootAbs = path.resolve(root);
|
|
56
|
+
const workspace = workspaceNameFor(rootAbs);
|
|
36
57
|
await ensureCollection(cfg);
|
|
37
58
|
const files = await discoverFiles(rootAbs, cfg);
|
|
38
59
|
log(`Found ${files.length} indexable files in ${rootAbs}`);
|
|
@@ -87,6 +108,7 @@ export async function indexRepo(root, cfg, opts = {}) {
|
|
|
87
108
|
text: `${file.rel}\n\n${c.text}`, // path gives the embedder useful context
|
|
88
109
|
payload: {
|
|
89
110
|
file_path: file.rel,
|
|
111
|
+
workspace,
|
|
90
112
|
language,
|
|
91
113
|
start_line: c.startLine,
|
|
92
114
|
end_line: c.endLine,
|
|
@@ -110,5 +132,9 @@ export async function indexRepo(root, cfg, opts = {}) {
|
|
|
110
132
|
result.removed++;
|
|
111
133
|
}
|
|
112
134
|
}
|
|
135
|
+
// Backfills points this run skipped as unchanged, and points indexed by an older bridge
|
|
136
|
+
// that never wrote the field — without it the name would stay missing until every file
|
|
137
|
+
// in the project happened to change.
|
|
138
|
+
await setWorkspacePayload(cfg, workspace);
|
|
113
139
|
return result;
|
|
114
140
|
}
|
package/dist/qdrant.js
CHANGED
|
@@ -58,6 +58,20 @@ export async function deleteByFilePath(cfg, filePath) {
|
|
|
58
58
|
body: JSON.stringify({ filter: { must: [{ key: "file_path", match: { value: filePath } }] } }),
|
|
59
59
|
}, { label: "delete points" });
|
|
60
60
|
}
|
|
61
|
+
/** Stamp `workspace` onto EVERY point in the collection.
|
|
62
|
+
*
|
|
63
|
+
* Setting it only on freshly embedded points would leave it off almost everything: an
|
|
64
|
+
* index run skips unchanged files, so on the second run nearly no point is rewritten.
|
|
65
|
+
* This is one payload write over the whole collection — no re-embedding — which also
|
|
66
|
+
* backfills collections indexed before the field existed. */
|
|
67
|
+
export async function setWorkspacePayload(cfg, workspace) {
|
|
68
|
+
await requestJSON(`${base(cfg)}/points/payload?wait=true`, {
|
|
69
|
+
method: "POST",
|
|
70
|
+
headers: headers(cfg),
|
|
71
|
+
// An empty filter selects every point in the collection.
|
|
72
|
+
body: JSON.stringify({ payload: { workspace }, filter: {} }),
|
|
73
|
+
}, { label: "set workspace payload" });
|
|
74
|
+
}
|
|
61
75
|
/** Map of file_path -> file_hash for every indexed file, used to skip unchanged files. */
|
|
62
76
|
export async function scrollFileHashes(cfg) {
|
|
63
77
|
const out = new Map();
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "embarsy-qdrant-mcp",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "Codebase-indexing MCP server for Embarsy
|
|
3
|
+
"version": "0.1.5",
|
|
4
|
+
"description": "Codebase-indexing MCP server for Embarsy \u2014 semantic code search over a local Qdrant + OpenAI-compatible embeddings stack. Routes embeddings and Qdrant through Embarsy's proxy so auth and Monitoring counters just work.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"embarsy-index": "dist/bin/index.js",
|