@modootoday/pmcp 0.6.0 → 0.7.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/README.md +10 -3
- package/dist/{chunk-6TSHDCUY.js → chunk-GNJSZCRH.js} +1 -132
- package/dist/chunk-NI2OLZPA.js +160 -0
- package/dist/{chunk-FXBLBKP6.js → chunk-POJOFL4L.js} +2 -2
- package/dist/{chunk-5TX2IAVD.js → chunk-QHFORYIN.js} +2 -2
- package/dist/chunk-ZLELYZHL.js +134 -0
- package/dist/cli.js +10 -109
- package/dist/http.d.ts +50 -2
- package/dist/http.js +33 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +6 -4
- package/dist/{server-Dm71qZgM.d.ts → server-wpYiAJ2n.d.ts} +3 -0
- package/dist/stdio.js +2 -2
- package/package.json +1 -1
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.
|
|
166
|
-
`index` command
|
|
167
|
-
default
|
|
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>.
|
|
@@ -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-
|
|
1107
|
+
//# sourceMappingURL=chunk-GNJSZCRH.js.map
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import {
|
|
2
|
+
openDatabase
|
|
3
|
+
} from "./chunk-ZLELYZHL.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-NI2OLZPA.js.map
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
createSkillServer
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-QHFORYIN.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-
|
|
38
|
+
//# sourceMappingURL=chunk-POJOFL4L.js.map
|
|
@@ -1129,7 +1129,7 @@ function registerSkillTools(server, tools) {
|
|
|
1129
1129
|
return server;
|
|
1130
1130
|
}
|
|
1131
1131
|
function createSkillServer(options) {
|
|
1132
|
-
const server = new McpServer({ name: SERVER_NAME, version: "0.
|
|
1132
|
+
const server = new McpServer({ name: SERVER_NAME, version: "0.7.0" });
|
|
1133
1133
|
const tools = createSkillTools(options);
|
|
1134
1134
|
registerSkillsExtension(server, tools.skills);
|
|
1135
1135
|
return registerSkillTools(server, tools);
|
|
@@ -1167,4 +1167,4 @@ export {
|
|
|
1167
1167
|
registerSkillTools,
|
|
1168
1168
|
createSkillServer
|
|
1169
1169
|
};
|
|
1170
|
-
//# sourceMappingURL=chunk-
|
|
1170
|
+
//# sourceMappingURL=chunk-QHFORYIN.js.map
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import {
|
|
2
|
+
normaliseIntent
|
|
3
|
+
} from "./chunk-QHFORYIN.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-ZLELYZHL.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-NI2OLZPA.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-
|
|
18
|
+
} from "./chunk-GNJSZCRH.js";
|
|
19
|
+
import "./chunk-ZLELYZHL.js";
|
|
16
20
|
import {
|
|
17
21
|
serve
|
|
18
|
-
} from "./chunk-
|
|
22
|
+
} from "./chunk-POJOFL4L.js";
|
|
19
23
|
import {
|
|
20
24
|
kindOf,
|
|
21
25
|
readCatalog
|
|
22
|
-
} from "./chunk-
|
|
26
|
+
} from "./chunk-QHFORYIN.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.
|
|
2311
|
+
var VERSION = "0.7.0";
|
|
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-
|
|
2
|
+
import { E as Embedder, S as SkillServerOptions } from './server-wpYiAJ2n.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-NI2OLZPA.js";
|
|
6
|
+
import "./chunk-ZLELYZHL.js";
|
|
1
7
|
import {
|
|
2
8
|
createSkillServer,
|
|
3
9
|
readCatalog
|
|
4
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-QHFORYIN.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-
|
|
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-
|
|
1
|
+
import { I as IntentCache, S as SkillServerOptions, a as SkillTools } from './server-wpYiAJ2n.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-wpYiAJ2n.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-
|
|
15
|
+
} from "./chunk-GNJSZCRH.js";
|
|
16
|
+
import {
|
|
17
|
+
DEFAULT_INTENT_CACHE_LIMIT,
|
|
18
|
+
openIntentCache
|
|
19
|
+
} from "./chunk-ZLELYZHL.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-
|
|
51
|
+
} from "./chunk-QHFORYIN.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[];
|
package/dist/stdio.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@modootoday/pmcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
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.",
|