@modootoday/pmcp 0.6.0 → 0.7.1

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/README.md CHANGED
@@ -160,11 +160,18 @@ export default { fetch: (request: Request) => skills.fetch(request) };
160
160
 
161
161
  Put Origin and Host validation in front of it when it is mounted bare.
162
162
 
163
+ Pass `semantic: { cacheDir, localOnly: true }` to rank by meaning: the handler encodes
164
+ the catalog at start with `@huggingface/transformers` and the multilingual model, so an
165
+ intent in one language finds a description in another. Requests rank lexically until
166
+ `ready()` settles, and stay lexical when the encoder cannot load (`onError` says why).
167
+
163
168
  ## Runtime and licence
164
169
 
165
- Node.js 22 or later, or Bun 1.3 or later. Default ranking is lexical. The optional
166
- `index` command requires a separately installed encoder; it is not part of the
167
- default installation.
170
+ Node.js 22 or later, or Bun 1.3 or later. Default ranking is lexical. Semantic
171
+ ranking (the `index` command, or the HTTP handler's `semantic` option) needs
172
+ `@huggingface/transformers` installed separately; it is not part of the default
173
+ installation. The default encoder is `Xenova/multilingual-e5-small` (MIT, about
174
+ 130 MB), so vectors indexed with the earlier English-only default are re-encoded.
168
175
 
169
176
  Elastic License 2.0. See `LICENSE` and `NOTICE` for terms and dependency notices.
170
177
  Documentation: <https://pmcp.build>.
@@ -791,9 +791,16 @@ function createSkillTools(options) {
791
791
  call(name) {
792
792
  const entry = catalog().find((candidate) => candidate.name === name);
793
793
  if (!entry) return null;
794
+ const root = assetRoot(entry);
795
+ const listing = listFiles(root);
796
+ const own = relative2(root, entry.path).split(sep).join("/");
794
797
  return {
795
798
  name: entry.name,
796
- body: readBody(readFileSync3(entry.path, "utf8"))
799
+ body: readBody(readFileSync3(entry.path, "utf8")),
800
+ files: listing.files.filter(
801
+ (file) => file.path !== own && file.path !== basename3(entry.path)
802
+ ),
803
+ filesTruncated: listing.truncated
797
804
  };
798
805
  },
799
806
  /** Drops the memoised catalog, so the next call re-reads the tree. */
@@ -1082,7 +1089,7 @@ function registerSkillTools(server, tools) {
1082
1089
  "skill_read",
1083
1090
  {
1084
1091
  ...readOnlyTool("Read a skill file"),
1085
- description: "A reference or script file inside one skill's directory in this catalog, as text, with its sha256. It reads only files the catalog ships and calls no external API; path is relative to the skill, as skill_describe lists it (layout: https://pmcp.build). Read only when the skill body points at the file. Scripts are returned, never run. Files over 256 KiB and paths outside the skill are refused.",
1092
+ description: "A reference or script file inside one skill's directory in this catalog, as text, with its sha256. It reads only files the catalog ships and calls no external API; path is relative to the skill, as skill_call and skill_describe list it (layout: https://pmcp.build). Scripts are returned as text, never run. Files over 256 KiB and paths outside the skill are refused.",
1086
1093
  inputSchema: z2.object({
1087
1094
  name: z2.string().min(1),
1088
1095
  path: z2.string().min(1).optional().describe(
@@ -1106,7 +1113,7 @@ function registerSkillTools(server, tools) {
1106
1113
  "skill_call",
1107
1114
  {
1108
1115
  ...readOnlyTool("Read a skill"),
1109
- description: "The body of a single skill, by the name skill_find or skill_catalog returned. This is the only call that costs a document.",
1116
+ description: "The body of a single skill, by the name skill_find or skill_catalog returned, followed by a list of the reference and script files the skill carries, if any. This is the only call that costs a document.",
1110
1117
  inputSchema: z2.object({
1111
1118
  name: z2.string().min(1).describe("The skill name, as <package>/<slug>.")
1112
1119
  })
@@ -1123,13 +1130,30 @@ function registerSkillTools(server, tools) {
1123
1130
  isError: true
1124
1131
  };
1125
1132
  }
1126
- return { content: [{ type: "text", text: found.body }] };
1133
+ if (found.files.length === 0) {
1134
+ return { content: [{ type: "text", text: found.body }] };
1135
+ }
1136
+ const lines = found.files.map(
1137
+ (file) => `- ${file.path} (${file.bytes} bytes, sha256 ${file.sha256.slice(0, 12)})`
1138
+ );
1139
+ if (found.filesTruncated)
1140
+ lines.push("- (list cut; skill_describe has the rest)");
1141
+ const manifest = [
1142
+ `Files in skill ${found.name}, each returned by skill_read with this name and the path:`,
1143
+ ...lines
1144
+ ].join("\n");
1145
+ return {
1146
+ content: [
1147
+ { type: "text", text: found.body },
1148
+ { type: "text", text: manifest }
1149
+ ]
1150
+ };
1127
1151
  }
1128
1152
  );
1129
1153
  return server;
1130
1154
  }
1131
1155
  function createSkillServer(options) {
1132
- const server = new McpServer({ name: SERVER_NAME, version: "0.6.0" });
1156
+ const server = new McpServer({ name: SERVER_NAME, version: "0.7.1" });
1133
1157
  const tools = createSkillTools(options);
1134
1158
  registerSkillsExtension(server, tools.skills);
1135
1159
  return registerSkillTools(server, tools);
@@ -1167,4 +1191,4 @@ export {
1167
1191
  registerSkillTools,
1168
1192
  createSkillServer
1169
1193
  };
1170
- //# sourceMappingURL=chunk-5TX2IAVD.js.map
1194
+ //# sourceMappingURL=chunk-C46YCY2I.js.map
@@ -1,131 +1,3 @@
1
- import {
2
- normaliseIntent
3
- } from "./chunk-5TX2IAVD.js";
4
-
5
- // src/cache.ts
6
- async function openDatabase(path) {
7
- try {
8
- const { DatabaseSync } = await import(`node:${"sqlite"}`);
9
- return { db: new DatabaseSync(path), provider: "node:sqlite" };
10
- } catch {
11
- }
12
- try {
13
- const { Database } = await import(`bun:${"sqlite"}`);
14
- const bun = new Database(path, { create: true });
15
- return {
16
- db: {
17
- exec: (sql) => bun.run(sql),
18
- prepare: (sql) => bun.query(sql),
19
- close: () => bun.close()
20
- },
21
- provider: "bun:sqlite"
22
- };
23
- } catch {
24
- return null;
25
- }
26
- }
27
- var DEFAULT_INTENT_CACHE_LIMIT = 4096;
28
- var SCHEMA = `
29
- CREATE TABLE IF NOT EXISTS intent_vector (
30
- intent TEXT NOT NULL PRIMARY KEY,
31
- model_id TEXT NOT NULL,
32
- dims INTEGER NOT NULL,
33
- vector BLOB NOT NULL,
34
- used_at INTEGER NOT NULL
35
- );
36
- CREATE INDEX IF NOT EXISTS intent_vector_used_at ON intent_vector (used_at);
37
- `;
38
- async function openIntentCache(options) {
39
- const opened = await openDatabase(options.path);
40
- if (!opened) return null;
41
- const { db, provider } = opened;
42
- try {
43
- db.exec("PRAGMA journal_mode = WAL");
44
- db.exec(SCHEMA);
45
- } catch {
46
- db.close();
47
- return null;
48
- }
49
- const limit = options.limit ?? DEFAULT_INTENT_CACHE_LIMIT;
50
- const now = options.now ?? (() => Date.now());
51
- let hits = 0;
52
- let misses = 0;
53
- let mismatched = 0;
54
- const selectRow = db.prepare("SELECT model_id, dims, vector FROM intent_vector WHERE intent = ?");
55
- const touch = db.prepare(
56
- "UPDATE intent_vector SET used_at = ? WHERE intent = ?"
57
- );
58
- const upsert = db.prepare(
59
- `INSERT INTO intent_vector (intent, model_id, dims, vector, used_at)
60
- VALUES (?, ?, ?, ?, ?)
61
- ON CONFLICT(intent) DO UPDATE SET
62
- model_id = excluded.model_id,
63
- dims = excluded.dims,
64
- vector = excluded.vector,
65
- used_at = excluded.used_at`
66
- );
67
- const countRows = db.prepare(
68
- "SELECT count(*) AS n FROM intent_vector"
69
- );
70
- const evict = db.prepare(
71
- `DELETE FROM intent_vector WHERE intent IN (
72
- SELECT intent FROM intent_vector ORDER BY used_at ASC LIMIT ?
73
- )`
74
- );
75
- return {
76
- get(intent) {
77
- const key = normaliseIntent(intent);
78
- const row = selectRow.get(key) ?? null;
79
- if (!row) {
80
- misses += 1;
81
- return null;
82
- }
83
- if (row.model_id !== options.modelId || row.dims !== options.dims) {
84
- mismatched += 1;
85
- return null;
86
- }
87
- hits += 1;
88
- touch.run(now(), key);
89
- const bytes = row.vector;
90
- return new Float32Array(
91
- bytes.buffer.slice(
92
- bytes.byteOffset,
93
- bytes.byteOffset + bytes.byteLength
94
- )
95
- );
96
- },
97
- put(intent, vector) {
98
- if (vector.length !== options.dims) {
99
- throw new Error(
100
- `intent vector is ${vector.length} wide, cache holds ${options.dims}`
101
- );
102
- }
103
- const key = normaliseIntent(intent);
104
- upsert.run(
105
- key,
106
- options.modelId,
107
- options.dims,
108
- new Uint8Array(vector.buffer, vector.byteOffset, vector.byteLength),
109
- now()
110
- );
111
- const rows = countRows.get()?.n ?? 0;
112
- if (rows > limit) evict.run(rows - limit);
113
- },
114
- stats() {
115
- return {
116
- hits,
117
- misses,
118
- mismatched,
119
- rows: countRows.get()?.n ?? 0,
120
- provider
121
- };
122
- },
123
- close() {
124
- db.close();
125
- }
126
- };
127
- }
128
-
129
1
  // src/config.ts
130
2
  import { existsSync, readFileSync } from "fs";
131
3
  import { dirname, join, resolve } from "path";
@@ -1215,9 +1087,6 @@ function readSpec(config) {
1215
1087
  }
1216
1088
 
1217
1089
  export {
1218
- openDatabase,
1219
- DEFAULT_INTENT_CACHE_LIMIT,
1220
- openIntentCache,
1221
1090
  CONFIG_FILE,
1222
1091
  ConfigError,
1223
1092
  findConfig,
@@ -1235,4 +1104,4 @@ export {
1235
1104
  TOOLS,
1236
1105
  readSpec
1237
1106
  };
1238
- //# sourceMappingURL=chunk-6TSHDCUY.js.map
1107
+ //# sourceMappingURL=chunk-GNJSZCRH.js.map
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  createSkillServer
3
- } from "./chunk-5TX2IAVD.js";
3
+ } from "./chunk-C46YCY2I.js";
4
4
 
5
5
  // src/stdio.ts
6
6
  import { serveStdio } from "@modelcontextprotocol/server/stdio";
@@ -35,4 +35,4 @@ export {
35
35
  serve,
36
36
  main
37
37
  };
38
- //# sourceMappingURL=chunk-FXBLBKP6.js.map
38
+ //# sourceMappingURL=chunk-IJPOJ6PG.js.map
@@ -0,0 +1,134 @@
1
+ import {
2
+ normaliseIntent
3
+ } from "./chunk-C46YCY2I.js";
4
+
5
+ // src/cache.ts
6
+ async function openDatabase(path) {
7
+ try {
8
+ const { DatabaseSync } = await import(`node:${"sqlite"}`);
9
+ return { db: new DatabaseSync(path), provider: "node:sqlite" };
10
+ } catch {
11
+ }
12
+ try {
13
+ const { Database } = await import(`bun:${"sqlite"}`);
14
+ const bun = new Database(path, { create: true });
15
+ return {
16
+ db: {
17
+ exec: (sql) => bun.run(sql),
18
+ prepare: (sql) => bun.query(sql),
19
+ close: () => bun.close()
20
+ },
21
+ provider: "bun:sqlite"
22
+ };
23
+ } catch {
24
+ return null;
25
+ }
26
+ }
27
+ var DEFAULT_INTENT_CACHE_LIMIT = 4096;
28
+ var SCHEMA = `
29
+ CREATE TABLE IF NOT EXISTS intent_vector (
30
+ intent TEXT NOT NULL PRIMARY KEY,
31
+ model_id TEXT NOT NULL,
32
+ dims INTEGER NOT NULL,
33
+ vector BLOB NOT NULL,
34
+ used_at INTEGER NOT NULL
35
+ );
36
+ CREATE INDEX IF NOT EXISTS intent_vector_used_at ON intent_vector (used_at);
37
+ `;
38
+ async function openIntentCache(options) {
39
+ const opened = await openDatabase(options.path);
40
+ if (!opened) return null;
41
+ const { db, provider } = opened;
42
+ try {
43
+ db.exec("PRAGMA journal_mode = WAL");
44
+ db.exec(SCHEMA);
45
+ } catch {
46
+ db.close();
47
+ return null;
48
+ }
49
+ const limit = options.limit ?? DEFAULT_INTENT_CACHE_LIMIT;
50
+ const now = options.now ?? (() => Date.now());
51
+ let hits = 0;
52
+ let misses = 0;
53
+ let mismatched = 0;
54
+ const selectRow = db.prepare("SELECT model_id, dims, vector FROM intent_vector WHERE intent = ?");
55
+ const touch = db.prepare(
56
+ "UPDATE intent_vector SET used_at = ? WHERE intent = ?"
57
+ );
58
+ const upsert = db.prepare(
59
+ `INSERT INTO intent_vector (intent, model_id, dims, vector, used_at)
60
+ VALUES (?, ?, ?, ?, ?)
61
+ ON CONFLICT(intent) DO UPDATE SET
62
+ model_id = excluded.model_id,
63
+ dims = excluded.dims,
64
+ vector = excluded.vector,
65
+ used_at = excluded.used_at`
66
+ );
67
+ const countRows = db.prepare(
68
+ "SELECT count(*) AS n FROM intent_vector"
69
+ );
70
+ const evict = db.prepare(
71
+ `DELETE FROM intent_vector WHERE intent IN (
72
+ SELECT intent FROM intent_vector ORDER BY used_at ASC LIMIT ?
73
+ )`
74
+ );
75
+ return {
76
+ get(intent) {
77
+ const key = normaliseIntent(intent);
78
+ const row = selectRow.get(key) ?? null;
79
+ if (!row) {
80
+ misses += 1;
81
+ return null;
82
+ }
83
+ if (row.model_id !== options.modelId || row.dims !== options.dims) {
84
+ mismatched += 1;
85
+ return null;
86
+ }
87
+ hits += 1;
88
+ touch.run(now(), key);
89
+ const bytes = row.vector;
90
+ return new Float32Array(
91
+ bytes.buffer.slice(
92
+ bytes.byteOffset,
93
+ bytes.byteOffset + bytes.byteLength
94
+ )
95
+ );
96
+ },
97
+ put(intent, vector) {
98
+ if (vector.length !== options.dims) {
99
+ throw new Error(
100
+ `intent vector is ${vector.length} wide, cache holds ${options.dims}`
101
+ );
102
+ }
103
+ const key = normaliseIntent(intent);
104
+ upsert.run(
105
+ key,
106
+ options.modelId,
107
+ options.dims,
108
+ new Uint8Array(vector.buffer, vector.byteOffset, vector.byteLength),
109
+ now()
110
+ );
111
+ const rows = countRows.get()?.n ?? 0;
112
+ if (rows > limit) evict.run(rows - limit);
113
+ },
114
+ stats() {
115
+ return {
116
+ hits,
117
+ misses,
118
+ mismatched,
119
+ rows: countRows.get()?.n ?? 0,
120
+ provider
121
+ };
122
+ },
123
+ close() {
124
+ db.close();
125
+ }
126
+ };
127
+ }
128
+
129
+ export {
130
+ openDatabase,
131
+ DEFAULT_INTENT_CACHE_LIMIT,
132
+ openIntentCache
133
+ };
134
+ //# sourceMappingURL=chunk-KP3TQ3QY.js.map
@@ -0,0 +1,160 @@
1
+ import {
2
+ openDatabase
3
+ } from "./chunk-KP3TQ3QY.js";
4
+
5
+ // src/embed.ts
6
+ var DEFAULT_MODEL_ID = "Xenova/multilingual-e5-small";
7
+ var PLAIN = {
8
+ queryPrefix: "",
9
+ passagePrefix: "",
10
+ pooling: "mean"
11
+ };
12
+ var E5 = {
13
+ queryPrefix: "query: ",
14
+ passagePrefix: "passage: ",
15
+ pooling: "mean"
16
+ };
17
+ var KNOWN_ENCODERS = {
18
+ "Xenova/multilingual-e5-small": E5,
19
+ "Xenova/multilingual-e5-base": E5,
20
+ "Xenova/multilingual-e5-large": E5,
21
+ "Xenova/bge-m3": { ...PLAIN, pooling: "cls" }
22
+ };
23
+ function encoderSpec(modelId) {
24
+ return KNOWN_ENCODERS[modelId] ?? PLAIN;
25
+ }
26
+ var ENTRY_SCHEMA = `
27
+ CREATE TABLE IF NOT EXISTS entry_vector (
28
+ name TEXT NOT NULL,
29
+ model_id TEXT NOT NULL,
30
+ dims INTEGER NOT NULL,
31
+ description_hash TEXT NOT NULL,
32
+ vector BLOB NOT NULL,
33
+ PRIMARY KEY (name, model_id)
34
+ );
35
+ `;
36
+ var TRANSFORMERS = "@huggingface/transformers";
37
+ async function loadEmbedder(modelId = DEFAULT_MODEL_ID, options = {}) {
38
+ let module;
39
+ try {
40
+ module = options.module ?? await import(TRANSFORMERS);
41
+ } catch {
42
+ return null;
43
+ }
44
+ const factory = module.pipeline;
45
+ if (typeof factory !== "function") return null;
46
+ if (module.env) {
47
+ if (options.cacheDir !== void 0)
48
+ module.env["cacheDir"] = options.cacheDir;
49
+ if (options.localOnly) module.env["allowRemoteModels"] = false;
50
+ }
51
+ const encode = await factory("feature-extraction", modelId, {
52
+ dtype: options.dtype ?? "q8"
53
+ });
54
+ const spec = encoderSpec(modelId);
55
+ let dims = 0;
56
+ const run = async (text) => {
57
+ const output = await encode(text, {
58
+ pooling: spec.pooling,
59
+ normalize: true
60
+ });
61
+ const vector = Float32Array.from(output.data);
62
+ dims = vector.length;
63
+ return vector;
64
+ };
65
+ return {
66
+ modelId,
67
+ get dims() {
68
+ return dims;
69
+ },
70
+ embed: (text) => run(spec.queryPrefix + text),
71
+ embedPassage: (text) => run(spec.passagePrefix + text)
72
+ };
73
+ }
74
+ async function encodeEntries(embedder, entries, previous = /* @__PURE__ */ new Map()) {
75
+ const out = /* @__PURE__ */ new Map();
76
+ const passage = (text) => embedder.embedPassage ? embedder.embedPassage(text) : embedder.embed(text);
77
+ for (const entry of entries) {
78
+ const hash = describeHash(entry.description);
79
+ const kept = previous.get(entry.name);
80
+ if (kept && kept.hash === hash) {
81
+ out.set(entry.name, kept);
82
+ continue;
83
+ }
84
+ out.set(entry.name, { hash, vector: await passage(entry.description) });
85
+ }
86
+ return out;
87
+ }
88
+ function describeHash(description) {
89
+ let hash = 2166136261;
90
+ for (let i = 0; i < description.length; i++) {
91
+ hash ^= description.charCodeAt(i);
92
+ hash = Math.imul(hash, 16777619) >>> 0;
93
+ }
94
+ return hash.toString(16).padStart(8, "0");
95
+ }
96
+ async function openEntryVectors(path) {
97
+ const opened = await openDatabase(path);
98
+ if (opened === null) return null;
99
+ const db = opened.db;
100
+ db.exec(ENTRY_SCHEMA);
101
+ return {
102
+ read(modelId) {
103
+ const out = /* @__PURE__ */ new Map();
104
+ const rows = db.prepare(
105
+ "SELECT name, dims, vector FROM entry_vector WHERE model_id = ?"
106
+ ).all(modelId);
107
+ for (const row of rows) {
108
+ const bytes = row.vector;
109
+ const vector = new Float32Array(
110
+ bytes.buffer.slice(
111
+ bytes.byteOffset,
112
+ bytes.byteOffset + bytes.byteLength
113
+ )
114
+ );
115
+ if (vector.length === row.dims) out.set(row.name, vector);
116
+ }
117
+ return out;
118
+ },
119
+ stale(modelId, entries) {
120
+ const rows = db.prepare(
121
+ "SELECT name, description_hash FROM entry_vector WHERE model_id = ?"
122
+ ).all(modelId);
123
+ const stored = new Map(
124
+ rows.map((row) => [row.name, row.description_hash])
125
+ );
126
+ const out = [];
127
+ for (const [name, description] of entries) {
128
+ if (stored.get(name) !== describeHash(description)) out.push(name);
129
+ }
130
+ return out;
131
+ },
132
+ write(modelId, name, description, vector) {
133
+ db.prepare(
134
+ `INSERT INTO entry_vector (name, model_id, dims, description_hash, vector)
135
+ VALUES (?, ?, ?, ?, ?)
136
+ ON CONFLICT(name, model_id) DO UPDATE SET
137
+ dims = excluded.dims,
138
+ description_hash = excluded.description_hash,
139
+ vector = excluded.vector`
140
+ ).run(
141
+ name,
142
+ modelId,
143
+ vector.length,
144
+ describeHash(description),
145
+ new Uint8Array(vector.buffer, vector.byteOffset, vector.byteLength)
146
+ );
147
+ },
148
+ close() {
149
+ db.close();
150
+ }
151
+ };
152
+ }
153
+
154
+ export {
155
+ DEFAULT_MODEL_ID,
156
+ loadEmbedder,
157
+ encodeEntries,
158
+ openEntryVectors
159
+ };
160
+ //# sourceMappingURL=chunk-VBLAH6J2.js.map
package/dist/cli.js CHANGED
@@ -1,25 +1,29 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ loadEmbedder,
4
+ openEntryVectors
5
+ } from "./chunk-VBLAH6J2.js";
2
6
  import {
3
7
  ConfigError,
4
8
  ProjectError,
5
9
  TOOLS,
6
10
  doctor,
7
11
  findConfig,
8
- openDatabase,
9
12
  project,
10
13
  readConfig,
11
14
  readSpec,
12
15
  renderFrontmatter,
13
16
  spawnRunner,
14
17
  splitFrontmatter
15
- } from "./chunk-6TSHDCUY.js";
18
+ } from "./chunk-GNJSZCRH.js";
19
+ import "./chunk-KP3TQ3QY.js";
16
20
  import {
17
21
  serve
18
- } from "./chunk-FXBLBKP6.js";
22
+ } from "./chunk-IJPOJ6PG.js";
19
23
  import {
20
24
  kindOf,
21
25
  readCatalog
22
- } from "./chunk-5TX2IAVD.js";
26
+ } from "./chunk-C46YCY2I.js";
23
27
 
24
28
  // src/cli/command.ts
25
29
  var ArgumentError = class extends Error {
@@ -198,109 +202,6 @@ var Ui = class {
198
202
  }
199
203
  };
200
204
 
201
- // src/embed.ts
202
- var DEFAULT_MODEL_ID = "Xenova/all-MiniLM-L6-v2";
203
- var ENTRY_SCHEMA = `
204
- CREATE TABLE IF NOT EXISTS entry_vector (
205
- name TEXT NOT NULL,
206
- model_id TEXT NOT NULL,
207
- dims INTEGER NOT NULL,
208
- description_hash TEXT NOT NULL,
209
- vector BLOB NOT NULL,
210
- PRIMARY KEY (name, model_id)
211
- );
212
- `;
213
- var TRANSFORMERS = "@huggingface/transformers";
214
- async function loadEmbedder(modelId = DEFAULT_MODEL_ID) {
215
- let module;
216
- try {
217
- module = await import(TRANSFORMERS);
218
- } catch {
219
- return null;
220
- }
221
- const factory = module.pipeline;
222
- if (typeof factory !== "function") return null;
223
- const encode = await factory("feature-extraction", modelId);
224
- let dims = 0;
225
- return {
226
- modelId,
227
- get dims() {
228
- return dims;
229
- },
230
- async embed(text) {
231
- const output = await encode(text, { pooling: "mean", normalize: true });
232
- const vector = Float32Array.from(output.data);
233
- dims = vector.length;
234
- return vector;
235
- }
236
- };
237
- }
238
- function describeHash(description) {
239
- let hash = 2166136261;
240
- for (let i = 0; i < description.length; i++) {
241
- hash ^= description.charCodeAt(i);
242
- hash = Math.imul(hash, 16777619) >>> 0;
243
- }
244
- return hash.toString(16).padStart(8, "0");
245
- }
246
- async function openEntryVectors(path) {
247
- const opened = await openDatabase(path);
248
- if (opened === null) return null;
249
- const db = opened.db;
250
- db.exec(ENTRY_SCHEMA);
251
- return {
252
- read(modelId) {
253
- const out = /* @__PURE__ */ new Map();
254
- const rows = db.prepare(
255
- "SELECT name, dims, vector FROM entry_vector WHERE model_id = ?"
256
- ).all(modelId);
257
- for (const row of rows) {
258
- const bytes = row.vector;
259
- const vector = new Float32Array(
260
- bytes.buffer.slice(
261
- bytes.byteOffset,
262
- bytes.byteOffset + bytes.byteLength
263
- )
264
- );
265
- if (vector.length === row.dims) out.set(row.name, vector);
266
- }
267
- return out;
268
- },
269
- stale(modelId, entries) {
270
- const rows = db.prepare(
271
- "SELECT name, description_hash FROM entry_vector WHERE model_id = ?"
272
- ).all(modelId);
273
- const stored = new Map(
274
- rows.map((row) => [row.name, row.description_hash])
275
- );
276
- const out = [];
277
- for (const [name, description] of entries) {
278
- if (stored.get(name) !== describeHash(description)) out.push(name);
279
- }
280
- return out;
281
- },
282
- write(modelId, name, description, vector) {
283
- db.prepare(
284
- `INSERT INTO entry_vector (name, model_id, dims, description_hash, vector)
285
- VALUES (?, ?, ?, ?, ?)
286
- ON CONFLICT(name, model_id) DO UPDATE SET
287
- dims = excluded.dims,
288
- description_hash = excluded.description_hash,
289
- vector = excluded.vector`
290
- ).run(
291
- name,
292
- modelId,
293
- vector.length,
294
- describeHash(description),
295
- new Uint8Array(vector.buffer, vector.byteOffset, vector.byteLength)
296
- );
297
- },
298
- close() {
299
- db.close();
300
- }
301
- };
302
- }
303
-
304
205
  // src/commands/roots.ts
305
206
  var CATALOG_OPTIONS = [
306
207
  {
@@ -454,7 +355,7 @@ var indexCommand = {
454
355
  return 0;
455
356
  }
456
357
  for (const entry2 of todo) {
457
- const vector = await embedder.embed(entry2.description);
358
+ const vector = embedder.embedPassage ? await embedder.embedPassage(entry2.description) : await embedder.embed(entry2.description);
458
359
  store.write(embedder.modelId, entry2.name, entry2.description, vector);
459
360
  }
460
361
  const indexed = store.read(embedder.modelId).size;
@@ -2407,7 +2308,7 @@ function dispatch(argv, options = {}) {
2407
2308
  }
2408
2309
  return run(command, rest, { ui, env, cwd });
2409
2310
  }
2410
- var VERSION = "0.6.0";
2311
+ var VERSION = "0.7.1";
2411
2312
  async function run(command, argv, context) {
2412
2313
  try {
2413
2314
  return await command.run({
package/dist/http.d.ts CHANGED
@@ -1,5 +1,40 @@
1
1
  import { S as SkillEntry } from './catalog-5v7jrGwP.js';
2
- import { S as SkillServerOptions } from './server-Dm71qZgM.js';
2
+ import { E as Embedder, S as SkillServerOptions } from './server-B9GDPRoE.js';
3
+
4
+ /**
5
+ * The optional encoder and the store for the vectors it produces.
6
+ *
7
+ * Neither the model nor its runtime is a dependency: a caller who wants
8
+ * semantic ranking installs the peer and runs `pmcp index`, and everything
9
+ * here degrades to null rather than throwing when that has not happened.
10
+ */
11
+
12
+ /**
13
+ * What `pmcp index` writes and what the server reads back. Multilingual, so a
14
+ * Korean intent finds an English description (measured 20261005: 8/8 Korean
15
+ * and 8/8 English intents in the top three over a 99-skill catalog).
16
+ */
17
+ declare const DEFAULT_MODEL_ID = "Xenova/multilingual-e5-small";
18
+ interface LoadEmbedderOptions {
19
+ /** Where model files live; a host bakes them into its image here. */
20
+ readonly cacheDir?: string;
21
+ /** Refuse to download, so a missing model is an error rather than a fetch. */
22
+ readonly localOnly?: boolean;
23
+ /**
24
+ * Weight precision. q8 is a quarter of fp32's size (118 MB against 470 MB for
25
+ * the default model) and ranked the same in the 20261005 measurement.
26
+ */
27
+ readonly dtype?: "q8" | "fp32";
28
+ /** Injected for tests. The optional peer is imported otherwise. */
29
+ readonly module?: unknown;
30
+ }
31
+ /**
32
+ * Loads the encoder, or returns null when the peer is not installed.
33
+ *
34
+ * Absence is not failure anywhere in this package: the caller reports it and
35
+ * ranking stays lexical.
36
+ */
37
+ declare function loadEmbedder(modelId?: string, options?: LoadEmbedderOptions): Promise<Embedder | null>;
3
38
 
4
39
  /**
5
40
  * The catalog over streamable HTTP, for a hosted skills server.
@@ -9,6 +44,15 @@ import { S as SkillServerOptions } from './server-Dm71qZgM.js';
9
44
  * tiers tiersFor() grants. The catalog is read once and filtered per request.
10
45
  */
11
46
 
47
+ /** Semantic ranking for a hosted catalog: the encoder runs in the host process. */
48
+ interface SkillHttpSemanticOptions extends Omit<LoadEmbedderOptions, "module"> {
49
+ /** Defaults to the multilingual model pmcp index uses. */
50
+ readonly modelId?: string;
51
+ /** Injected for tests, or by a host that already holds an encoder. */
52
+ readonly embedder?: Embedder;
53
+ /** Told when the encoder cannot load; ranking then stays lexical. */
54
+ readonly onError?: (error: unknown) => void;
55
+ }
12
56
  interface SkillCaller {
13
57
  /** Stable id of the signed-in user, recorded by the host, never by pmcp. */
14
58
  readonly id: string;
@@ -21,6 +65,8 @@ interface SkillHttpOptions extends SkillServerOptions {
21
65
  readonly tiersFor?: (caller: SkillCaller) => readonly string[];
22
66
  /** Sent in WWW-Authenticate on a refused request, e.g. a resource metadata URL. */
23
67
  readonly challenge?: string;
68
+ /** Encode the catalog at start so find ranks by meaning across languages. */
69
+ readonly semantic?: SkillHttpSemanticOptions;
24
70
  }
25
71
  declare const DEFAULT_TIERS: readonly string[];
26
72
  declare function visibleTo(entries: readonly SkillEntry[], tiers: readonly string[]): SkillEntry[];
@@ -28,7 +74,9 @@ interface SkillHttpHandler {
28
74
  fetch(request: Request): Promise<Response>;
29
75
  /** Re-read the catalog, for a host that deploys new skill releases in place. */
30
76
  reload(): void;
77
+ /** Settles once the catalog is encoded, or at once when ranking is lexical. */
78
+ ready(): Promise<"semantic" | "lexical">;
31
79
  }
32
80
  declare function createSkillHttpHandler(options: SkillHttpOptions): SkillHttpHandler;
33
81
 
34
- export { DEFAULT_TIERS, type SkillCaller, type SkillHttpHandler, type SkillHttpOptions, createSkillHttpHandler, visibleTo };
82
+ export { DEFAULT_MODEL_ID, DEFAULT_TIERS, Embedder, type SkillCaller, type SkillHttpHandler, type SkillHttpOptions, type SkillHttpSemanticOptions, createSkillHttpHandler, loadEmbedder, visibleTo };
package/dist/http.js CHANGED
@@ -1,7 +1,13 @@
1
+ import {
2
+ DEFAULT_MODEL_ID,
3
+ encodeEntries,
4
+ loadEmbedder
5
+ } from "./chunk-VBLAH6J2.js";
6
+ import "./chunk-KP3TQ3QY.js";
1
7
  import {
2
8
  createSkillServer,
3
9
  readCatalog
4
- } from "./chunk-5TX2IAVD.js";
10
+ } from "./chunk-C46YCY2I.js";
5
11
 
6
12
  // src/http.ts
7
13
  import {
@@ -17,6 +23,25 @@ function createSkillHttpHandler(options) {
17
23
  let all = load(options);
18
24
  const tiersFor = options.tiersFor ?? (() => DEFAULT_TIERS);
19
25
  const handlers = /* @__PURE__ */ new Map();
26
+ let embedder;
27
+ let encoded = /* @__PURE__ */ new Map();
28
+ let vectors;
29
+ const semantic = options.semantic;
30
+ const encode = async () => {
31
+ if (!semantic) return "lexical";
32
+ try {
33
+ embedder ??= semantic.embedder ?? await loadEmbedder(semantic.modelId, semantic) ?? void 0;
34
+ if (!embedder)
35
+ throw new Error("@huggingface/transformers is not installed");
36
+ encoded = await encodeEntries(embedder, all, encoded);
37
+ vectors = new Map([...encoded].map(([name, v]) => [name, v.vector]));
38
+ return "semantic";
39
+ } catch (error) {
40
+ semantic.onError?.(error);
41
+ return "lexical";
42
+ }
43
+ };
44
+ let ready = encode();
20
45
  const handlerFor = (tiers) => {
21
46
  const key = [...tiers].sort().join(",");
22
47
  const cached = handlers.get(key);
@@ -24,6 +49,7 @@ function createSkillHttpHandler(options) {
24
49
  const handler = createMcpHandler(
25
50
  () => createSkillServer({
26
51
  ...options,
52
+ ...embedder && vectors ? { embedder, vectors } : {},
27
53
  loadCatalog: () => visibleTo(all, tiers)
28
54
  })
29
55
  );
@@ -54,12 +80,17 @@ function createSkillHttpHandler(options) {
54
80
  reload() {
55
81
  all = load(options);
56
82
  handlers.clear();
57
- }
83
+ vectors = void 0;
84
+ ready = encode();
85
+ },
86
+ ready: () => ready
58
87
  };
59
88
  }
60
89
  export {
90
+ DEFAULT_MODEL_ID,
61
91
  DEFAULT_TIERS,
62
92
  createSkillHttpHandler,
93
+ loadEmbedder,
63
94
  visibleTo
64
95
  };
65
96
  //# sourceMappingURL=http.js.map
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { I as IntentCache, S as SkillServerOptions, a as SkillTools } from './server-Dm71qZgM.js';
2
- export { C as CATALOG_PAGE_MAX, b as CATALOG_PAGE_SIZE, c as CallResponse, d as CatalogItem, e as CatalogResponse, D as DEFAULT_FIND_LIMIT, f as DESCRIBE_FILE_LIMIT, E as Embedder, F as FindOptions, g as FindResult, h as IntentCacheStats, K as KindFilter, M as Match, R as READ_BUDGET_BYTES, i as Ranking, j as ReadResult, k as SkillFilters, l as cosine, m as createSkillTools, n as find, o as lexicalScore, p as normaliseIntent } from './server-Dm71qZgM.js';
1
+ import { I as IntentCache, S as SkillServerOptions, a as SkillTools } from './server-B9GDPRoE.js';
2
+ export { C as CATALOG_PAGE_MAX, b as CATALOG_PAGE_SIZE, c as CallResponse, d as CatalogItem, e as CatalogResponse, D as DEFAULT_FIND_LIMIT, f as DESCRIBE_FILE_LIMIT, E as Embedder, F as FindOptions, g as FindResult, h as IntentCacheStats, K as KindFilter, M as Match, R as READ_BUDGET_BYTES, i as Ranking, j as ReadResult, k as SkillFilters, l as cosine, m as createSkillTools, n as find, o as lexicalScore, p as normaliseIntent } from './server-B9GDPRoE.js';
3
3
  import { S as SkillEntry } from './catalog-5v7jrGwP.js';
4
4
  export { a as ASSET_KINDS, A as AssetKind, C as CatalogOptions, M as MAX_SKILL_DEPTH, b as SKIPPED_DIRS, d as displayPath, k as kindOf, r as readBody, c as readCatalog, e as readFrontmatter, w as withoutBody } from './catalog-5v7jrGwP.js';
5
5
  import { McpServer } from '@modelcontextprotocol/server';
package/dist/index.js CHANGED
@@ -1,7 +1,6 @@
1
1
  import {
2
2
  CONFIG_FILE,
3
3
  ConfigError,
4
- DEFAULT_INTENT_CACHE_LIMIT,
5
4
  LOCK_FILE,
6
5
  ProjectError,
7
6
  TOOLS,
@@ -9,12 +8,15 @@ import {
9
8
  deriveName,
10
9
  doctor,
11
10
  findConfig,
12
- openIntentCache,
13
11
  plan,
14
12
  project,
15
13
  readConfig,
16
14
  readSpec
17
- } from "./chunk-6TSHDCUY.js";
15
+ } from "./chunk-GNJSZCRH.js";
16
+ import {
17
+ DEFAULT_INTENT_CACHE_LIMIT,
18
+ openIntentCache
19
+ } from "./chunk-KP3TQ3QY.js";
18
20
  import {
19
21
  ASSET_KINDS,
20
22
  CATALOG_PAGE_MAX,
@@ -46,7 +48,7 @@ import {
46
48
  registerSkillsExtension,
47
49
  skillUri,
48
50
  withoutBody
49
- } from "./chunk-5TX2IAVD.js";
51
+ } from "./chunk-C46YCY2I.js";
50
52
  export {
51
53
  ASSET_KINDS,
52
54
  CATALOG_PAGE_MAX,
@@ -56,7 +56,10 @@ interface FindResult {
56
56
  interface Embedder {
57
57
  readonly modelId: string;
58
58
  readonly dims: number;
59
+ /** Encodes an intent, the query side. */
59
60
  embed(text: string): Promise<Float32Array>;
61
+ /** Encodes a description, for models that write the two sides differently. */
62
+ embedPassage?(text: string): Promise<Float32Array>;
60
63
  }
61
64
  interface FindOptions {
62
65
  readonly entries: readonly SkillEntry[];
@@ -152,6 +155,13 @@ interface CatalogItem {
152
155
  interface CallResponse {
153
156
  readonly name: string;
154
157
  readonly body: string;
158
+ /** Files the skill carries besides its body, for skill_read. */
159
+ readonly files: readonly {
160
+ path: string;
161
+ bytes: number;
162
+ sha256: string;
163
+ }[];
164
+ readonly filesTruncated: boolean;
155
165
  }
156
166
  /**
157
167
  * The catalog, the search and the read, as plain functions.
package/dist/stdio.js CHANGED
@@ -3,8 +3,8 @@ import {
3
3
  main,
4
4
  resolveRoots,
5
5
  serve
6
- } from "./chunk-FXBLBKP6.js";
7
- import "./chunk-5TX2IAVD.js";
6
+ } from "./chunk-IJPOJ6PG.js";
7
+ import "./chunk-C46YCY2I.js";
8
8
  export {
9
9
  main,
10
10
  resolveRoots,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@modootoday/pmcp",
3
- "version": "0.6.0",
3
+ "version": "0.7.1",
4
4
  "license": "Elastic-2.0",
5
5
  "author": "modootoday",
6
6
  "description": "An MCP server that finds the skills your installed packages already ship. Reads SKILL.md out of node_modules by shape rather than by path, so it sees the skills, .agent, .claude, .gemini and .codex conventions alike, and keeps session startup flat as the number of installed skills grows.",