@jim80net/memex-core 0.4.0 → 0.6.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 +42 -0
- package/dist/cache.js +1 -1
- package/dist/index.d.ts +5 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/origin.d.ts +137 -0
- package/dist/origin.d.ts.map +1 -0
- package/dist/origin.js +490 -0
- package/dist/origin.js.map +1 -0
- package/dist/portable-location.d.ts +84 -0
- package/dist/portable-location.d.ts.map +1 -0
- package/dist/portable-location.js +248 -0
- package/dist/portable-location.js.map +1 -0
- package/dist/skill-index.d.ts +19 -1
- package/dist/skill-index.d.ts.map +1 -1
- package/dist/skill-index.js +97 -33
- package/dist/skill-index.js.map +1 -1
- package/dist/types.d.ts +98 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { join, relative, resolve, sep } from "node:path";
|
|
3
|
+
export const HANDLE_PREFIX = "memex://";
|
|
4
|
+
/**
|
|
5
|
+
* Portable handles are opaque location tokens — not general URIs.
|
|
6
|
+
* Form: memex://{rootKey}/{posix-rel}[#{fragment}]
|
|
7
|
+
*/
|
|
8
|
+
/** Injective escape for rel segments and fragments (% first, then #). */
|
|
9
|
+
export function escapePortableText(text) {
|
|
10
|
+
return text.replace(/%/g, "%25").replace(/#/g, "%23");
|
|
11
|
+
}
|
|
12
|
+
/** Reverse of escapePortableText (# last on decode, then %). */
|
|
13
|
+
export function unescapePortableText(text) {
|
|
14
|
+
return text.replace(/%23/g, "#").replace(/%25/g, "%");
|
|
15
|
+
}
|
|
16
|
+
/** @alias escapePortableText */
|
|
17
|
+
export const encodeFragment = escapePortableText;
|
|
18
|
+
/** @alias unescapePortableText */
|
|
19
|
+
export const decodeFragment = unescapePortableText;
|
|
20
|
+
/**
|
|
21
|
+
* Split at the last literal '#' — escaped #s are %23 and do not split.
|
|
22
|
+
* Fragment is unescaped exactly once.
|
|
23
|
+
*/
|
|
24
|
+
export function splitPortableHandle(handle) {
|
|
25
|
+
const lastHash = handle.lastIndexOf("#");
|
|
26
|
+
if (lastHash === -1)
|
|
27
|
+
return { body: handle };
|
|
28
|
+
return {
|
|
29
|
+
body: handle.slice(0, lastHash),
|
|
30
|
+
fragment: unescapePortableText(handle.slice(lastHash + 1)),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
function encodeRelPath(rel) {
|
|
34
|
+
return normalizeRel(rel).split("/").map(escapePortableText).join("/");
|
|
35
|
+
}
|
|
36
|
+
function decodeRelPath(rel) {
|
|
37
|
+
return rel.split("/").map(unescapePortableText).join("/");
|
|
38
|
+
}
|
|
39
|
+
function normalizeRel(rel) {
|
|
40
|
+
return rel.split(sep).join("/");
|
|
41
|
+
}
|
|
42
|
+
function isAbsolutePath(p) {
|
|
43
|
+
return p.startsWith("/") || /^[A-Za-z]:[\\/]/.test(p);
|
|
44
|
+
}
|
|
45
|
+
function hasUnsafeRelSegments(rel) {
|
|
46
|
+
const segments = rel.split("/");
|
|
47
|
+
return segments.some((s) => s === ".." || s === "");
|
|
48
|
+
}
|
|
49
|
+
function isPathContained(rootPath, absolute) {
|
|
50
|
+
const root = resolve(rootPath);
|
|
51
|
+
const normalized = resolve(absolute);
|
|
52
|
+
return normalized === root || normalized.startsWith(root + sep);
|
|
53
|
+
}
|
|
54
|
+
function sortRegistry(roots) {
|
|
55
|
+
return [...roots].sort((a, b) => b.rootPath.length - a.rootPath.length);
|
|
56
|
+
}
|
|
57
|
+
function matchRoot(registry, filePath) {
|
|
58
|
+
const normalized = resolve(filePath);
|
|
59
|
+
return registry.find((r) => isPathContained(r.rootPath, normalized));
|
|
60
|
+
}
|
|
61
|
+
function pathHasSegment(resolvedPath, segment) {
|
|
62
|
+
return resolve(resolvedPath).split(sep).includes(segment);
|
|
63
|
+
}
|
|
64
|
+
/** Stable fallback rootKey from normalized absolute dir path (§4.4 path-hash option). */
|
|
65
|
+
export function stableUnclassifiedKey(kind, dir) {
|
|
66
|
+
const hash = createHash("sha256").update(resolve(dir)).digest("hex").slice(0, 8);
|
|
67
|
+
return `${kind}-unclassified-${hash}`;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Build labeled scan roots from harness context and scan directory spec.
|
|
71
|
+
* Same semantic root MUST use the same rootKey on every host (normative catalog).
|
|
72
|
+
*/
|
|
73
|
+
export function buildScanRoots(ctx, spec) {
|
|
74
|
+
const harness = ctx.harness;
|
|
75
|
+
const roots = [];
|
|
76
|
+
const seen = new Set();
|
|
77
|
+
const add = (key, dir) => {
|
|
78
|
+
const rootPath = resolve(dir);
|
|
79
|
+
if (seen.has(rootPath))
|
|
80
|
+
return;
|
|
81
|
+
seen.add(rootPath);
|
|
82
|
+
roots.push({ key, rootPath });
|
|
83
|
+
};
|
|
84
|
+
const globalSkillKeys = new Map();
|
|
85
|
+
for (const dir of ctx.globalSkillsDirs) {
|
|
86
|
+
const resolved = resolve(dir);
|
|
87
|
+
if (pathHasSegment(resolved, ".grok")) {
|
|
88
|
+
globalSkillKeys.set(resolved, "grok-global");
|
|
89
|
+
}
|
|
90
|
+
else if (pathHasSegment(resolved, ".claude")) {
|
|
91
|
+
globalSkillKeys.set(resolved, "claude-global");
|
|
92
|
+
}
|
|
93
|
+
else if (pathHasSegment(resolved, ".codex")) {
|
|
94
|
+
globalSkillKeys.set(resolved, "codex-global");
|
|
95
|
+
}
|
|
96
|
+
else if (pathHasSegment(resolved, ".hermes")) {
|
|
97
|
+
globalSkillKeys.set(resolved, "hermes-global");
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
const globalRuleKeys = new Map();
|
|
101
|
+
for (const dir of ctx.globalRulesDirs) {
|
|
102
|
+
const resolved = resolve(dir);
|
|
103
|
+
if (pathHasSegment(resolved, ".grok")) {
|
|
104
|
+
globalRuleKeys.set(resolved, "grok-rules-global");
|
|
105
|
+
}
|
|
106
|
+
else if (pathHasSegment(resolved, ".claude")) {
|
|
107
|
+
globalRuleKeys.set(resolved, "claude-rules-global");
|
|
108
|
+
}
|
|
109
|
+
else if (pathHasSegment(resolved, ".codex")) {
|
|
110
|
+
globalRuleKeys.set(resolved, "codex-rules-global");
|
|
111
|
+
}
|
|
112
|
+
else if (pathHasSegment(resolved, ".hermes")) {
|
|
113
|
+
globalRuleKeys.set(resolved, "hermes-rules-global");
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
const projectSkillsResolved = resolve(ctx.projectSkillsDir);
|
|
117
|
+
const projectRulesResolved = resolve(ctx.projectRulesDir);
|
|
118
|
+
const syncSkillsDir = ctx.syncEnabled && ctx.syncRepoDir ? resolve(join(ctx.syncRepoDir, "skills")) : undefined;
|
|
119
|
+
const syncRulesDir = ctx.syncEnabled && ctx.syncRepoDir ? resolve(join(ctx.syncRepoDir, "rules")) : undefined;
|
|
120
|
+
const unclassifiedSkillDirs = [];
|
|
121
|
+
for (const dir of spec.skillDirs) {
|
|
122
|
+
const resolved = resolve(dir);
|
|
123
|
+
const catalogKey = globalSkillKeys.get(resolved);
|
|
124
|
+
if (catalogKey) {
|
|
125
|
+
add(catalogKey, dir);
|
|
126
|
+
}
|
|
127
|
+
else if (resolved === projectSkillsResolved) {
|
|
128
|
+
add(`${harness}-project`, dir);
|
|
129
|
+
}
|
|
130
|
+
else if (syncSkillsDir && resolved === syncSkillsDir) {
|
|
131
|
+
add("sync-skills", dir);
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
unclassifiedSkillDirs.push(dir);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
for (const dir of unclassifiedSkillDirs) {
|
|
138
|
+
add(stableUnclassifiedKey("skill", dir), dir);
|
|
139
|
+
}
|
|
140
|
+
const unclassifiedRuleDirs = [];
|
|
141
|
+
for (const dir of spec.ruleDirs) {
|
|
142
|
+
const resolved = resolve(dir);
|
|
143
|
+
const catalogKey = globalRuleKeys.get(resolved);
|
|
144
|
+
if (catalogKey) {
|
|
145
|
+
add(catalogKey, dir);
|
|
146
|
+
}
|
|
147
|
+
else if (resolved === projectRulesResolved) {
|
|
148
|
+
add(`${harness}-rules-project`, dir);
|
|
149
|
+
}
|
|
150
|
+
else if (syncRulesDir && resolved === syncRulesDir) {
|
|
151
|
+
add("sync-rules", dir);
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
unclassifiedRuleDirs.push(dir);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
for (const dir of unclassifiedRuleDirs) {
|
|
158
|
+
add(stableUnclassifiedKey("rule", dir), dir);
|
|
159
|
+
}
|
|
160
|
+
for (const dir of spec.memoryDirs) {
|
|
161
|
+
add(stableUnclassifiedKey("memory", dir), dir);
|
|
162
|
+
}
|
|
163
|
+
return sortRegistry(roots);
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Absolute path (+ optional #fragment) → portable handle.
|
|
167
|
+
* Fail-open: returns null when mapping is impossible (caller should skip-with-warning).
|
|
168
|
+
*/
|
|
169
|
+
export function encodePortableLocation(registry, absolute, warn, fragment) {
|
|
170
|
+
const body = resolve(absolute);
|
|
171
|
+
const root = matchRoot(registry, body);
|
|
172
|
+
if (!root) {
|
|
173
|
+
warn?.(`skipped indexing ${absolute} — no portable handle (outside registered scan roots)`);
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
const rel = normalizeRel(relative(root.rootPath, body));
|
|
177
|
+
if (hasUnsafeRelSegments(rel)) {
|
|
178
|
+
warn?.(`skipped indexing ${absolute} — no portable handle (unsafe relative path: ${rel})`);
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
const handle = `${HANDLE_PREFIX}${root.key}/${encodeRelPath(rel)}`;
|
|
182
|
+
if (fragment === undefined)
|
|
183
|
+
return handle;
|
|
184
|
+
return `${handle}#${escapePortableText(fragment)}`;
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Portable handle → absolute file path + optional section name.
|
|
188
|
+
* Fragment is split and %23-unescaped exactly once — never rejoin with '#'.
|
|
189
|
+
* Fail-closed on traversal escape.
|
|
190
|
+
*/
|
|
191
|
+
export function decodePortableLocationResolved(registry, handle) {
|
|
192
|
+
if (!handle.startsWith(HANDLE_PREFIX)) {
|
|
193
|
+
throw new Error(`invalid portable location handle: ${handle}`);
|
|
194
|
+
}
|
|
195
|
+
const { body, fragment: sectionName } = splitPortableHandle(handle);
|
|
196
|
+
const pathBody = body.slice(HANDLE_PREFIX.length);
|
|
197
|
+
const slash = pathBody.indexOf("/");
|
|
198
|
+
if (slash === -1) {
|
|
199
|
+
throw new Error(`invalid portable location handle (missing path segment): ${handle}`);
|
|
200
|
+
}
|
|
201
|
+
const key = pathBody.slice(0, slash);
|
|
202
|
+
const rel = decodeRelPath(pathBody.slice(slash + 1));
|
|
203
|
+
if (hasUnsafeRelSegments(rel)) {
|
|
204
|
+
throw new Error(`portable location handle escapes scan root: ${handle}`);
|
|
205
|
+
}
|
|
206
|
+
const root = registry.find((r) => r.key === key);
|
|
207
|
+
if (!root) {
|
|
208
|
+
throw new Error(`unknown portable location root '${key}'`);
|
|
209
|
+
}
|
|
210
|
+
const filePath = resolve(root.rootPath, rel);
|
|
211
|
+
if (!isPathContained(root.rootPath, filePath)) {
|
|
212
|
+
throw new Error(`portable location handle escapes scan root: ${handle}`);
|
|
213
|
+
}
|
|
214
|
+
return sectionName !== undefined ? { filePath, sectionName } : { filePath };
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Portable handle → absolute path string (legacy join form).
|
|
218
|
+
* Prefer decodePortableLocationResolved when a fragment may be present.
|
|
219
|
+
*/
|
|
220
|
+
export function decodePortableLocation(registry, handle) {
|
|
221
|
+
const { filePath, sectionName } = decodePortableLocationResolved(registry, handle);
|
|
222
|
+
return sectionName !== undefined ? `${filePath}#${sectionName}` : filePath;
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Resolve portable handle or legacy absolute path for filesystem I/O.
|
|
226
|
+
* Phase 2: agent-facing read tools MUST pass `allowAbsolute: false` so untrusted
|
|
227
|
+
* absolute paths cannot bypass containment (memex-grok#19 closes only then).
|
|
228
|
+
*/
|
|
229
|
+
export function resolvePortableLocationResolved(registry, input, options) {
|
|
230
|
+
const trimmed = input.trim();
|
|
231
|
+
if (!trimmed)
|
|
232
|
+
return { filePath: trimmed };
|
|
233
|
+
if (trimmed.startsWith(HANDLE_PREFIX)) {
|
|
234
|
+
return decodePortableLocationResolved(registry, trimmed);
|
|
235
|
+
}
|
|
236
|
+
if (options?.allowAbsolute !== false && isAbsolutePath(trimmed)) {
|
|
237
|
+
options?.warn?.(`deprecated: readSkillContent received absolute path; use portable memex:// handles`);
|
|
238
|
+
const { body, fragment } = splitPortableHandle(trimmed);
|
|
239
|
+
return fragment !== undefined ? { filePath: body, sectionName: fragment } : { filePath: body };
|
|
240
|
+
}
|
|
241
|
+
throw new Error(`unrecognized location: ${input}`);
|
|
242
|
+
}
|
|
243
|
+
/** @deprecated Prefer resolvePortableLocationResolved to avoid fragment double-decode. */
|
|
244
|
+
export function resolvePortableLocation(registry, input, options) {
|
|
245
|
+
const { filePath, sectionName } = resolvePortableLocationResolved(registry, input, options);
|
|
246
|
+
return sectionName !== undefined ? `${filePath}#${sectionName}` : filePath;
|
|
247
|
+
}
|
|
248
|
+
//# sourceMappingURL=portable-location.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"portable-location.js","sourceRoot":"","sources":["../src/portable-location.ts"],"names":[],"mappings":"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"}
|
package/dist/skill-index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { EmbeddingProvider } from "./embeddings.js";
|
|
2
|
+
import { type PortableLocationWarn, type ScanRootRegistry } from "./portable-location.js";
|
|
2
3
|
import type { MemexCoreConfig, ParsedFrontmatter, ScoringMode, SkillSearchResult, SkillType } from "./types.js";
|
|
3
4
|
export declare function parseFrontmatter(content: string): {
|
|
4
5
|
meta: ParsedFrontmatter;
|
|
@@ -37,16 +38,33 @@ export type ScanDirs = {
|
|
|
37
38
|
memoryDirs: string[];
|
|
38
39
|
ruleDirs: string[];
|
|
39
40
|
};
|
|
41
|
+
export type SkillIndexOptions = {
|
|
42
|
+
registry?: ScanRootRegistry;
|
|
43
|
+
warn?: PortableLocationWarn;
|
|
44
|
+
};
|
|
40
45
|
export declare class SkillIndex {
|
|
41
46
|
private config;
|
|
42
47
|
private provider;
|
|
43
48
|
private cachePath;
|
|
49
|
+
private options;
|
|
44
50
|
private skills;
|
|
45
51
|
private skillMtimes;
|
|
46
52
|
private cache;
|
|
47
53
|
private cacheLoaded;
|
|
48
54
|
private buildTime;
|
|
49
|
-
constructor(config: MemexCoreConfig, provider: EmbeddingProvider, cachePath: string);
|
|
55
|
+
constructor(config: MemexCoreConfig, provider: EmbeddingProvider, cachePath: string, options?: SkillIndexOptions);
|
|
56
|
+
private get registry();
|
|
57
|
+
private warn;
|
|
58
|
+
/** Map absolute scan path to persisted location (portable handle or absolute). */
|
|
59
|
+
private toStoredLocation;
|
|
60
|
+
/** Absolute path used for mtime tracking (scan truth). Fail-closed — agent read paths only. */
|
|
61
|
+
private mtimeKeyFromStored;
|
|
62
|
+
/**
|
|
63
|
+
* Cache-key decode — fail-open. Orphaned handles (registry changed) return null
|
|
64
|
+
* so build() skips/re-embeds instead of bricking the whole index.
|
|
65
|
+
*/
|
|
66
|
+
private mtimeKeyFromStoredSafe;
|
|
67
|
+
private memorySectionPrefix;
|
|
50
68
|
get skillCount(): number;
|
|
51
69
|
needsRebuild(): boolean;
|
|
52
70
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"skill-index.d.ts","sourceRoot":"","sources":["../src/skill-index.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAEzD,OAAO,KAAK,EAGV,eAAe,EACf,iBAAiB,EACjB,WAAW,EACX,iBAAiB,EACjB,SAAS,EACV,MAAM,YAAY,CAAC;AAQpB,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG;IAAE,IAAI,EAAE,iBAAiB,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAyD3F;AAMD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,eAAe,CAC7B,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,MAAM,GACf,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,EAAE,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,CAiD/E;AA4ED,MAAM,MAAM,QAAQ,GAAG;IACrB,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB,CAAC;AAkBF,qBAAa,UAAU;IAQnB,OAAO,CAAC,MAAM;IACd,OAAO,CAAC,QAAQ;IAChB,OAAO,CAAC,SAAS;
|
|
1
|
+
{"version":3,"file":"skill-index.d.ts","sourceRoot":"","sources":["../src/skill-index.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAEzD,OAAO,EAKL,KAAK,oBAAoB,EAEzB,KAAK,gBAAgB,EAEtB,MAAM,wBAAwB,CAAC;AAChC,OAAO,KAAK,EAGV,eAAe,EACf,iBAAiB,EACjB,WAAW,EACX,iBAAiB,EACjB,SAAS,EACV,MAAM,YAAY,CAAC;AAQpB,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG;IAAE,IAAI,EAAE,iBAAiB,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAyD3F;AAMD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,eAAe,CAC7B,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,MAAM,GACf,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,EAAE,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,CAiD/E;AA4ED,MAAM,MAAM,QAAQ,GAAG;IACrB,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,iBAAiB,GAAG;IAC9B,QAAQ,CAAC,EAAE,gBAAgB,CAAC;IAC5B,IAAI,CAAC,EAAE,oBAAoB,CAAC;CAC7B,CAAC;AAkBF,qBAAa,UAAU;IAQnB,OAAO,CAAC,MAAM;IACd,OAAO,CAAC,QAAQ;IAChB,OAAO,CAAC,SAAS;IACjB,OAAO,CAAC,OAAO;IAVjB,OAAO,CAAC,MAAM,CAAsB;IACpC,OAAO,CAAC,WAAW,CAAkC;IACrD,OAAO,CAAC,KAAK,CAA0B;IACvC,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,SAAS,CAAK;gBAGZ,MAAM,EAAE,eAAe,EACvB,QAAQ,EAAE,iBAAiB,EAC3B,SAAS,EAAE,MAAM,EACjB,OAAO,GAAE,iBAAsB;IAGzC,OAAO,KAAK,QAAQ,GAEnB;IAED,OAAO,CAAC,IAAI;IAIZ,kFAAkF;IAClF,OAAO,CAAC,gBAAgB;IAKxB,+FAA+F;IAC/F,OAAO,CAAC,kBAAkB;IAa1B;;;OAGG;IACH,OAAO,CAAC,sBAAsB;IAU9B,OAAO,CAAC,mBAAmB;IAK3B,IAAI,UAAU,IAAI,MAAM,CAEvB;IAED,YAAY,IAAI,OAAO;IAKvB;;;OAGG;IACG,KAAK,CAAC,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC;IA+L9C;;;OAGG;IACG,MAAM,CACV,KAAK,EAAE,MAAM,EACb,IAAI,EAAE,MAAM,EACZ,SAAS,EAAE,MAAM,EACjB,UAAU,CAAC,EAAE,SAAS,EAAE,EACxB,WAAW,GAAE,WAAwB,EACrC,UAAU,GAAE,MAAa,GACxB,OAAO,CAAC,iBAAiB,EAAE,CAAC;IA0C/B;;OAEG;IACG,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IA2BzD,OAAO,CAAC,sBAAsB;IAuB9B,OAAO,CAAC,uBAAuB;IAuB/B,OAAO,CAAC,qBAAqB;CA6B9B"}
|
package/dist/skill-index.js
CHANGED
|
@@ -2,6 +2,7 @@ import { readdir, readFile, stat } from "node:fs/promises";
|
|
|
2
2
|
import { basename, join } from "node:path";
|
|
3
3
|
import { fromCachedSkill, loadCache, saveCache, toCachedSkill } from "./cache.js";
|
|
4
4
|
import { cosineSimilarity } from "./embeddings.js";
|
|
5
|
+
import { decodePortableLocation, encodeFragment, encodePortableLocation, HANDLE_PREFIX, resolvePortableLocationResolved, splitPortableHandle, } from "./portable-location.js";
|
|
5
6
|
// ---------------------------------------------------------------------------
|
|
6
7
|
// Frontmatter parsing
|
|
7
8
|
// ---------------------------------------------------------------------------
|
|
@@ -206,15 +207,60 @@ export class SkillIndex {
|
|
|
206
207
|
config;
|
|
207
208
|
provider;
|
|
208
209
|
cachePath;
|
|
210
|
+
options;
|
|
209
211
|
skills = [];
|
|
210
212
|
skillMtimes = new Map();
|
|
211
213
|
cache = null;
|
|
212
214
|
cacheLoaded = false;
|
|
213
215
|
buildTime = 0;
|
|
214
|
-
constructor(config, provider, cachePath) {
|
|
216
|
+
constructor(config, provider, cachePath, options = {}) {
|
|
215
217
|
this.config = config;
|
|
216
218
|
this.provider = provider;
|
|
217
219
|
this.cachePath = cachePath;
|
|
220
|
+
this.options = options;
|
|
221
|
+
}
|
|
222
|
+
get registry() {
|
|
223
|
+
return this.options.registry;
|
|
224
|
+
}
|
|
225
|
+
warn(msg) {
|
|
226
|
+
this.options.warn?.(msg);
|
|
227
|
+
}
|
|
228
|
+
/** Map absolute scan path to persisted location (portable handle or absolute). */
|
|
229
|
+
toStoredLocation(absolute) {
|
|
230
|
+
if (!this.registry)
|
|
231
|
+
return absolute;
|
|
232
|
+
return encodePortableLocation(this.registry, absolute, (m) => this.warn(m));
|
|
233
|
+
}
|
|
234
|
+
/** Absolute path used for mtime tracking (scan truth). Fail-closed — agent read paths only. */
|
|
235
|
+
mtimeKeyFromStored(stored) {
|
|
236
|
+
if (!this.registry) {
|
|
237
|
+
const hash = stored.indexOf("#");
|
|
238
|
+
return hash === -1 ? stored : stored.slice(0, hash);
|
|
239
|
+
}
|
|
240
|
+
const { body } = splitPortableHandle(stored);
|
|
241
|
+
if (body.startsWith(HANDLE_PREFIX)) {
|
|
242
|
+
return decodePortableLocation(this.registry, body);
|
|
243
|
+
}
|
|
244
|
+
const hash = body.indexOf("#");
|
|
245
|
+
return hash === -1 ? body : body.slice(0, hash);
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Cache-key decode — fail-open. Orphaned handles (registry changed) return null
|
|
249
|
+
* so build() skips/re-embeds instead of bricking the whole index.
|
|
250
|
+
*/
|
|
251
|
+
mtimeKeyFromStoredSafe(stored) {
|
|
252
|
+
try {
|
|
253
|
+
return this.mtimeKeyFromStored(stored);
|
|
254
|
+
}
|
|
255
|
+
catch (err) {
|
|
256
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
257
|
+
this.warn(`skipped unresolvable cached handle ${stored} — ${reason}`);
|
|
258
|
+
return null;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
memorySectionPrefix(absoluteFile) {
|
|
262
|
+
const base = this.toStoredLocation(absoluteFile);
|
|
263
|
+
return base ? `${base}#` : null;
|
|
218
264
|
}
|
|
219
265
|
get skillCount() {
|
|
220
266
|
return this.skills.length;
|
|
@@ -236,8 +282,13 @@ export class SkillIndex {
|
|
|
236
282
|
// Hydrate from cache on cold start
|
|
237
283
|
if (this.skills.length === 0 && Object.keys(this.cache.skills).length > 0) {
|
|
238
284
|
for (const [location, cached] of Object.entries(this.cache.skills)) {
|
|
285
|
+
const mtimeKey = this.mtimeKeyFromStoredSafe(location);
|
|
286
|
+
if (mtimeKey === null) {
|
|
287
|
+
delete this.cache.skills[location];
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
239
290
|
this.skills.push(fromCachedSkill(location, cached));
|
|
240
|
-
this.skillMtimes.set(
|
|
291
|
+
this.skillMtimes.set(mtimeKey, cached.mtime);
|
|
241
292
|
}
|
|
242
293
|
}
|
|
243
294
|
}
|
|
@@ -289,17 +340,21 @@ export class SkillIndex {
|
|
|
289
340
|
// Find files that need (re)embedding
|
|
290
341
|
const toEmbed = [];
|
|
291
342
|
for (const info of statResults) {
|
|
343
|
+
const storedLocation = this.toStoredLocation(info.location);
|
|
344
|
+
if (storedLocation === null)
|
|
345
|
+
continue;
|
|
292
346
|
if (info.kind === "memory") {
|
|
293
|
-
// Memory files may produce multiple sections — each keyed as "path#SectionName"
|
|
294
347
|
const cachedMtime = this.skillMtimes.get(info.location);
|
|
295
348
|
if (cachedMtime === info.mtime)
|
|
296
349
|
continue;
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
350
|
+
const sectionPrefix = this.memorySectionPrefix(info.location);
|
|
351
|
+
if (sectionPrefix) {
|
|
352
|
+
this.skills = this.skills.filter((s) => !s.location.startsWith(sectionPrefix));
|
|
353
|
+
if (this.cache) {
|
|
354
|
+
for (const key of Object.keys(this.cache.skills)) {
|
|
355
|
+
if (key.startsWith(sectionPrefix))
|
|
356
|
+
delete this.cache.skills[key];
|
|
357
|
+
}
|
|
303
358
|
}
|
|
304
359
|
}
|
|
305
360
|
try {
|
|
@@ -312,31 +367,28 @@ export class SkillIndex {
|
|
|
312
367
|
this.skillMtimes.set(info.location, info.mtime);
|
|
313
368
|
continue;
|
|
314
369
|
}
|
|
315
|
-
|
|
316
|
-
const cached = this.cache?.skills[info.location];
|
|
370
|
+
const cached = this.cache?.skills[storedLocation];
|
|
317
371
|
if (cached && cached.mtime === info.mtime) {
|
|
318
|
-
|
|
319
|
-
const
|
|
320
|
-
const skill = fromCachedSkill(info.location, cached);
|
|
372
|
+
const existing = this.skills.findIndex((s) => s.location === storedLocation);
|
|
373
|
+
const skill = fromCachedSkill(storedLocation, cached);
|
|
321
374
|
if (existing >= 0)
|
|
322
375
|
this.skills[existing] = skill;
|
|
323
|
-
else if (!this.skills.some((s) => s.location ===
|
|
376
|
+
else if (!this.skills.some((s) => s.location === storedLocation))
|
|
324
377
|
this.skills.push(skill);
|
|
325
378
|
this.skillMtimes.set(info.location, info.mtime);
|
|
326
379
|
continue;
|
|
327
380
|
}
|
|
328
|
-
// Check in-memory cache
|
|
329
381
|
const unchanged = this.skillMtimes.get(info.location) === info.mtime &&
|
|
330
|
-
this.skills.some((s) => s.location ===
|
|
382
|
+
this.skills.some((s) => s.location === storedLocation);
|
|
331
383
|
if (unchanged)
|
|
332
384
|
continue;
|
|
333
385
|
try {
|
|
334
386
|
const raw = await readFile(info.location, "utf-8");
|
|
335
387
|
if (info.kind === "rule") {
|
|
336
|
-
this.parseRuleFileForEmbed(raw, info, toEmbed);
|
|
388
|
+
this.parseRuleFileForEmbed(raw, info, toEmbed, storedLocation);
|
|
337
389
|
}
|
|
338
390
|
else {
|
|
339
|
-
this.parseSkillFileForEmbed(raw, info, toEmbed);
|
|
391
|
+
this.parseSkillFileForEmbed(raw, info, toEmbed, storedLocation);
|
|
340
392
|
}
|
|
341
393
|
}
|
|
342
394
|
catch {
|
|
@@ -372,16 +424,17 @@ export class SkillIndex {
|
|
|
372
424
|
offset += item.queries.length;
|
|
373
425
|
}
|
|
374
426
|
}
|
|
375
|
-
// Remove deleted entries (handle memory section keys
|
|
427
|
+
// Remove deleted entries (handle memory section keys)
|
|
376
428
|
this.skills = this.skills.filter((s) => {
|
|
377
|
-
const baseLocation =
|
|
378
|
-
|
|
429
|
+
const baseLocation = this.mtimeKeyFromStoredSafe(s.location);
|
|
430
|
+
if (baseLocation === null)
|
|
431
|
+
return false;
|
|
432
|
+
return currentLocations.has(baseLocation);
|
|
379
433
|
});
|
|
380
|
-
// Clean cache of deleted entries
|
|
381
434
|
if (this.cache) {
|
|
382
435
|
for (const key of Object.keys(this.cache.skills)) {
|
|
383
|
-
const baseKey =
|
|
384
|
-
if (
|
|
436
|
+
const baseKey = this.mtimeKeyFromStoredSafe(key);
|
|
437
|
+
if (baseKey === null || !currentLocations.has(baseKey)) {
|
|
385
438
|
delete this.cache.skills[key];
|
|
386
439
|
}
|
|
387
440
|
}
|
|
@@ -441,21 +494,29 @@ export class SkillIndex {
|
|
|
441
494
|
* Read the body content of a skill file, stripping frontmatter.
|
|
442
495
|
*/
|
|
443
496
|
async readSkillContent(location) {
|
|
444
|
-
|
|
445
|
-
|
|
497
|
+
const trimmed = location.trim();
|
|
498
|
+
const { filePath, sectionName } = this.registry
|
|
499
|
+
? resolvePortableLocationResolved(this.registry, trimmed, { warn: (m) => this.warn(m) })
|
|
500
|
+
: (() => {
|
|
501
|
+
const { body, fragment } = splitPortableHandle(trimmed);
|
|
502
|
+
return fragment !== undefined
|
|
503
|
+
? { filePath: body, sectionName: fragment }
|
|
504
|
+
: { filePath: body };
|
|
505
|
+
})();
|
|
506
|
+
if (sectionName !== undefined) {
|
|
446
507
|
const raw = await readFile(filePath, "utf-8");
|
|
447
508
|
const sections = parseMemoryFile(raw, filePath);
|
|
448
509
|
const section = sections.find((s) => s.name === sectionName);
|
|
449
510
|
return section?.body.trim() || "";
|
|
450
511
|
}
|
|
451
|
-
const raw = await readFile(
|
|
512
|
+
const raw = await readFile(filePath, "utf-8");
|
|
452
513
|
const { body } = parseFrontmatter(raw);
|
|
453
514
|
return body.trim();
|
|
454
515
|
}
|
|
455
516
|
// -------------------------------------------------------------------------
|
|
456
517
|
// Private parsing helpers
|
|
457
518
|
// -------------------------------------------------------------------------
|
|
458
|
-
parseSkillFileForEmbed(raw, info, toEmbed) {
|
|
519
|
+
parseSkillFileForEmbed(raw, info, toEmbed, storedLocation) {
|
|
459
520
|
const { meta, body } = parseFrontmatter(raw);
|
|
460
521
|
if (!meta.name || !meta.description)
|
|
461
522
|
return;
|
|
@@ -464,7 +525,7 @@ export class SkillIndex {
|
|
|
464
525
|
toEmbed.push({
|
|
465
526
|
name: meta.name,
|
|
466
527
|
description: meta.description,
|
|
467
|
-
location:
|
|
528
|
+
location: storedLocation,
|
|
468
529
|
queries,
|
|
469
530
|
type,
|
|
470
531
|
mtime: info.mtime,
|
|
@@ -474,9 +535,12 @@ export class SkillIndex {
|
|
|
474
535
|
});
|
|
475
536
|
}
|
|
476
537
|
parseMemoryFileForEmbed(raw, info, toEmbed) {
|
|
538
|
+
const baseStored = this.toStoredLocation(info.location);
|
|
539
|
+
if (!baseStored)
|
|
540
|
+
return;
|
|
477
541
|
const sections = parseMemoryFile(raw, info.location);
|
|
478
542
|
for (const section of sections) {
|
|
479
|
-
const key = `${
|
|
543
|
+
const key = `${baseStored}#${encodeFragment(section.name)}`;
|
|
480
544
|
const queries = section.queries.length > 0 ? section.queries : [section.description];
|
|
481
545
|
toEmbed.push({
|
|
482
546
|
name: section.name,
|
|
@@ -489,7 +553,7 @@ export class SkillIndex {
|
|
|
489
553
|
});
|
|
490
554
|
}
|
|
491
555
|
}
|
|
492
|
-
parseRuleFileForEmbed(raw, info, toEmbed) {
|
|
556
|
+
parseRuleFileForEmbed(raw, info, toEmbed, storedLocation) {
|
|
493
557
|
const { meta, body } = parseFrontmatter(raw);
|
|
494
558
|
const name = meta.name || basename(info.location, ".md");
|
|
495
559
|
const description = meta.description || body.split("\n")[0]?.trim() || name;
|
|
@@ -504,7 +568,7 @@ export class SkillIndex {
|
|
|
504
568
|
toEmbed.push({
|
|
505
569
|
name,
|
|
506
570
|
description,
|
|
507
|
-
location:
|
|
571
|
+
location: storedLocation,
|
|
508
572
|
queries,
|
|
509
573
|
type: "rule",
|
|
510
574
|
mtime: info.mtime,
|