@sackville-mcp/core 0.0.1-alpha.0

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/LICENSE ADDED
@@ -0,0 +1,201 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work
38
+ (an example is provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship. For the purposes
44
+ of this License, Derivative Works shall not include works that remain
45
+ separable from, or merely link (or bind by name) to the interfaces of,
46
+ the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including
49
+ the original version of the Work and any modifications or additions
50
+ to that Work or Derivative Works thereof, that is intentionally
51
+ submitted to Licensor for inclusion in the Work by the copyright owner
52
+ or by an individual or Legal Entity authorized to submit on behalf of
53
+ the copyright owner. For the purposes of this definition, "submitted"
54
+ means any form of electronic, verbal, or written communication sent
55
+ to the Licensor or its representatives, including but not limited to
56
+ communication on electronic mailing lists, source code control systems,
57
+ and issue tracking systems that are managed by, or on behalf of, the
58
+ Licensor for the purpose of discussing and improving the Work, but
59
+ excluding communication that is conspicuously marked or otherwise
60
+ designated in writing by the copyright owner as "Not a Contribution."
61
+
62
+ "Contributor" shall mean Licensor and any individual or Legal Entity
63
+ on behalf of whom a Contribution has been received by Licensor and
64
+ subsequently incorporated within the Work.
65
+
66
+ 2. Grant of Copyright License. Subject to the terms and conditions of
67
+ this License, each Contributor hereby grants to You a perpetual,
68
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+ copyright license to reproduce, prepare Derivative Works of,
70
+ publicly display, publicly perform, sublicense, and distribute the
71
+ Work and such Derivative Works in Source or Object form.
72
+
73
+ 3. Grant of Patent License. Subject to the terms and conditions of
74
+ this License, each Contributor hereby grants to You a perpetual,
75
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+ (except as stated in this section) patent license to make, have made,
77
+ use, offer to sell, sell, import, and otherwise transfer the Work,
78
+ where such license applies only to those patent claims licensable
79
+ by such Contributor that are necessarily infringed by their
80
+ Contribution(s) alone or by combination of their Contribution(s)
81
+ with the Work to which such Contribution(s) was submitted. If You
82
+ institute patent litigation against any entity (including a
83
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+ or a Contribution incorporated within the Work constitutes direct
85
+ or contributory patent infringement, then any patent licenses
86
+ granted to You under this License for that Work shall terminate
87
+ as of the date such litigation is filed.
88
+
89
+ 4. Redistribution. You may reproduce and distribute copies of the
90
+ Work or Derivative Works thereof in any medium, with or without
91
+ modifications, and in Source or Object form, provided that You
92
+ meet the following conditions:
93
+
94
+ (a) You must give any other recipients of the Work or Derivative
95
+ Works a copy of this License; and
96
+
97
+ (b) You must cause any modified files to carry prominent notices
98
+ stating that You changed the files; and
99
+
100
+ (c) You must retain, in the Source form of any Derivative Works
101
+ that You distribute, all copyright, patent, trademark, and
102
+ attribution notices from the Source form of the Work,
103
+ excluding those notices that do not pertain to any part of
104
+ the Derivative Works; and
105
+
106
+ (d) If the Work includes a "NOTICE" text file as part of its
107
+ distribution, then any Derivative Works that You distribute must
108
+ include a readable copy of the attribution notices contained
109
+ within such NOTICE file, excluding those notices that do not
110
+ pertain to any part of the Derivative Works, in at least one
111
+ of the following places: within a NOTICE text file distributed
112
+ as part of the Derivative Works; within the Source form or
113
+ documentation, if provided along with the Derivative Works; or,
114
+ within a display generated by the Derivative Works, if and
115
+ wherever such third-party notices normally appear. The contents
116
+ of the NOTICE file are for informational purposes only and
117
+ do not modify the License. You may add Your own attribution
118
+ notices within Derivative Works that You distribute, alongside
119
+ or as an addendum to the NOTICE text from the Work, provided
120
+ that such additional attribution notices cannot be construed
121
+ as modifying the License.
122
+
123
+ You may add Your own copyright statement to Your modifications and
124
+ may provide additional or different license terms and conditions
125
+ for use, reproduction, or distribution of Your modifications, or
126
+ for any such Derivative Works as a whole, provided Your use,
127
+ reproduction, and distribution of the Work otherwise complies with
128
+ the conditions stated in this License.
129
+
130
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131
+ any Contribution intentionally submitted for inclusion in the Work
132
+ by You to the Licensor shall be under the terms and conditions of
133
+ this License, without any additional terms or conditions.
134
+ Notwithstanding the above, nothing herein shall supersede or modify
135
+ the terms of any separate license agreement you may have executed
136
+ with Licensor regarding such Contributions.
137
+
138
+ 6. Trademarks. This License does not grant permission to use the trade
139
+ names, trademarks, service marks, or product names of the Licensor,
140
+ except as required for reasonable and customary use in describing the
141
+ origin of the Work and reproducing the content of the NOTICE file.
142
+
143
+ 7. Disclaimer of Warranty. Unless required by applicable law or
144
+ agreed to in writing, Licensor provides the Work (and each
145
+ Contributor provides its Contributions) on an "AS IS" BASIS,
146
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
+ implied, including, without limitation, any warranties or conditions
148
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
+ PARTICULAR PURPOSE. You are solely responsible for determining the
150
+ appropriateness of using or redistributing the Work and assume any
151
+ risks associated with Your exercise of permissions under this License.
152
+
153
+ 8. Limitation of Liability. In no event and under no legal theory,
154
+ whether in tort (including negligence), contract, or otherwise,
155
+ unless required by applicable law (such as deliberate and grossly
156
+ negligent acts) or agreed to in writing, shall any Contributor be
157
+ liable to You for damages, including any direct, indirect, special,
158
+ incidental, or consequential damages of any character arising as a
159
+ result of this License or out of the use or inability to use the
160
+ Work (including but not limited to damages for loss of goodwill,
161
+ work stoppage, computer failure or malfunction, or any and all
162
+ other commercial damages or losses), even if such Contributor
163
+ has been advised of the possibility of such damages.
164
+
165
+ 9. Accepting Warranty or Additional Liability. While redistributing
166
+ the Work or Derivative Works thereof, You may choose to offer,
167
+ and charge a fee for, acceptance of support, warranty, indemnity,
168
+ or other liability obligations and/or rights consistent with this
169
+ License. However, in accepting such obligations, You may act only
170
+ on Your own behalf and on Your sole responsibility, not on behalf
171
+ of any other Contributor, and only if You agree to indemnify,
172
+ defend, and hold each Contributor harmless for any liability
173
+ incurred by, or claims asserted against, such Contributor by reason
174
+ of your accepting any such warranty or additional liability.
175
+
176
+ END OF TERMS AND CONDITIONS
177
+
178
+ APPENDIX: How to apply the Apache License to your work.
179
+
180
+ To apply the Apache License to your work, attach the following
181
+ boilerplate notice, with the fields enclosed by brackets "[]"
182
+ replaced with your own identifying information. (Don't include
183
+ the brackets!) The text should be enclosed in the appropriate
184
+ comment syntax for the file format. We also recommend that a
185
+ file or class name and description of purpose be included on the
186
+ same "printed page" as the copyright notice for easier
187
+ identification within third-party archives.
188
+
189
+ Copyright 2026 Curtis Autery
190
+
191
+ Licensed under the Apache License, Version 2.0 (the "License");
192
+ you may not use this file except in compliance with the License.
193
+ You may obtain a copy of the License at
194
+
195
+ http://www.apache.org/licenses/LICENSE-2.0
196
+
197
+ Unless required by applicable law or agreed to in writing, software
198
+ distributed under the License is distributed on an "AS IS" BASIS,
199
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200
+ See the License for the specific language governing permissions and
201
+ limitations under the License.
@@ -0,0 +1,138 @@
1
+ import Database from "better-sqlite3";
2
+
3
+ //#region src/types.d.ts
4
+ /** Metadata read from a Sackville index's `sackville_meta` table. */
5
+ interface SchemaMeta {
6
+ schemaVersion: number;
7
+ embedModel: string;
8
+ embedDim: number;
9
+ builtAt: string | null;
10
+ builderVersion: string | null;
11
+ }
12
+ /** A full documentation fragment — the complete record for one `docs` row. */
13
+ interface DocFragment {
14
+ id: number;
15
+ library: string;
16
+ version: string;
17
+ title: string;
18
+ symbol: string | null;
19
+ type: string | null;
20
+ headingPath: string | null;
21
+ url: string | null;
22
+ attribution: string | null;
23
+ body: string;
24
+ }
25
+ /** Filters and limits for a docs search. */
26
+ interface SearchOptions {
27
+ library?: string;
28
+ version?: string;
29
+ type?: string;
30
+ /** Defaults to 8, clamped to 25. */
31
+ limit?: number;
32
+ /**
33
+ * Optional query embedding (length must equal the schema's embed_dim). When
34
+ * provided, results fuse FTS (bm25) with vector KNN via reciprocal rank
35
+ * fusion; when omitted, search is full-text only.
36
+ */
37
+ queryVector?: number[];
38
+ }
39
+ /** One search hit. Compact by design — full bodies are fetched separately. */
40
+ interface SearchResult {
41
+ id: number;
42
+ title: string;
43
+ symbol: string | null;
44
+ type: string | null;
45
+ library: string;
46
+ version: string;
47
+ /** Relevance score; higher is better. Results are pre-sorted best-first. */
48
+ score: number;
49
+ /** Short highlighted excerpt from the body. */
50
+ snippet: string;
51
+ }
52
+ //#endregion
53
+ //#region src/db.d.ts
54
+ interface OpenOptions {
55
+ /** Open read-only (default true — the server never mutates an index). */
56
+ readonly?: boolean;
57
+ }
58
+ /**
59
+ * Open a Sackville index, load the sqlite-vec extension, and assert the schema
60
+ * version matches what this build expects. Throws on mismatch so a stale or
61
+ * foreign index can never be served silently.
62
+ */
63
+ declare function openDb(path: string, options?: OpenOptions): Database.Database;
64
+ /** Read the `sackville_meta` key/value table into a typed object. */
65
+ declare function readMeta(db: Database.Database): SchemaMeta;
66
+ //#endregion
67
+ //#region src/doc.d.ts
68
+ /**
69
+ * Fetch a full documentation fragment by id. This is the one place full body
70
+ * text is returned (the MCP `get_doc` tool and the `sackville://doc/{id}`
71
+ * resource wrap it); search results stay compact. Returns undefined if absent.
72
+ */
73
+ declare function getDoc(db: Database.Database, id: number): DocFragment | undefined;
74
+ //#endregion
75
+ //#region src/project.d.ts
76
+ /** Package ecosystems Sackville can detect an installed version in. */
77
+ type Ecosystem = 'node' | 'python' | 'ruby';
78
+ /** Where a detected version came from (most authoritative first, per ecosystem). */
79
+ type VersionSource = 'node_modules' | 'package-lock.json' | 'package.json' | 'python:dist-info' | 'python:lock' | 'python:requirements' | 'python:pyproject' | 'ruby:Gemfile.lock' | 'ruby:Gemfile' | 'none';
80
+ interface DetectedVersion {
81
+ /** Concrete installed version, or a declared range/constraint, or null. */
82
+ version: string | null;
83
+ source: VersionSource;
84
+ }
85
+ interface DetectOptions {
86
+ /** Restrict detection to one ecosystem. Omit to auto-probe node → python → ruby. */
87
+ ecosystem?: Ecosystem;
88
+ }
89
+ /**
90
+ * Detect the installed version of a package in a project directory.
91
+ *
92
+ * With an explicit `ecosystem`, runs only that detector. Otherwise auto-probes
93
+ * node → python → ruby and returns the first hit. Each ecosystem prefers the
94
+ * concrete installed version, then a lockfile, then the declared range — the
95
+ * result feeds `resolveVersion`, which accepts concrete versions and ranges.
96
+ */
97
+ declare function detectInstalledVersion(projectDir: string, pkg: string, opts?: DetectOptions): DetectedVersion;
98
+ //#endregion
99
+ //#region src/schema.d.ts
100
+ declare const EXPECTED_SCHEMA_VERSION = 1;
101
+ declare const EXPECTED_EMBED_DIM = 384;
102
+ declare const EXPECTED_EMBED_MODEL = "bge-small-en-v1.5";
103
+ //#endregion
104
+ //#region src/search.d.ts
105
+ /**
106
+ * Search the docs index. Full-text (FTS5/bm25) always runs; when `queryVector`
107
+ * is supplied, vector KNN (sqlite-vec) is fused with it via reciprocal rank
108
+ * fusion. Library/version/type filters apply to both halves. Results are sorted
109
+ * best-first; bodies are never returned (fetch them via getDoc).
110
+ */
111
+ declare function searchDocs(db: Database.Database, query: string, options?: SearchOptions): SearchResult[];
112
+ //#endregion
113
+ //#region src/version.d.ts
114
+ /** Outcome of resolving an installed version to an available doc release. */
115
+ interface VersionResolution {
116
+ /** The available version string to use, or null if none is acceptable. */
117
+ resolved: string | null;
118
+ /** True when the request was exactly satisfied (not a same-major fallback). */
119
+ exact: boolean;
120
+ /** Human/agent-readable explanation of the decision. */
121
+ note: string;
122
+ /** Available versions, newest first. */
123
+ available: string[];
124
+ }
125
+ /** Distinct versions of a library present in the index, newest first. */
126
+ declare function listVersions(db: Database.Database, library: string): string[];
127
+ /**
128
+ * Resolve a requested/installed version (exact, range, or bare major) to the
129
+ * best available doc release.
130
+ *
131
+ * Policy (ARCHITECTURE §7.2): prefer an exactly-satisfying release; otherwise
132
+ * fall back to the newest release sharing the requested MAJOR and flag it;
133
+ * never silently return a wrong-major release.
134
+ */
135
+ declare function resolveVersion(available: string[], requested: string): VersionResolution;
136
+ //#endregion
137
+ export { type DetectOptions, type DetectedVersion, type DocFragment, EXPECTED_EMBED_DIM, EXPECTED_EMBED_MODEL, EXPECTED_SCHEMA_VERSION, type Ecosystem, type OpenOptions, type SchemaMeta, type SearchOptions, type SearchResult, type VersionResolution, type VersionSource, detectInstalledVersion, getDoc, listVersions, openDb, readMeta, resolveVersion, searchDocs };
138
+ //# sourceMappingURL=index.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/types.ts","../src/db.ts","../src/doc.ts","../src/project.ts","../src/schema.ts","../src/search.ts","../src/version.ts"],"mappings":";;;;UACiB,UAAA;EACf,aAAA;EACA,UAAA;EACA,QAAA;EACA,OAAA;EACA,cAAA;AAAA;;UAIe,WAAA;EACf,EAAA;EACA,OAAA;EACA,OAAA;EACA,KAAA;EACA,MAAA;EACA,IAAA;EACA,WAAA;EACA,GAAA;EACA,WAAA;EACA,IAAA;AAAA;;UAIe,aAAA;EACf,OAAA;EACA,OAAA;EACA,IAAA;EARA;EAUA,KAAA;EATI;AAAA;AAIN;;;EAWE,WAAA;AAAA;;UAIe,YAAA;EACf,EAAA;EACA,KAAA;EACA,MAAA;EACA,IAAA;EACA,OAAA;EACA,OAAA;;EAEA,KAAA;EAPA;EASA,OAAA;AAAA;;;UC5Ce,WAAA;EDJA;ECMf,QAAQ;AAAA;;;;;;iBAQM,MAAA,CAAO,IAAA,UAAc,OAAA,GAAS,WAAA,GAAmB,QAAA,CAAS,QAAQ;;iBAqBlE,QAAA,CAAS,EAAA,EAAI,QAAA,CAAS,QAAA,GAAW,UAAU;;;;ADnC3D;;;;iBEOgB,MAAA,CAAO,EAAA,EAAI,QAAA,CAAS,QAAA,EAAU,EAAA,WAAa,WAAW;;;;KCJ1D,SAAA;;KAGA,aAAA;AAAA,UAYK,eAAA;;EAEf,OAAA;EACA,MAAA,EAAQ,aAAa;AAAA;AAAA,UAGN,aAAA;EHpBf;EGsBA,SAAA,GAAY,SAAS;AAAA;AHrBP;AAIhB;;;;;;;AAJgB,iBGgEA,sBAAA,CACd,UAAA,UACA,GAAA,UACA,IAAA,GAAM,aAAA,GACL,eAAe;;;cCvEL,uBAAA;AAAA,cACA,kBAAA;AAAA,cACA,oBAAA;;;;AJJb;;;;;iBKsCgB,UAAA,CACd,EAAA,EAAI,QAAA,CAAS,QAAA,EACb,KAAA,UACA,OAAA,GAAS,aAAA,GACR,YAAA;;;;UCvCc,iBAAA;ENHA;EMKf,QAAA;;EAEA,KAAA;ENNA;EMQA,IAAA;ENNA;EMQA,SAAA;AAAA;;iBAIc,YAAA,CAAa,EAAA,EAAI,QAAA,CAAS,QAAQ,EAAE,OAAA;ANNpD;;;;;;;;AAAA,iBM8BgB,cAAA,CAAe,SAAA,YAAqB,SAAA,WAAoB,iBAAiB"}
package/dist/index.mjs ADDED
@@ -0,0 +1,465 @@
1
+ import Database from "better-sqlite3";
2
+ import * as sqliteVec from "sqlite-vec";
3
+ import { readFileSync, readdirSync } from "node:fs";
4
+ import { join } from "node:path";
5
+ import semver from "semver";
6
+ //#region src/schema.ts
7
+ const EXPECTED_SCHEMA_VERSION = 1;
8
+ const EXPECTED_EMBED_DIM = 384;
9
+ const EXPECTED_EMBED_MODEL = "bge-small-en-v1.5";
10
+ //#endregion
11
+ //#region src/db.ts
12
+ /**
13
+ * Open a Sackville index, load the sqlite-vec extension, and assert the schema
14
+ * version matches what this build expects. Throws on mismatch so a stale or
15
+ * foreign index can never be served silently.
16
+ */
17
+ function openDb(path, options = {}) {
18
+ const db = new Database(path, { readonly: options.readonly ?? true });
19
+ sqliteVec.load(db);
20
+ const meta = readMeta(db);
21
+ if (meta.schemaVersion !== 1) {
22
+ db.close();
23
+ throw new Error(`Sackville index schema mismatch at ${path}: file is v${meta.schemaVersion}, this build expects v1. Rebuild the index.`);
24
+ }
25
+ return db;
26
+ }
27
+ /** Read the `sackville_meta` key/value table into a typed object. */
28
+ function readMeta(db) {
29
+ const rows = db.prepare("SELECT key, value FROM sackville_meta").all();
30
+ const map = new Map(rows.map((r) => [r.key, r.value]));
31
+ return {
32
+ schemaVersion: Number(map.get("schema_version")),
33
+ embedModel: map.get("embed_model") ?? "",
34
+ embedDim: Number(map.get("embed_dim")),
35
+ builtAt: map.get("built_at") ?? null,
36
+ builderVersion: map.get("builder_version") ?? null
37
+ };
38
+ }
39
+ //#endregion
40
+ //#region src/doc.ts
41
+ /**
42
+ * Fetch a full documentation fragment by id. This is the one place full body
43
+ * text is returned (the MCP `get_doc` tool and the `sackville://doc/{id}`
44
+ * resource wrap it); search results stay compact. Returns undefined if absent.
45
+ */
46
+ function getDoc(db, id) {
47
+ return db.prepare(`
48
+ SELECT id,
49
+ library,
50
+ version,
51
+ title,
52
+ symbol,
53
+ type,
54
+ heading_path AS headingPath,
55
+ url,
56
+ attribution,
57
+ body
58
+ FROM docs
59
+ WHERE id = ?
60
+ `).get(id);
61
+ }
62
+ //#endregion
63
+ //#region src/project.ts
64
+ const NONE = {
65
+ version: null,
66
+ source: "none"
67
+ };
68
+ function readText(path) {
69
+ try {
70
+ return readFileSync(path, "utf8");
71
+ } catch {
72
+ return;
73
+ }
74
+ }
75
+ function readJson(path) {
76
+ const text = readText(path);
77
+ if (text === void 0) return void 0;
78
+ try {
79
+ return JSON.parse(text);
80
+ } catch {
81
+ return;
82
+ }
83
+ }
84
+ function readdirSafe(path) {
85
+ try {
86
+ return readdirSync(path);
87
+ } catch {
88
+ return [];
89
+ }
90
+ }
91
+ function escapeRe(s) {
92
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
93
+ }
94
+ /**
95
+ * Detect the installed version of a package in a project directory.
96
+ *
97
+ * With an explicit `ecosystem`, runs only that detector. Otherwise auto-probes
98
+ * node → python → ruby and returns the first hit. Each ecosystem prefers the
99
+ * concrete installed version, then a lockfile, then the declared range — the
100
+ * result feeds `resolveVersion`, which accepts concrete versions and ranges.
101
+ */
102
+ function detectInstalledVersion(projectDir, pkg, opts = {}) {
103
+ const detectors = {
104
+ node: detectNode,
105
+ python: detectPython,
106
+ ruby: detectRuby
107
+ };
108
+ if (opts.ecosystem) return detectors[opts.ecosystem](projectDir, pkg);
109
+ for (const eco of [
110
+ "node",
111
+ "python",
112
+ "ruby"
113
+ ]) {
114
+ const found = detectors[eco](projectDir, pkg);
115
+ if (found.version !== null) return found;
116
+ }
117
+ return NONE;
118
+ }
119
+ function depRange(manifest, pkg) {
120
+ for (const field of [
121
+ "dependencies",
122
+ "devDependencies",
123
+ "peerDependencies",
124
+ "optionalDependencies"
125
+ ]) {
126
+ const deps = manifest[field];
127
+ if (deps?.[pkg]) return deps[pkg];
128
+ }
129
+ }
130
+ function detectNode(dir, pkg) {
131
+ const installed = readJson(join(dir, "node_modules", pkg, "package.json"));
132
+ if (typeof installed?.version === "string") return {
133
+ version: installed.version,
134
+ source: "node_modules"
135
+ };
136
+ const lock = readJson(join(dir, "package-lock.json"));
137
+ if (lock) {
138
+ const packages = lock.packages;
139
+ const deps = lock.dependencies;
140
+ const locked = packages?.[`node_modules/${pkg}`]?.version ?? deps?.[pkg]?.version;
141
+ if (locked) return {
142
+ version: locked,
143
+ source: "package-lock.json"
144
+ };
145
+ }
146
+ const manifest = readJson(join(dir, "package.json"));
147
+ if (manifest) {
148
+ const range = depRange(manifest, pkg);
149
+ if (range) return {
150
+ version: range,
151
+ source: "package.json"
152
+ };
153
+ }
154
+ return NONE;
155
+ }
156
+ /** PEP 503 name normalization: lowercase, runs of `-_.` collapse to a single `-`. */
157
+ function canonPy(name) {
158
+ return name.toLowerCase().replace(/[-_.]+/g, "-");
159
+ }
160
+ /** Candidate site-packages roots inside a project's virtualenv(s). */
161
+ function sitePackagesDirs(dir) {
162
+ const out = [];
163
+ for (const venv of [
164
+ ".venv",
165
+ "venv",
166
+ "env"
167
+ ]) {
168
+ const lib = join(dir, venv, "lib");
169
+ for (const py of readdirSafe(lib)) if (py.startsWith("python")) out.push(join(lib, py, "site-packages"));
170
+ out.push(join(dir, venv, "Lib", "site-packages"));
171
+ }
172
+ return out;
173
+ }
174
+ function pythonDistInfo(dir, want) {
175
+ for (const sp of sitePackagesDirs(dir)) for (const entry of readdirSafe(sp)) {
176
+ if (!entry.endsWith(".dist-info")) continue;
177
+ const meta = readText(join(sp, entry, "METADATA"));
178
+ if (!meta) continue;
179
+ const name = /^Name:\s*(.+)$/m.exec(meta)?.[1]?.trim();
180
+ const version = /^Version:\s*(.+)$/m.exec(meta)?.[1]?.trim();
181
+ if (name && version && canonPy(name) === want) return version;
182
+ }
183
+ }
184
+ /** uv.lock / poetry.lock: `[[package]]` blocks with `name`/`version`. */
185
+ function tomlLockVersion(text, want) {
186
+ for (const block of text.split("[[package]]")) {
187
+ const name = /^\s*name\s*=\s*"([^"]+)"/m.exec(block)?.[1];
188
+ if (!name || canonPy(name) !== want) continue;
189
+ const version = /^\s*version\s*=\s*"([^"]+)"/m.exec(block)?.[1];
190
+ if (version) return version;
191
+ }
192
+ }
193
+ /** Pipfile.lock (JSON): `default`/`develop` → `{ pkg: { version: "==x.y" } }`. */
194
+ function pipfileLockVersion(text, want) {
195
+ let json;
196
+ try {
197
+ json = JSON.parse(text);
198
+ } catch {
199
+ return;
200
+ }
201
+ for (const section of ["default", "develop"]) {
202
+ const deps = json[section];
203
+ if (!deps) continue;
204
+ for (const [name, spec] of Object.entries(deps)) if (canonPy(name) === want && typeof spec?.version === "string") return spec.version.replace(/^==/, "").trim();
205
+ }
206
+ }
207
+ /** Parse a PEP 508 requirement spec into name + constraint (markers dropped). */
208
+ function pep508(spec) {
209
+ const m = /^([A-Za-z0-9_.-]+)\s*(?:\[[^\]]*\])?\s*(.*)$/.exec(spec.trim());
210
+ if (!m?.[1]) return void 0;
211
+ return {
212
+ name: m[1],
213
+ constraint: (m[2] ?? "").split(";")[0]?.trim() ?? ""
214
+ };
215
+ }
216
+ function requirementsVersion(text, want) {
217
+ for (const raw of text.split(/\r?\n/)) {
218
+ const line = (raw.split("#")[0] ?? "").trim();
219
+ if (!line || line.startsWith("-")) continue;
220
+ const parsed = pep508(line);
221
+ if (!parsed || canonPy(parsed.name) !== want) continue;
222
+ if (parsed.constraint.startsWith("==")) return parsed.constraint.slice(2).trim();
223
+ return parsed.constraint || "*";
224
+ }
225
+ }
226
+ function pyprojectVersion(text, want) {
227
+ const arr = /dependencies\s*=\s*\[([\s\S]*?)\]/.exec(text)?.[1];
228
+ if (arr) for (const m of arr.matchAll(/["']([^"']+)["']/g)) {
229
+ const parsed = m[1] ? pep508(m[1]) : void 0;
230
+ if (parsed && canonPy(parsed.name) === want) return parsed.constraint || "*";
231
+ }
232
+ const poetry = /\[tool\.poetry\.dependencies\]([\s\S]*?)(?:\n\[|$)/.exec(text)?.[1];
233
+ if (poetry) for (const raw of poetry.split(/\r?\n/)) {
234
+ const m = /^\s*([A-Za-z0-9_.-]+)\s*=\s*["']([^"']+)["']/.exec(raw);
235
+ if (m?.[1] && canonPy(m[1]) === want) return m[2] ?? "*";
236
+ }
237
+ }
238
+ function detectPython(dir, pkg) {
239
+ const want = canonPy(pkg);
240
+ const installed = pythonDistInfo(dir, want);
241
+ if (installed) return {
242
+ version: installed,
243
+ source: "python:dist-info"
244
+ };
245
+ for (const lock of ["uv.lock", "poetry.lock"]) {
246
+ const text = readText(join(dir, lock));
247
+ const version = text && tomlLockVersion(text, want);
248
+ if (version) return {
249
+ version,
250
+ source: "python:lock"
251
+ };
252
+ }
253
+ const pipfile = readText(join(dir, "Pipfile.lock"));
254
+ const pipfileVersion = pipfile && pipfileLockVersion(pipfile, want);
255
+ if (pipfileVersion) return {
256
+ version: pipfileVersion,
257
+ source: "python:lock"
258
+ };
259
+ const req = readText(join(dir, "requirements.txt"));
260
+ const reqVersion = req && requirementsVersion(req, want);
261
+ if (reqVersion) return {
262
+ version: reqVersion,
263
+ source: "python:requirements"
264
+ };
265
+ const pyproject = readText(join(dir, "pyproject.toml"));
266
+ const pyprojectVer = pyproject && pyprojectVersion(pyproject, want);
267
+ if (pyprojectVer) return {
268
+ version: pyprojectVer,
269
+ source: "python:pyproject"
270
+ };
271
+ return NONE;
272
+ }
273
+ function detectRuby(dir, pkg) {
274
+ const want = pkg.toLowerCase();
275
+ const lock = readText(join(dir, "Gemfile.lock"));
276
+ if (lock) {
277
+ for (const m of lock.matchAll(/^ {4}([A-Za-z0-9_.-]+) \(([^)]+)\)/gm)) if (m[1]?.toLowerCase() === want) return {
278
+ version: m[2] ?? "",
279
+ source: "ruby:Gemfile.lock"
280
+ };
281
+ }
282
+ const gemfile = readText(join(dir, "Gemfile"));
283
+ if (gemfile) {
284
+ const m = new RegExp(`gem\\s+['"]${escapeRe(pkg)}['"]\\s*(?:,\\s*['"]([^'"]+)['"])?`, "i").exec(gemfile);
285
+ if (m) return {
286
+ version: m[1] ?? "*",
287
+ source: "ruby:Gemfile"
288
+ };
289
+ }
290
+ return NONE;
291
+ }
292
+ //#endregion
293
+ //#region src/search.ts
294
+ const DEFAULT_LIMIT = 8;
295
+ const MAX_LIMIT = 25;
296
+ const RRF_K = 60;
297
+ const CANDIDATES = 64;
298
+ /** Build a safe FTS5 MATCH string: quote each alphanumeric token (implicit AND). */
299
+ function ftsMatch(query) {
300
+ const tokens = query.toLowerCase().match(/[\p{L}\p{N}]+/gu);
301
+ if (!tokens || tokens.length === 0) return null;
302
+ return tokens.map((t) => `"${t}"`).join(" ");
303
+ }
304
+ function excerpt(body, max = 160) {
305
+ const collapsed = body.replace(/\s+/g, " ").trim();
306
+ return collapsed.length <= max ? collapsed : `${collapsed.slice(0, max).trimEnd()}…`;
307
+ }
308
+ /**
309
+ * Search the docs index. Full-text (FTS5/bm25) always runs; when `queryVector`
310
+ * is supplied, vector KNN (sqlite-vec) is fused with it via reciprocal rank
311
+ * fusion. Library/version/type filters apply to both halves. Results are sorted
312
+ * best-first; bodies are never returned (fetch them via getDoc).
313
+ */
314
+ function searchDocs(db, query, options = {}) {
315
+ const limit = Math.min(Math.max(options.limit ?? DEFAULT_LIMIT, 1), MAX_LIMIT);
316
+ const useVector = Array.isArray(options.queryVector) && options.queryVector.length === 384;
317
+ const snippets = /* @__PURE__ */ new Map();
318
+ const ftsRanked = runFts(db, query, options, snippets);
319
+ const vecRanked = useVector ? runVector(db, options) : [];
320
+ const scores = /* @__PURE__ */ new Map();
321
+ for (const ranked of [ftsRanked, vecRanked]) ranked.forEach((id, i) => {
322
+ scores.set(id, (scores.get(id) ?? 0) + 1 / (RRF_K + i + 1));
323
+ });
324
+ if (scores.size === 0) return [];
325
+ const topIds = [...scores.entries()].sort((a, b) => b[1] - a[1]).slice(0, limit).map(([id]) => id);
326
+ const placeholders = topIds.map(() => "?").join(",");
327
+ const rows = db.prepare(`SELECT id, title, symbol, type, library, version, body
328
+ FROM docs WHERE id IN (${placeholders})`).all(...topIds);
329
+ const byId = new Map(rows.map((r) => [r.id, r]));
330
+ const results = [];
331
+ for (const id of topIds) {
332
+ const row = byId.get(id);
333
+ if (!row) continue;
334
+ const { body, ...meta } = row;
335
+ results.push({
336
+ ...meta,
337
+ score: scores.get(id) ?? 0,
338
+ snippet: snippets.get(id) ?? excerpt(body)
339
+ });
340
+ }
341
+ return results;
342
+ }
343
+ function runFts(db, query, options, snippets) {
344
+ const match = ftsMatch(query);
345
+ if (!match) return [];
346
+ const clauses = ["docs_fts MATCH @match"];
347
+ const params = {
348
+ match,
349
+ cand: CANDIDATES
350
+ };
351
+ if (options.library !== void 0) {
352
+ clauses.push("d.library = @library");
353
+ params.library = options.library;
354
+ }
355
+ if (options.version !== void 0) {
356
+ clauses.push("d.version = @version");
357
+ params.version = options.version;
358
+ }
359
+ if (options.type !== void 0) {
360
+ clauses.push("d.type = @type");
361
+ params.type = options.type;
362
+ }
363
+ const rows = db.prepare(`SELECT docs_fts.rowid AS id, snippet(docs_fts, 1, '[', ']', '…', 12) AS snippet
364
+ FROM docs_fts JOIN docs d ON d.id = docs_fts.rowid
365
+ WHERE ${clauses.join(" AND ")}
366
+ ORDER BY bm25(docs_fts) LIMIT @cand`).all(params);
367
+ const ids = [];
368
+ for (const r of rows) {
369
+ ids.push(r.id);
370
+ snippets.set(r.id, r.snippet);
371
+ }
372
+ return ids;
373
+ }
374
+ function runVector(db, options) {
375
+ const clauses = ["embedding MATCH @vec", "k = @k"];
376
+ const params = {
377
+ vec: JSON.stringify(options.queryVector),
378
+ k: CANDIDATES
379
+ };
380
+ if (options.library !== void 0) {
381
+ clauses.push("library = @library");
382
+ params.library = options.library;
383
+ }
384
+ if (options.version !== void 0) {
385
+ clauses.push("version = @version");
386
+ params.version = options.version;
387
+ }
388
+ if (options.type !== void 0) {
389
+ clauses.push("type = @type");
390
+ params.type = options.type;
391
+ }
392
+ return db.prepare(`SELECT doc_id AS id FROM docs_vec
393
+ WHERE ${clauses.join(" AND ")}
394
+ ORDER BY distance`).all(params).map((r) => r.id);
395
+ }
396
+ //#endregion
397
+ //#region src/version.ts
398
+ /** Distinct versions of a library present in the index, newest first. */
399
+ function listVersions(db, library) {
400
+ return sortDesc(db.prepare("SELECT DISTINCT version FROM docs WHERE library = ? ORDER BY version").all(library).map((r) => r.version));
401
+ }
402
+ function sortDesc(versions) {
403
+ return [...versions].sort((a, b) => {
404
+ const av = semver.coerce(a);
405
+ const bv = semver.coerce(b);
406
+ if (av && bv) return semver.rcompare(av, bv);
407
+ return b.localeCompare(a);
408
+ });
409
+ }
410
+ /**
411
+ * Resolve a requested/installed version (exact, range, or bare major) to the
412
+ * best available doc release.
413
+ *
414
+ * Policy (ARCHITECTURE §7.2): prefer an exactly-satisfying release; otherwise
415
+ * fall back to the newest release sharing the requested MAJOR and flag it;
416
+ * never silently return a wrong-major release.
417
+ */
418
+ function resolveVersion(available, requested) {
419
+ const sorted = sortDesc(available);
420
+ if (sorted.length === 0) return {
421
+ resolved: null,
422
+ exact: false,
423
+ note: "no versions are indexed",
424
+ available: sorted
425
+ };
426
+ for (const version of sorted) {
427
+ const coerced = semver.coerce(version);
428
+ if (coerced && satisfies(coerced, requested)) return {
429
+ resolved: version,
430
+ exact: true,
431
+ note: `matched ${requested} to ${version}`,
432
+ available: sorted
433
+ };
434
+ }
435
+ const major = requestedMajor(requested);
436
+ if (major !== null) {
437
+ for (const version of sorted) if (semver.coerce(version)?.major === major) return {
438
+ resolved: version,
439
+ exact: false,
440
+ note: `no exact docs for ${requested}; using nearest ${major}.x release ${version}`,
441
+ available: sorted
442
+ };
443
+ }
444
+ return {
445
+ resolved: null,
446
+ exact: false,
447
+ note: `no docs for ${requested}; available versions: ${sorted.join(", ")}`,
448
+ available: sorted
449
+ };
450
+ }
451
+ function satisfies(version, requested) {
452
+ if (semver.validRange(requested)) return semver.satisfies(version, requested);
453
+ const coerced = semver.coerce(requested);
454
+ return coerced ? semver.eq(version, coerced) : false;
455
+ }
456
+ function requestedMajor(requested) {
457
+ const coerced = semver.coerce(requested);
458
+ if (coerced) return coerced.major;
459
+ const min = semver.validRange(requested) ? semver.minVersion(requested) : null;
460
+ return min ? min.major : null;
461
+ }
462
+ //#endregion
463
+ export { EXPECTED_EMBED_DIM, EXPECTED_EMBED_MODEL, EXPECTED_SCHEMA_VERSION, detectInstalledVersion, getDoc, listVersions, openDb, readMeta, resolveVersion, searchDocs };
464
+
465
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../src/schema.ts","../src/db.ts","../src/doc.ts","../src/project.ts","../src/search.ts","../src/version.ts"],"sourcesContent":["// Contract constants. These MUST equal the values in schema/sackville.schema.json\n// (guarded by schema.test.ts). Both languages assert them before operating on an\n// index file, which is what makes the file-as-contract safe.\nexport const EXPECTED_SCHEMA_VERSION = 1\nexport const EXPECTED_EMBED_DIM = 384\nexport const EXPECTED_EMBED_MODEL = 'bge-small-en-v1.5'\n","import Database from 'better-sqlite3'\nimport * as sqliteVec from 'sqlite-vec'\nimport { EXPECTED_SCHEMA_VERSION } from './schema.js'\nimport type { SchemaMeta } from './types.js'\n\nexport interface OpenOptions {\n /** Open read-only (default true — the server never mutates an index). */\n readonly?: boolean\n}\n\n/**\n * Open a Sackville index, load the sqlite-vec extension, and assert the schema\n * version matches what this build expects. Throws on mismatch so a stale or\n * foreign index can never be served silently.\n */\nexport function openDb(path: string, options: OpenOptions = {}): Database.Database {\n const db = new Database(path, { readonly: options.readonly ?? true })\n sqliteVec.load(db)\n\n const meta = readMeta(db)\n if (meta.schemaVersion !== EXPECTED_SCHEMA_VERSION) {\n db.close()\n throw new Error(\n `Sackville index schema mismatch at ${path}: file is v${meta.schemaVersion}, ` +\n `this build expects v${EXPECTED_SCHEMA_VERSION}. Rebuild the index.`,\n )\n }\n return db\n}\n\ninterface MetaRow {\n key: string\n value: string\n}\n\n/** Read the `sackville_meta` key/value table into a typed object. */\nexport function readMeta(db: Database.Database): SchemaMeta {\n const rows = db.prepare('SELECT key, value FROM sackville_meta').all() as MetaRow[]\n const map = new Map(rows.map((r) => [r.key, r.value]))\n return {\n schemaVersion: Number(map.get('schema_version')),\n embedModel: map.get('embed_model') ?? '',\n embedDim: Number(map.get('embed_dim')),\n builtAt: map.get('built_at') ?? null,\n builderVersion: map.get('builder_version') ?? null,\n }\n}\n","import type Database from 'better-sqlite3'\nimport type { DocFragment } from './types.js'\n\n/**\n * Fetch a full documentation fragment by id. This is the one place full body\n * text is returned (the MCP `get_doc` tool and the `sackville://doc/{id}`\n * resource wrap it); search results stay compact. Returns undefined if absent.\n */\nexport function getDoc(db: Database.Database, id: number): DocFragment | undefined {\n const sql = `\n SELECT id,\n library,\n version,\n title,\n symbol,\n type,\n heading_path AS headingPath,\n url,\n attribution,\n body\n FROM docs\n WHERE id = ?\n `\n return db.prepare(sql).get(id) as DocFragment | undefined\n}\n","import { readdirSync, readFileSync } from 'node:fs'\nimport { join } from 'node:path'\n\n/** Package ecosystems Sackville can detect an installed version in. */\nexport type Ecosystem = 'node' | 'python' | 'ruby'\n\n/** Where a detected version came from (most authoritative first, per ecosystem). */\nexport type VersionSource =\n | 'node_modules'\n | 'package-lock.json'\n | 'package.json'\n | 'python:dist-info'\n | 'python:lock'\n | 'python:requirements'\n | 'python:pyproject'\n | 'ruby:Gemfile.lock'\n | 'ruby:Gemfile'\n | 'none'\n\nexport interface DetectedVersion {\n /** Concrete installed version, or a declared range/constraint, or null. */\n version: string | null\n source: VersionSource\n}\n\nexport interface DetectOptions {\n /** Restrict detection to one ecosystem. Omit to auto-probe node → python → ruby. */\n ecosystem?: Ecosystem\n}\n\nconst NONE: DetectedVersion = { version: null, source: 'none' }\n\nfunction readText(path: string): string | undefined {\n try {\n return readFileSync(path, 'utf8')\n } catch {\n return undefined\n }\n}\n\nfunction readJson(path: string): Record<string, unknown> | undefined {\n const text = readText(path)\n if (text === undefined) return undefined\n try {\n return JSON.parse(text)\n } catch {\n return undefined\n }\n}\n\nfunction readdirSafe(path: string): string[] {\n try {\n return readdirSync(path)\n } catch {\n return []\n }\n}\n\nfunction escapeRe(s: string): string {\n return s.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')\n}\n\n/**\n * Detect the installed version of a package in a project directory.\n *\n * With an explicit `ecosystem`, runs only that detector. Otherwise auto-probes\n * node → python → ruby and returns the first hit. Each ecosystem prefers the\n * concrete installed version, then a lockfile, then the declared range — the\n * result feeds `resolveVersion`, which accepts concrete versions and ranges.\n */\nexport function detectInstalledVersion(\n projectDir: string,\n pkg: string,\n opts: DetectOptions = {},\n): DetectedVersion {\n const detectors: Record<Ecosystem, (d: string, p: string) => DetectedVersion> = {\n node: detectNode,\n python: detectPython,\n ruby: detectRuby,\n }\n if (opts.ecosystem) return detectors[opts.ecosystem](projectDir, pkg)\n for (const eco of ['node', 'python', 'ruby'] as const) {\n const found = detectors[eco](projectDir, pkg)\n if (found.version !== null) return found\n }\n return NONE\n}\n\n// ── Node ────────────────────────────────────────────────────────────────────\n\nfunction depRange(manifest: Record<string, unknown>, pkg: string): string | undefined {\n for (const field of [\n 'dependencies',\n 'devDependencies',\n 'peerDependencies',\n 'optionalDependencies',\n ]) {\n const deps = manifest[field] as Record<string, string> | undefined\n if (deps?.[pkg]) return deps[pkg]\n }\n return undefined\n}\n\nfunction detectNode(dir: string, pkg: string): DetectedVersion {\n const installed = readJson(join(dir, 'node_modules', pkg, 'package.json'))\n if (typeof installed?.version === 'string') {\n return { version: installed.version, source: 'node_modules' }\n }\n\n const lock = readJson(join(dir, 'package-lock.json'))\n if (lock) {\n const packages = lock.packages as Record<string, { version?: string }> | undefined\n const deps = lock.dependencies as Record<string, { version?: string }> | undefined\n const locked = packages?.[`node_modules/${pkg}`]?.version ?? deps?.[pkg]?.version\n if (locked) return { version: locked, source: 'package-lock.json' }\n }\n\n const manifest = readJson(join(dir, 'package.json'))\n if (manifest) {\n const range = depRange(manifest, pkg)\n if (range) return { version: range, source: 'package.json' }\n }\n\n return NONE\n}\n\n// ── Python ───────────────────────────────────────────────────────────────────\n\n/** PEP 503 name normalization: lowercase, runs of `-_.` collapse to a single `-`. */\nfunction canonPy(name: string): string {\n return name.toLowerCase().replace(/[-_.]+/g, '-')\n}\n\n/** Candidate site-packages roots inside a project's virtualenv(s). */\nfunction sitePackagesDirs(dir: string): string[] {\n const out: string[] = []\n for (const venv of ['.venv', 'venv', 'env']) {\n const lib = join(dir, venv, 'lib')\n for (const py of readdirSafe(lib)) {\n if (py.startsWith('python')) out.push(join(lib, py, 'site-packages'))\n }\n out.push(join(dir, venv, 'Lib', 'site-packages')) // Windows layout\n }\n return out\n}\n\nfunction pythonDistInfo(dir: string, want: string): string | undefined {\n for (const sp of sitePackagesDirs(dir)) {\n for (const entry of readdirSafe(sp)) {\n if (!entry.endsWith('.dist-info')) continue\n const meta = readText(join(sp, entry, 'METADATA'))\n if (!meta) continue\n const name = /^Name:\\s*(.+)$/m.exec(meta)?.[1]?.trim()\n const version = /^Version:\\s*(.+)$/m.exec(meta)?.[1]?.trim()\n if (name && version && canonPy(name) === want) return version\n }\n }\n return undefined\n}\n\n/** uv.lock / poetry.lock: `[[package]]` blocks with `name`/`version`. */\nfunction tomlLockVersion(text: string, want: string): string | undefined {\n for (const block of text.split('[[package]]')) {\n const name = /^\\s*name\\s*=\\s*\"([^\"]+)\"/m.exec(block)?.[1]\n if (!name || canonPy(name) !== want) continue\n const version = /^\\s*version\\s*=\\s*\"([^\"]+)\"/m.exec(block)?.[1]\n if (version) return version\n }\n return undefined\n}\n\n/** Pipfile.lock (JSON): `default`/`develop` → `{ pkg: { version: \"==x.y\" } }`. */\nfunction pipfileLockVersion(text: string, want: string): string | undefined {\n let json: Record<string, unknown>\n try {\n json = JSON.parse(text)\n } catch {\n return undefined\n }\n for (const section of ['default', 'develop']) {\n const deps = json[section] as Record<string, { version?: string }> | undefined\n if (!deps) continue\n for (const [name, spec] of Object.entries(deps)) {\n if (canonPy(name) === want && typeof spec?.version === 'string') {\n return spec.version.replace(/^==/, '').trim()\n }\n }\n }\n return undefined\n}\n\n/** Parse a PEP 508 requirement spec into name + constraint (markers dropped). */\nfunction pep508(spec: string): { name: string; constraint: string } | undefined {\n const m = /^([A-Za-z0-9_.-]+)\\s*(?:\\[[^\\]]*\\])?\\s*(.*)$/.exec(spec.trim())\n if (!m?.[1]) return undefined\n return { name: m[1], constraint: (m[2] ?? '').split(';')[0]?.trim() ?? '' }\n}\n\nfunction requirementsVersion(text: string, want: string): string | undefined {\n for (const raw of text.split(/\\r?\\n/)) {\n const line = (raw.split('#')[0] ?? '').trim()\n if (!line || line.startsWith('-')) continue // skip blanks + options (-r, --hash, …)\n const parsed = pep508(line)\n if (!parsed || canonPy(parsed.name) !== want) continue\n if (parsed.constraint.startsWith('==')) return parsed.constraint.slice(2).trim()\n return parsed.constraint || '*'\n }\n return undefined\n}\n\nfunction pyprojectVersion(text: string, want: string): string | undefined {\n // PEP 621 [project] dependencies = [\"pkg>=1\", …]\n const arr = /dependencies\\s*=\\s*\\[([\\s\\S]*?)\\]/.exec(text)?.[1]\n if (arr) {\n for (const m of arr.matchAll(/[\"']([^\"']+)[\"']/g)) {\n const parsed = m[1] ? pep508(m[1]) : undefined\n if (parsed && canonPy(parsed.name) === want) return parsed.constraint || '*'\n }\n }\n // Poetry [tool.poetry.dependencies] pkg = \"^1.0\"\n const poetry = /\\[tool\\.poetry\\.dependencies\\]([\\s\\S]*?)(?:\\n\\[|$)/.exec(text)?.[1]\n if (poetry) {\n for (const raw of poetry.split(/\\r?\\n/)) {\n const m = /^\\s*([A-Za-z0-9_.-]+)\\s*=\\s*[\"']([^\"']+)[\"']/.exec(raw)\n if (m?.[1] && canonPy(m[1]) === want) return m[2] ?? '*'\n }\n }\n return undefined\n}\n\nfunction detectPython(dir: string, pkg: string): DetectedVersion {\n const want = canonPy(pkg)\n\n const installed = pythonDistInfo(dir, want)\n if (installed) return { version: installed, source: 'python:dist-info' }\n\n for (const lock of ['uv.lock', 'poetry.lock']) {\n const text = readText(join(dir, lock))\n const version = text && tomlLockVersion(text, want)\n if (version) return { version, source: 'python:lock' }\n }\n const pipfile = readText(join(dir, 'Pipfile.lock'))\n const pipfileVersion = pipfile && pipfileLockVersion(pipfile, want)\n if (pipfileVersion) return { version: pipfileVersion, source: 'python:lock' }\n\n const req = readText(join(dir, 'requirements.txt'))\n const reqVersion = req && requirementsVersion(req, want)\n if (reqVersion) return { version: reqVersion, source: 'python:requirements' }\n\n const pyproject = readText(join(dir, 'pyproject.toml'))\n const pyprojectVer = pyproject && pyprojectVersion(pyproject, want)\n if (pyprojectVer) return { version: pyprojectVer, source: 'python:pyproject' }\n\n return NONE\n}\n\n// ── Ruby ──────────────────────────────────────────────────────────────────────\n\nfunction detectRuby(dir: string, pkg: string): DetectedVersion {\n const want = pkg.toLowerCase()\n\n const lock = readText(join(dir, 'Gemfile.lock'))\n if (lock) {\n // Top-level resolved specs are indented exactly four spaces: ` name (x.y.z)`.\n for (const m of lock.matchAll(/^ {4}([A-Za-z0-9_.-]+) \\(([^)]+)\\)/gm)) {\n if (m[1]?.toLowerCase() === want) return { version: m[2] ?? '', source: 'ruby:Gemfile.lock' }\n }\n }\n\n const gemfile = readText(join(dir, 'Gemfile'))\n if (gemfile) {\n const re = new RegExp(`gem\\\\s+['\"]${escapeRe(pkg)}['\"]\\\\s*(?:,\\\\s*['\"]([^'\"]+)['\"])?`, 'i')\n const m = re.exec(gemfile)\n if (m) return { version: m[1] ?? '*', source: 'ruby:Gemfile' }\n }\n\n return NONE\n}\n","import type Database from 'better-sqlite3'\nimport { EXPECTED_EMBED_DIM } from './schema.js'\nimport type { SearchOptions, SearchResult } from './types.js'\n\nconst DEFAULT_LIMIT = 8\nconst MAX_LIMIT = 25\n// Reciprocal Rank Fusion constant (standard default).\nconst RRF_K = 60\n// Candidate pool pulled from each ranked list before fusion.\nconst CANDIDATES = 64\n\n/** Build a safe FTS5 MATCH string: quote each alphanumeric token (implicit AND). */\nfunction ftsMatch(query: string): string | null {\n const tokens = query.toLowerCase().match(/[\\p{L}\\p{N}]+/gu)\n if (!tokens || tokens.length === 0) return null\n return tokens.map((t) => `\"${t}\"`).join(' ')\n}\n\nfunction excerpt(body: string, max = 160): string {\n const collapsed = body.replace(/\\s+/g, ' ').trim()\n return collapsed.length <= max ? collapsed : `${collapsed.slice(0, max).trimEnd()}…`\n}\n\ninterface MetaRow {\n id: number\n title: string\n symbol: string | null\n type: string | null\n library: string\n version: string\n body: string\n}\n\n/**\n * Search the docs index. Full-text (FTS5/bm25) always runs; when `queryVector`\n * is supplied, vector KNN (sqlite-vec) is fused with it via reciprocal rank\n * fusion. Library/version/type filters apply to both halves. Results are sorted\n * best-first; bodies are never returned (fetch them via getDoc).\n */\nexport function searchDocs(\n db: Database.Database,\n query: string,\n options: SearchOptions = {},\n): SearchResult[] {\n const limit = Math.min(Math.max(options.limit ?? DEFAULT_LIMIT, 1), MAX_LIMIT)\n const useVector =\n Array.isArray(options.queryVector) && options.queryVector.length === EXPECTED_EMBED_DIM\n\n const snippets = new Map<number, string>()\n const ftsRanked = runFts(db, query, options, snippets)\n const vecRanked = useVector ? runVector(db, options) : []\n\n // Reciprocal rank fusion across the available ranked lists.\n const scores = new Map<number, number>()\n for (const ranked of [ftsRanked, vecRanked]) {\n ranked.forEach((id, i) => {\n scores.set(id, (scores.get(id) ?? 0) + 1 / (RRF_K + i + 1))\n })\n }\n if (scores.size === 0) return []\n\n const topIds = [...scores.entries()]\n .sort((a, b) => b[1] - a[1])\n .slice(0, limit)\n .map(([id]) => id)\n\n const placeholders = topIds.map(() => '?').join(',')\n const rows = db\n .prepare(\n `SELECT id, title, symbol, type, library, version, body\n FROM docs WHERE id IN (${placeholders})`,\n )\n .all(...topIds) as MetaRow[]\n const byId = new Map(rows.map((r) => [r.id, r]))\n\n const results: SearchResult[] = []\n for (const id of topIds) {\n const row = byId.get(id)\n if (!row) continue\n const { body, ...meta } = row\n results.push({\n ...meta,\n score: scores.get(id) ?? 0,\n snippet: snippets.get(id) ?? excerpt(body),\n })\n }\n return results\n}\n\nfunction runFts(\n db: Database.Database,\n query: string,\n options: SearchOptions,\n snippets: Map<number, string>,\n): number[] {\n const match = ftsMatch(query)\n if (!match) return []\n\n const clauses = ['docs_fts MATCH @match']\n const params: Record<string, unknown> = { match, cand: CANDIDATES }\n if (options.library !== undefined) {\n clauses.push('d.library = @library')\n params.library = options.library\n }\n if (options.version !== undefined) {\n clauses.push('d.version = @version')\n params.version = options.version\n }\n if (options.type !== undefined) {\n clauses.push('d.type = @type')\n params.type = options.type\n }\n\n const rows = db\n .prepare(\n `SELECT docs_fts.rowid AS id, snippet(docs_fts, 1, '[', ']', '…', 12) AS snippet\n FROM docs_fts JOIN docs d ON d.id = docs_fts.rowid\n WHERE ${clauses.join(' AND ')}\n ORDER BY bm25(docs_fts) LIMIT @cand`,\n )\n .all(params) as { id: number; snippet: string }[]\n\n const ids: number[] = []\n for (const r of rows) {\n ids.push(r.id)\n snippets.set(r.id, r.snippet)\n }\n return ids\n}\n\nfunction runVector(db: Database.Database, options: SearchOptions): number[] {\n const clauses = ['embedding MATCH @vec', 'k = @k']\n const params: Record<string, unknown> = {\n vec: JSON.stringify(options.queryVector),\n k: CANDIDATES,\n }\n if (options.library !== undefined) {\n clauses.push('library = @library')\n params.library = options.library\n }\n if (options.version !== undefined) {\n clauses.push('version = @version')\n params.version = options.version\n }\n if (options.type !== undefined) {\n clauses.push('type = @type')\n params.type = options.type\n }\n\n const rows = db\n .prepare(\n `SELECT doc_id AS id FROM docs_vec\n WHERE ${clauses.join(' AND ')}\n ORDER BY distance`,\n )\n .all(params) as { id: number }[]\n return rows.map((r) => r.id)\n}\n","import type Database from 'better-sqlite3'\nimport semver from 'semver'\n\n/** Outcome of resolving an installed version to an available doc release. */\nexport interface VersionResolution {\n /** The available version string to use, or null if none is acceptable. */\n resolved: string | null\n /** True when the request was exactly satisfied (not a same-major fallback). */\n exact: boolean\n /** Human/agent-readable explanation of the decision. */\n note: string\n /** Available versions, newest first. */\n available: string[]\n}\n\n/** Distinct versions of a library present in the index, newest first. */\nexport function listVersions(db: Database.Database, library: string): string[] {\n const rows = db\n .prepare('SELECT DISTINCT version FROM docs WHERE library = ? ORDER BY version')\n .all(library) as { version: string }[]\n return sortDesc(rows.map((r) => r.version))\n}\n\nfunction sortDesc(versions: string[]): string[] {\n return [...versions].sort((a, b) => {\n const av = semver.coerce(a)\n const bv = semver.coerce(b)\n if (av && bv) return semver.rcompare(av, bv)\n return b.localeCompare(a)\n })\n}\n\n/**\n * Resolve a requested/installed version (exact, range, or bare major) to the\n * best available doc release.\n *\n * Policy (ARCHITECTURE §7.2): prefer an exactly-satisfying release; otherwise\n * fall back to the newest release sharing the requested MAJOR and flag it;\n * never silently return a wrong-major release.\n */\nexport function resolveVersion(available: string[], requested: string): VersionResolution {\n const sorted = sortDesc(available)\n if (sorted.length === 0) {\n return { resolved: null, exact: false, note: 'no versions are indexed', available: sorted }\n }\n\n // 1. Exact / range satisfaction — newest first.\n for (const version of sorted) {\n const coerced = semver.coerce(version)\n if (coerced && satisfies(coerced, requested)) {\n return {\n resolved: version,\n exact: true,\n note: `matched ${requested} to ${version}`,\n available: sorted,\n }\n }\n }\n\n // 2. Same-major fallback.\n const major = requestedMajor(requested)\n if (major !== null) {\n for (const version of sorted) {\n if (semver.coerce(version)?.major === major) {\n return {\n resolved: version,\n exact: false,\n note: `no exact docs for ${requested}; using nearest ${major}.x release ${version}`,\n available: sorted,\n }\n }\n }\n }\n\n // 3. Refuse — never serve a wrong-major release silently.\n return {\n resolved: null,\n exact: false,\n note: `no docs for ${requested}; available versions: ${sorted.join(', ')}`,\n available: sorted,\n }\n}\n\nfunction satisfies(version: semver.SemVer, requested: string): boolean {\n if (semver.validRange(requested)) {\n return semver.satisfies(version, requested)\n }\n const coerced = semver.coerce(requested)\n return coerced ? semver.eq(version, coerced) : false\n}\n\nfunction requestedMajor(requested: string): number | null {\n const coerced = semver.coerce(requested)\n if (coerced) return coerced.major\n const min = semver.validRange(requested) ? semver.minVersion(requested) : null\n return min ? min.major : null\n}\n"],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "@sackville-mcp/core",
3
+ "version": "0.0.1-alpha.0",
4
+ "type": "module",
5
+ "license": "Apache-2.0",
6
+ "exports": {
7
+ ".": {
8
+ "import": {
9
+ "types": "./dist/index.d.mts",
10
+ "default": "./dist/index.mjs"
11
+ }
12
+ }
13
+ },
14
+ "main": "./dist/index.mjs",
15
+ "types": "./src/index.ts",
16
+ "files": [
17
+ "dist"
18
+ ],
19
+ "dependencies": {
20
+ "better-sqlite3": "^12.10.0",
21
+ "semver": "^7.8.1",
22
+ "sqlite-vec": "^0.1.9"
23
+ },
24
+ "devDependencies": {
25
+ "@types/better-sqlite3": "^7.6.13",
26
+ "@types/semver": "^7.7.1"
27
+ },
28
+ "publishConfig": {
29
+ "access": "public"
30
+ },
31
+ "repository": {
32
+ "type": "git",
33
+ "url": "git+https://github.com/ceautery/sackville.git",
34
+ "directory": "packages/core"
35
+ },
36
+ "scripts": {
37
+ "build": "tsdown src/index.ts --dts",
38
+ "typecheck": "tsc --noEmit"
39
+ }
40
+ }