@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 +201 -0
- package/dist/index.d.mts +138 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +465 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +40 -0
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.
|
package/dist/index.d.mts
ADDED
|
@@ -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
|
+
}
|