@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.
@@ -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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"}
@@ -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;IATnB,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;IAG3B,IAAI,UAAU,IAAI,MAAM,CAEvB;IAED,YAAY,IAAI,OAAO;IAKvB;;;OAGG;IACG,KAAK,CAAC,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC;IAyL9C;;;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;IAkBzD,OAAO,CAAC,sBAAsB;IAsB9B,OAAO,CAAC,uBAAuB;IAqB/B,OAAO,CAAC,qBAAqB;CA4B9B"}
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"}
@@ -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(location, cached.mtime);
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
- // Remove old sections for this memory file
298
- this.skills = this.skills.filter((s) => !s.location.startsWith(`${info.location}#`));
299
- if (this.cache) {
300
- for (const key of Object.keys(this.cache.skills)) {
301
- if (key.startsWith(`${info.location}#`))
302
- delete this.cache.skills[key];
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
- // Skills and rules
316
- const cached = this.cache?.skills[info.location];
370
+ const cached = this.cache?.skills[storedLocation];
317
371
  if (cached && cached.mtime === info.mtime) {
318
- // Use cached embeddings no re-embed
319
- const existing = this.skills.findIndex((s) => s.location === info.location);
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 === info.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 === info.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 like "path#SectionName")
427
+ // Remove deleted entries (handle memory section keys)
376
428
  this.skills = this.skills.filter((s) => {
377
- const baseLocation = s.location.includes("#") ? s.location.split("#")[0] : s.location;
378
- return currentLocations.has(baseLocation) || currentLocations.has(s.location);
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 = key.includes("#") ? key.split("#")[0] : key;
384
- if (!currentLocations.has(baseKey) && !currentLocations.has(key)) {
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
- if (location.includes("#")) {
445
- const [filePath, sectionName] = location.split("#", 2);
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(location, "utf-8");
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: info.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 = `${info.location}#${section.name}`;
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: info.location,
571
+ location: storedLocation,
508
572
  queries,
509
573
  type: "rule",
510
574
  mtime: info.mtime,