@jim80net/memex-core 0.1.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.
Files changed (61) hide show
  1. package/dist/cache.d.ts +9 -0
  2. package/dist/cache.d.ts.map +1 -0
  3. package/dist/cache.js +57 -0
  4. package/dist/cache.js.map +1 -0
  5. package/dist/config.d.ts +8 -0
  6. package/dist/config.d.ts.map +1 -0
  7. package/dist/config.js +47 -0
  8. package/dist/config.js.map +1 -0
  9. package/dist/embeddings.d.ts +24 -0
  10. package/dist/embeddings.d.ts.map +1 -0
  11. package/dist/embeddings.js +143 -0
  12. package/dist/embeddings.js.map +1 -0
  13. package/dist/file-lock.d.ts +11 -0
  14. package/dist/file-lock.d.ts.map +1 -0
  15. package/dist/file-lock.js +63 -0
  16. package/dist/file-lock.js.map +1 -0
  17. package/dist/index.d.ts +16 -0
  18. package/dist/index.d.ts.map +1 -0
  19. package/dist/index.js +15 -0
  20. package/dist/index.js.map +1 -0
  21. package/dist/path-encoder.d.ts +12 -0
  22. package/dist/path-encoder.d.ts.map +1 -0
  23. package/dist/path-encoder.js +14 -0
  24. package/dist/path-encoder.js.map +1 -0
  25. package/dist/project-mapping.d.ts +29 -0
  26. package/dist/project-mapping.d.ts.map +1 -0
  27. package/dist/project-mapping.js +106 -0
  28. package/dist/project-mapping.js.map +1 -0
  29. package/dist/project-registry.d.ts +12 -0
  30. package/dist/project-registry.d.ts.map +1 -0
  31. package/dist/project-registry.js +38 -0
  32. package/dist/project-registry.js.map +1 -0
  33. package/dist/session.d.ts +21 -0
  34. package/dist/session.d.ts.map +1 -0
  35. package/dist/session.js +36 -0
  36. package/dist/session.js.map +1 -0
  37. package/dist/skill-index.d.ts +52 -0
  38. package/dist/skill-index.d.ts.map +1 -0
  39. package/dist/skill-index.js +467 -0
  40. package/dist/skill-index.js.map +1 -0
  41. package/dist/sync.d.ts +23 -0
  42. package/dist/sync.d.ts.map +1 -0
  43. package/dist/sync.js +331 -0
  44. package/dist/sync.js.map +1 -0
  45. package/dist/telemetry.d.ts +12 -0
  46. package/dist/telemetry.d.ts.map +1 -0
  47. package/dist/telemetry.js +56 -0
  48. package/dist/telemetry.js.map +1 -0
  49. package/dist/traces.d.ts +20 -0
  50. package/dist/traces.d.ts.map +1 -0
  51. package/dist/traces.js +82 -0
  52. package/dist/traces.js.map +1 -0
  53. package/dist/types.d.ts +125 -0
  54. package/dist/types.d.ts.map +1 -0
  55. package/dist/types.js +5 -0
  56. package/dist/types.js.map +1 -0
  57. package/dist/version.d.ts +3 -0
  58. package/dist/version.d.ts.map +1 -0
  59. package/dist/version.js +3 -0
  60. package/dist/version.js.map +1 -0
  61. package/package.json +39 -0
@@ -0,0 +1,12 @@
1
+ import type { ProjectRegistry } from "./types.js";
2
+ export declare function loadRegistry(registryPath: string): Promise<ProjectRegistry>;
3
+ export declare function saveRegistry(registryPath: string, data: ProjectRegistry): Promise<void>;
4
+ /**
5
+ * Register a project cwd as known. Mutates in place.
6
+ */
7
+ export declare function registerProject(registry: ProjectRegistry, cwd: string): void;
8
+ /**
9
+ * Get list of known project paths, sorted by most recently seen.
10
+ */
11
+ export declare function getKnownProjects(registry: ProjectRegistry): string[];
12
+ //# sourceMappingURL=project-registry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"project-registry.d.ts","sourceRoot":"","sources":["../src/project-registry.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAElD,wBAAsB,YAAY,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC,CAUjF;AAED,wBAAsB,YAAY,CAAC,YAAY,EAAE,MAAM,EAAE,IAAI,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC,CAM7F;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,eAAe,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI,CAE5E;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,eAAe,GAAG,MAAM,EAAE,CAIpE"}
@@ -0,0 +1,38 @@
1
+ import { randomBytes } from "node:crypto";
2
+ import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
3
+ import { dirname } from "node:path";
4
+ export async function loadRegistry(registryPath) {
5
+ const empty = { version: 1, projects: {} };
6
+ try {
7
+ const raw = await readFile(registryPath, "utf-8");
8
+ const data = JSON.parse(raw);
9
+ if (data.version !== 1)
10
+ return empty;
11
+ return data;
12
+ }
13
+ catch {
14
+ return empty;
15
+ }
16
+ }
17
+ export async function saveRegistry(registryPath, data) {
18
+ const dir = dirname(registryPath);
19
+ await mkdir(dir, { recursive: true });
20
+ const tmpPath = `${registryPath}.${randomBytes(4).toString("hex")}.tmp`;
21
+ await writeFile(tmpPath, JSON.stringify(data, null, 2), "utf-8");
22
+ await rename(tmpPath, registryPath);
23
+ }
24
+ /**
25
+ * Register a project cwd as known. Mutates in place.
26
+ */
27
+ export function registerProject(registry, cwd) {
28
+ registry.projects[cwd] = { lastSeen: new Date().toISOString() };
29
+ }
30
+ /**
31
+ * Get list of known project paths, sorted by most recently seen.
32
+ */
33
+ export function getKnownProjects(registry) {
34
+ return Object.entries(registry.projects)
35
+ .sort(([, a], [, b]) => b.lastSeen.localeCompare(a.lastSeen))
36
+ .map(([path]) => path);
37
+ }
38
+ //# sourceMappingURL=project-registry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"project-registry.js","sourceRoot":"","sources":["../src/project-registry.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AACtE,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAGpC,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,YAAoB;IACrD,MAAM,KAAK,GAAoB,EAAE,OAAO,EAAE,CAAC,EAAE,QAAQ,EAAE,EAAE,EAAE,CAAC;IAC5D,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,MAAM,QAAQ,CAAC,YAAY,EAAE,OAAO,CAAC,CAAC;QAClD,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAoB,CAAC;QAChD,IAAI,IAAI,CAAC,OAAO,KAAK,CAAC;YAAE,OAAO,KAAK,CAAC;QACrC,OAAO,IAAI,CAAC;IACd,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,YAAoB,EAAE,IAAqB;IAC5E,MAAM,GAAG,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC;IAClC,MAAM,KAAK,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IACtC,MAAM,OAAO,GAAG,GAAG,YAAY,IAAI,WAAW,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,MAAM,CAAC;IACxE,MAAM,SAAS,CAAC,OAAO,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC;IACjE,MAAM,MAAM,CAAC,OAAO,EAAE,YAAY,CAAC,CAAC;AACtC,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,eAAe,CAAC,QAAyB,EAAE,GAAW;IACpE,QAAQ,CAAC,QAAQ,CAAC,GAAG,CAAC,GAAG,EAAE,QAAQ,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,EAAE,CAAC;AAClE,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,gBAAgB,CAAC,QAAyB;IACxD,OAAO,MAAM,CAAC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC;SACrC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,aAAa,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC;SAC5D,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,CAAC;AAC3B,CAAC"}
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Interface for tracking which rules have been shown per session.
3
+ * Implementations may use in-memory or file-based persistence.
4
+ */
5
+ export interface SessionTracker {
6
+ hasRuleBeenShown(sessionId: string, location: string): boolean;
7
+ markRuleShown(sessionId: string, location: string): void;
8
+ clearSession(sessionId: string): void;
9
+ cleanup(maxAgeMs?: number): void;
10
+ }
11
+ /**
12
+ * In-memory session tracker. State resets on process restart.
13
+ */
14
+ export declare class InMemorySessionTracker implements SessionTracker {
15
+ private shownRules;
16
+ hasRuleBeenShown(sessionId: string, location: string): boolean;
17
+ markRuleShown(sessionId: string, location: string): void;
18
+ clearSession(sessionId: string): void;
19
+ cleanup(maxAgeMs?: number): void;
20
+ }
21
+ //# sourceMappingURL=session.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../src/session.ts"],"names":[],"mappings":"AAIA;;;GAGG;AACH,MAAM,WAAW,cAAc;IAC7B,gBAAgB,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC;IAC/D,aAAa,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACzD,YAAY,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACtC,OAAO,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAClC;AAED;;GAEG;AACH,qBAAa,sBAAuB,YAAW,cAAc;IAC3D,OAAO,CAAC,UAAU,CAAsE;IAExF,gBAAgB,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO;IAM9D,aAAa,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI;IAUxD,YAAY,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI;IAIrC,OAAO,CAAC,QAAQ,GAAE,MAAiB,GAAG,IAAI;CAQ3C"}
@@ -0,0 +1,36 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Session tracking for graduated disclosure
3
+ // ---------------------------------------------------------------------------
4
+ /**
5
+ * In-memory session tracker. State resets on process restart.
6
+ */
7
+ export class InMemorySessionTracker {
8
+ shownRules = new Map();
9
+ hasRuleBeenShown(sessionId, location) {
10
+ const entry = this.shownRules.get(sessionId);
11
+ if (entry)
12
+ entry.lastAccess = Date.now();
13
+ return entry?.rules.has(location) ?? false;
14
+ }
15
+ markRuleShown(sessionId, location) {
16
+ let entry = this.shownRules.get(sessionId);
17
+ if (!entry) {
18
+ entry = { rules: new Set(), lastAccess: Date.now() };
19
+ this.shownRules.set(sessionId, entry);
20
+ }
21
+ entry.lastAccess = Date.now();
22
+ entry.rules.add(location);
23
+ }
24
+ clearSession(sessionId) {
25
+ this.shownRules.delete(sessionId);
26
+ }
27
+ cleanup(maxAgeMs = 3600_000) {
28
+ const cutoff = Date.now() - maxAgeMs;
29
+ for (const [key, entry] of this.shownRules) {
30
+ if (entry.lastAccess < cutoff) {
31
+ this.shownRules.delete(key);
32
+ }
33
+ }
34
+ }
35
+ }
36
+ //# sourceMappingURL=session.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session.js","sourceRoot":"","sources":["../src/session.ts"],"names":[],"mappings":"AAAA,8EAA8E;AAC9E,4CAA4C;AAC5C,8EAA8E;AAa9E;;GAEG;AACH,MAAM,OAAO,sBAAsB;IACzB,UAAU,GAA4D,IAAI,GAAG,EAAE,CAAC;IAExF,gBAAgB,CAAC,SAAiB,EAAE,QAAgB;QAClD,MAAM,KAAK,GAAG,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;QAC7C,IAAI,KAAK;YAAE,KAAK,CAAC,UAAU,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QACzC,OAAO,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,KAAK,CAAC;IAC7C,CAAC;IAED,aAAa,CAAC,SAAiB,EAAE,QAAgB;QAC/C,IAAI,KAAK,GAAG,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;QAC3C,IAAI,CAAC,KAAK,EAAE,CAAC;YACX,KAAK,GAAG,EAAE,KAAK,EAAE,IAAI,GAAG,EAAE,EAAE,UAAU,EAAE,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC;YACrD,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,SAAS,EAAE,KAAK,CAAC,CAAC;QACxC,CAAC;QACD,KAAK,CAAC,UAAU,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QAC9B,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;IAC5B,CAAC;IAED,YAAY,CAAC,SAAiB;QAC5B,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;IACpC,CAAC;IAED,OAAO,CAAC,WAAmB,QAAQ;QACjC,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,QAAQ,CAAC;QACrC,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,IAAI,CAAC,UAAU,EAAE,CAAC;YAC3C,IAAI,KAAK,CAAC,UAAU,GAAG,MAAM,EAAE,CAAC;gBAC9B,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;YAC9B,CAAC;QACH,CAAC;IACH,CAAC;CACF"}
@@ -0,0 +1,52 @@
1
+ import type { EmbeddingProvider } from "./embeddings.js";
2
+ import type { MemexCoreConfig, ParsedFrontmatter, ScoringMode, SkillSearchResult, SkillType } from "./types.js";
3
+ export declare function parseFrontmatter(content: string): {
4
+ meta: ParsedFrontmatter;
5
+ body: string;
6
+ };
7
+ /**
8
+ * Parse a memory markdown file into sections.
9
+ * Extracts ## sections and looks for `Triggers:` lines as queries.
10
+ */
11
+ export declare function parseMemoryFile(content: string, _filePath: string): Array<{
12
+ name: string;
13
+ description: string;
14
+ queries: string[];
15
+ body: string;
16
+ }>;
17
+ export type ScanDirs = {
18
+ skillDirs: string[];
19
+ memoryDirs: string[];
20
+ ruleDirs: string[];
21
+ };
22
+ export declare class SkillIndex {
23
+ private config;
24
+ private provider;
25
+ private cachePath;
26
+ private skills;
27
+ private skillMtimes;
28
+ private cache;
29
+ private cacheLoaded;
30
+ private buildTime;
31
+ constructor(config: MemexCoreConfig, provider: EmbeddingProvider, cachePath: string);
32
+ get skillCount(): number;
33
+ needsRebuild(): boolean;
34
+ /**
35
+ * Build the index by scanning all skill, rule, and memory directories.
36
+ * Uses cache for unchanged files (mtime-gated).
37
+ */
38
+ build(scanDirs: ScanDirs): Promise<void>;
39
+ /**
40
+ * Search the index for skills matching the query.
41
+ * Supports both relative and absolute scoring modes.
42
+ */
43
+ search(query: string, topK: number, threshold: number, typeFilter?: SkillType[], scoringMode?: ScoringMode, maxDropoff?: number): Promise<SkillSearchResult[]>;
44
+ /**
45
+ * Read the body content of a skill file, stripping frontmatter.
46
+ */
47
+ readSkillContent(location: string): Promise<string>;
48
+ private parseSkillFileForEmbed;
49
+ private parseMemoryFileForEmbed;
50
+ private parseRuleFileForEmbed;
51
+ }
52
+ //# sourceMappingURL=skill-index.d.ts.map
@@ -0,0 +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,CAqD3F;AAMD;;;GAGG;AACH,wBAAgB,eAAe,CAC7B,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,MAAM,GAChB,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,CAmC/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;AAiBF,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;IA0L9C;;;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;IA+B/B;;OAEG;IACG,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAkBzD,OAAO,CAAC,sBAAsB;IAqB9B,OAAO,CAAC,uBAAuB;IAqB/B,OAAO,CAAC,qBAAqB;CA2B9B"}
@@ -0,0 +1,467 @@
1
+ import { readdir, readFile, stat } from "node:fs/promises";
2
+ import { basename, join } from "node:path";
3
+ import { fromCachedSkill, loadCache, saveCache, toCachedSkill } from "./cache.js";
4
+ import { cosineSimilarity } from "./embeddings.js";
5
+ // ---------------------------------------------------------------------------
6
+ // Frontmatter parsing
7
+ // ---------------------------------------------------------------------------
8
+ const LIST_KEYS = new Set(["queries", "paths", "hooks", "keywords"]);
9
+ export function parseFrontmatter(content) {
10
+ const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
11
+ if (!match)
12
+ return { meta: {}, body: content };
13
+ const frontmatter = match[1];
14
+ const body = match[2];
15
+ const meta = {};
16
+ let currentListKey = "";
17
+ const listAccumulators = {};
18
+ for (const line of frontmatter.split(/\r?\n/)) {
19
+ // Continue accumulating list items
20
+ if (currentListKey) {
21
+ const listItem = line.match(/^\s+-\s+(.*)/);
22
+ if (listItem) {
23
+ listAccumulators[currentListKey].push(listItem[1].replace(/^["']|["']$/g, "").trim());
24
+ continue;
25
+ }
26
+ currentListKey = "";
27
+ }
28
+ const colonIdx = line.indexOf(":");
29
+ if (colonIdx === -1)
30
+ continue;
31
+ const key = line.slice(0, colonIdx).trim();
32
+ const rawValue = line.slice(colonIdx + 1).trim();
33
+ const value = rawValue.replace(/^["']|["']$/g, "");
34
+ // Scalar keys
35
+ if (key === "name")
36
+ meta.name = value;
37
+ if (key === "description")
38
+ meta.description = value;
39
+ if (key === "type")
40
+ meta.type = value;
41
+ if (key === "one-liner")
42
+ meta.oneLiner = value;
43
+ // List keys — block-style (empty value + indented items) or inline value
44
+ if (LIST_KEYS.has(key)) {
45
+ if (rawValue === "") {
46
+ currentListKey = key;
47
+ listAccumulators[key] = [];
48
+ }
49
+ else {
50
+ // Inline value: treat as a single-element list
51
+ listAccumulators[key] = listAccumulators[key] || [];
52
+ listAccumulators[key].push(value);
53
+ }
54
+ }
55
+ }
56
+ if (listAccumulators.queries?.length)
57
+ meta.queries = listAccumulators.queries;
58
+ if (listAccumulators.paths?.length)
59
+ meta.paths = listAccumulators.paths;
60
+ if (listAccumulators.hooks?.length)
61
+ meta.hooks = listAccumulators.hooks;
62
+ if (listAccumulators.keywords?.length)
63
+ meta.keywords = listAccumulators.keywords;
64
+ return { meta, body };
65
+ }
66
+ // ---------------------------------------------------------------------------
67
+ // Memory file parsing
68
+ // ---------------------------------------------------------------------------
69
+ /**
70
+ * Parse a memory markdown file into sections.
71
+ * Extracts ## sections and looks for `Triggers:` lines as queries.
72
+ */
73
+ export function parseMemoryFile(content, _filePath) {
74
+ const results = [];
75
+ const sections = content.split(/^(?=##\s)/m);
76
+ for (const section of sections) {
77
+ const headingMatch = section.match(/^##\s+(.+)/);
78
+ if (!headingMatch)
79
+ continue;
80
+ const name = headingMatch[1].trim();
81
+ const bodyLines = [];
82
+ const queries = [];
83
+ for (const line of section.split(/\r?\n/).slice(1)) {
84
+ const triggerMatch = line.match(/^Triggers?:\s*(.+)/i);
85
+ if (triggerMatch) {
86
+ const raw = triggerMatch[1];
87
+ const parsed = raw
88
+ .split(/,\s*/)
89
+ .map((t) => t.replace(/^["']|["']$/g, "").trim())
90
+ .filter((t) => t.length > 0);
91
+ queries.push(...parsed);
92
+ }
93
+ else {
94
+ bodyLines.push(line);
95
+ }
96
+ }
97
+ const body = bodyLines.join("\n").trim();
98
+ if (body.length > 0 || queries.length > 0) {
99
+ const description = body.split("\n")[0]?.trim() || name;
100
+ results.push({ name, description, queries, body });
101
+ }
102
+ }
103
+ return results;
104
+ }
105
+ // ---------------------------------------------------------------------------
106
+ // Directory scanning
107
+ // ---------------------------------------------------------------------------
108
+ async function scanSkillDirs(dirs) {
109
+ const skillFiles = [];
110
+ for (const dir of dirs) {
111
+ let entries;
112
+ try {
113
+ entries = await readdir(dir);
114
+ }
115
+ catch {
116
+ continue;
117
+ }
118
+ for (const entry of entries) {
119
+ const skillMd = join(dir, entry, "SKILL.md");
120
+ try {
121
+ await stat(skillMd);
122
+ skillFiles.push(skillMd);
123
+ }
124
+ catch {
125
+ // No SKILL.md in this subdirectory
126
+ }
127
+ }
128
+ }
129
+ return skillFiles;
130
+ }
131
+ async function scanMemoryDirs(dirs) {
132
+ const memoryFiles = [];
133
+ for (const dir of dirs) {
134
+ let entries;
135
+ try {
136
+ entries = await readdir(dir);
137
+ }
138
+ catch {
139
+ continue;
140
+ }
141
+ for (const entry of entries) {
142
+ if (!entry.endsWith(".md"))
143
+ continue;
144
+ if (entry === "MEMORY.md")
145
+ continue;
146
+ memoryFiles.push(join(dir, entry));
147
+ }
148
+ }
149
+ return memoryFiles;
150
+ }
151
+ async function scanRuleDirs(dirs) {
152
+ const ruleFiles = [];
153
+ for (const dir of dirs) {
154
+ let entries;
155
+ try {
156
+ entries = await readdir(dir);
157
+ }
158
+ catch {
159
+ continue;
160
+ }
161
+ for (const entry of entries) {
162
+ if (!entry.endsWith(".md"))
163
+ continue;
164
+ ruleFiles.push(join(dir, entry));
165
+ }
166
+ }
167
+ return ruleFiles;
168
+ }
169
+ export class SkillIndex {
170
+ config;
171
+ provider;
172
+ cachePath;
173
+ skills = [];
174
+ skillMtimes = new Map();
175
+ cache = null;
176
+ cacheLoaded = false;
177
+ buildTime = 0;
178
+ constructor(config, provider, cachePath) {
179
+ this.config = config;
180
+ this.provider = provider;
181
+ this.cachePath = cachePath;
182
+ }
183
+ get skillCount() {
184
+ return this.skills.length;
185
+ }
186
+ needsRebuild() {
187
+ if (this.buildTime === 0)
188
+ return true;
189
+ return Date.now() - this.buildTime >= this.config.cacheTimeMs;
190
+ }
191
+ /**
192
+ * Build the index by scanning all skill, rule, and memory directories.
193
+ * Uses cache for unchanged files (mtime-gated).
194
+ */
195
+ async build(scanDirs) {
196
+ // Load persistent cache on first build
197
+ if (!this.cacheLoaded) {
198
+ this.cache = await loadCache(this.cachePath, this.config.embeddingModel);
199
+ this.cacheLoaded = true;
200
+ // Hydrate from cache on cold start
201
+ if (this.skills.length === 0 && Object.keys(this.cache.skills).length > 0) {
202
+ for (const [location, cached] of Object.entries(this.cache.skills)) {
203
+ this.skills.push(fromCachedSkill(location, cached));
204
+ this.skillMtimes.set(location, cached.mtime);
205
+ }
206
+ }
207
+ }
208
+ // Scan all sources in parallel
209
+ const [skillFiles, memoryFiles, ruleFiles] = await Promise.all([
210
+ scanSkillDirs(scanDirs.skillDirs),
211
+ scanMemoryDirs(scanDirs.memoryDirs),
212
+ scanRuleDirs(scanDirs.ruleDirs),
213
+ ]);
214
+ const statPromises = [
215
+ ...skillFiles.map(async (f) => {
216
+ try {
217
+ const s = await stat(f);
218
+ return { location: f, mtime: s.mtimeMs, kind: "skill" };
219
+ }
220
+ catch {
221
+ return null;
222
+ }
223
+ }),
224
+ ...memoryFiles.map(async (f) => {
225
+ try {
226
+ const s = await stat(f);
227
+ return { location: f, mtime: s.mtimeMs, kind: "memory" };
228
+ }
229
+ catch {
230
+ return null;
231
+ }
232
+ }),
233
+ ...ruleFiles.map(async (f) => {
234
+ try {
235
+ const s = await stat(f);
236
+ return { location: f, mtime: s.mtimeMs, kind: "rule" };
237
+ }
238
+ catch {
239
+ return null;
240
+ }
241
+ }),
242
+ ];
243
+ const statResults = (await Promise.all(statPromises)).filter((r) => r !== null);
244
+ const currentLocations = new Set(statResults.map((s) => s.location));
245
+ // Check for changes (fast path: skip if nothing changed)
246
+ const anyNew = statResults.some((s) => !this.skillMtimes.has(s.location));
247
+ const anyChanged = statResults.some((s) => this.skillMtimes.get(s.location) !== s.mtime);
248
+ const anyDeleted = [...this.skillMtimes.keys()].some((loc) => !currentLocations.has(loc));
249
+ if (this.buildTime > 0 && !anyNew && !anyChanged && !anyDeleted) {
250
+ this.buildTime = Date.now();
251
+ return;
252
+ }
253
+ // Find files that need (re)embedding
254
+ const toEmbed = [];
255
+ for (const info of statResults) {
256
+ if (info.kind === "memory") {
257
+ // Memory files may produce multiple sections — each keyed as "path#SectionName"
258
+ const cachedMtime = this.skillMtimes.get(info.location);
259
+ if (cachedMtime === info.mtime)
260
+ continue;
261
+ // Remove old sections for this memory file
262
+ this.skills = this.skills.filter((s) => !s.location.startsWith(`${info.location}#`));
263
+ if (this.cache) {
264
+ for (const key of Object.keys(this.cache.skills)) {
265
+ if (key.startsWith(`${info.location}#`))
266
+ delete this.cache.skills[key];
267
+ }
268
+ }
269
+ try {
270
+ const raw = await readFile(info.location, "utf-8");
271
+ this.parseMemoryFileForEmbed(raw, info, toEmbed);
272
+ }
273
+ catch {
274
+ // Skip unreadable
275
+ }
276
+ this.skillMtimes.set(info.location, info.mtime);
277
+ continue;
278
+ }
279
+ // Skills and rules
280
+ const cached = this.cache?.skills[info.location];
281
+ if (cached && cached.mtime === info.mtime) {
282
+ // Use cached embeddings — no re-embed
283
+ const existing = this.skills.findIndex((s) => s.location === info.location);
284
+ const skill = fromCachedSkill(info.location, cached);
285
+ if (existing >= 0)
286
+ this.skills[existing] = skill;
287
+ else if (!this.skills.some((s) => s.location === info.location))
288
+ this.skills.push(skill);
289
+ this.skillMtimes.set(info.location, info.mtime);
290
+ continue;
291
+ }
292
+ // Check in-memory cache
293
+ const unchanged = this.skillMtimes.get(info.location) === info.mtime &&
294
+ this.skills.some((s) => s.location === info.location);
295
+ if (unchanged)
296
+ continue;
297
+ try {
298
+ const raw = await readFile(info.location, "utf-8");
299
+ if (info.kind === "rule") {
300
+ this.parseRuleFileForEmbed(raw, info, toEmbed);
301
+ }
302
+ else {
303
+ this.parseSkillFileForEmbed(raw, info, toEmbed);
304
+ }
305
+ }
306
+ catch {
307
+ // Skip unreadable files
308
+ }
309
+ }
310
+ // Embed new/changed entries in one batch
311
+ if (toEmbed.length > 0) {
312
+ const flatQueries = toEmbed.flatMap((p) => p.queries);
313
+ const flatEmbeddings = await this.provider.embed(flatQueries);
314
+ let offset = 0;
315
+ for (const item of toEmbed) {
316
+ const embeddings = flatEmbeddings.slice(offset, offset + item.queries.length);
317
+ const skill = {
318
+ name: item.name,
319
+ description: item.description,
320
+ location: item.location,
321
+ type: item.type,
322
+ embeddings,
323
+ queries: item.queries,
324
+ oneLiner: item.oneLiner,
325
+ };
326
+ const existing = this.skills.findIndex((s) => s.location === item.location);
327
+ if (existing >= 0)
328
+ this.skills[existing] = skill;
329
+ else
330
+ this.skills.push(skill);
331
+ // Update persistent cache
332
+ if (this.cache) {
333
+ this.cache.skills[item.location] = toCachedSkill(skill, item.mtime);
334
+ }
335
+ offset += item.queries.length;
336
+ }
337
+ }
338
+ // Remove deleted entries (handle memory section keys like "path#SectionName")
339
+ this.skills = this.skills.filter((s) => {
340
+ const baseLocation = s.location.includes("#") ? s.location.split("#")[0] : s.location;
341
+ return currentLocations.has(baseLocation) || currentLocations.has(s.location);
342
+ });
343
+ // Clean cache of deleted entries
344
+ if (this.cache) {
345
+ for (const key of Object.keys(this.cache.skills)) {
346
+ const baseKey = key.includes("#") ? key.split("#")[0] : key;
347
+ if (!currentLocations.has(baseKey) && !currentLocations.has(key)) {
348
+ delete this.cache.skills[key];
349
+ }
350
+ }
351
+ }
352
+ // Update mtime tracking
353
+ this.skillMtimes = new Map(statResults.map((s) => [s.location, s.mtime]));
354
+ // Persist cache (fire-and-forget)
355
+ if (this.cache && toEmbed.length > 0) {
356
+ saveCache(this.cachePath, this.cache).catch(() => {
357
+ // Cache save is best-effort
358
+ });
359
+ }
360
+ this.buildTime = Date.now();
361
+ }
362
+ /**
363
+ * Search the index for skills matching the query.
364
+ * Supports both relative and absolute scoring modes.
365
+ */
366
+ async search(query, topK, threshold, typeFilter, scoringMode = "absolute", maxDropoff = 0.15) {
367
+ let candidates = this.skills;
368
+ if (typeFilter && typeFilter.length > 0) {
369
+ const allowed = new Set(typeFilter);
370
+ candidates = candidates.filter((s) => allowed.has(s.type));
371
+ }
372
+ if (candidates.length === 0)
373
+ return [];
374
+ const [queryEmbedding] = await this.provider.embed([query]);
375
+ const scored = candidates.map((skill) => {
376
+ const similarities = skill.embeddings.map((e) => cosineSimilarity(queryEmbedding, e));
377
+ const score = Math.max(...similarities);
378
+ return { skill, score };
379
+ });
380
+ const sorted = scored.sort((a, b) => b.score - a.score);
381
+ if (scoringMode === "relative") {
382
+ // Relative mode: if the best match clears the floor, inject top-K
383
+ // but drop results that fall too far below the best
384
+ if (sorted.length === 0 || sorted[0].score < threshold)
385
+ return [];
386
+ const bestScore = sorted[0].score;
387
+ return sorted.filter((r) => bestScore - r.score <= maxDropoff).slice(0, topK);
388
+ }
389
+ // Absolute mode: each result must individually pass threshold
390
+ return sorted.filter((r) => r.score >= threshold).slice(0, topK);
391
+ }
392
+ /**
393
+ * Read the body content of a skill file, stripping frontmatter.
394
+ */
395
+ async readSkillContent(location) {
396
+ if (location.includes("#")) {
397
+ const [filePath, sectionName] = location.split("#", 2);
398
+ const raw = await readFile(filePath, "utf-8");
399
+ const sections = parseMemoryFile(raw, filePath);
400
+ const section = sections.find((s) => s.name === sectionName);
401
+ return section?.body.trim() || "";
402
+ }
403
+ const raw = await readFile(location, "utf-8");
404
+ const { body } = parseFrontmatter(raw);
405
+ return body.trim();
406
+ }
407
+ // -------------------------------------------------------------------------
408
+ // Private parsing helpers
409
+ // -------------------------------------------------------------------------
410
+ parseSkillFileForEmbed(raw, info, toEmbed) {
411
+ const { meta, body } = parseFrontmatter(raw);
412
+ if (!meta.name || !meta.description)
413
+ return;
414
+ const queries = meta.queries?.length ? meta.queries : [meta.description];
415
+ const type = meta.type || "skill";
416
+ toEmbed.push({
417
+ name: meta.name,
418
+ description: meta.description,
419
+ location: info.location,
420
+ queries,
421
+ type,
422
+ mtime: info.mtime,
423
+ body,
424
+ oneLiner: meta.oneLiner,
425
+ });
426
+ }
427
+ parseMemoryFileForEmbed(raw, info, toEmbed) {
428
+ const sections = parseMemoryFile(raw, info.location);
429
+ for (const section of sections) {
430
+ const key = `${info.location}#${section.name}`;
431
+ const queries = section.queries.length > 0 ? section.queries : [section.description];
432
+ toEmbed.push({
433
+ name: section.name,
434
+ description: section.description,
435
+ location: key,
436
+ queries,
437
+ type: "memory",
438
+ mtime: info.mtime,
439
+ body: section.body,
440
+ });
441
+ }
442
+ }
443
+ parseRuleFileForEmbed(raw, info, toEmbed) {
444
+ const { meta, body } = parseFrontmatter(raw);
445
+ const name = meta.name || basename(info.location, ".md");
446
+ const description = meta.description || body.split("\n")[0]?.trim() || name;
447
+ const oneLiner = meta.oneLiner || description;
448
+ const queries = [];
449
+ if (meta.queries?.length)
450
+ queries.push(...meta.queries);
451
+ if (meta.keywords?.length)
452
+ queries.push(...meta.keywords);
453
+ if (queries.length === 0)
454
+ queries.push(description);
455
+ toEmbed.push({
456
+ name,
457
+ description,
458
+ location: info.location,
459
+ queries,
460
+ type: "rule",
461
+ mtime: info.mtime,
462
+ body,
463
+ oneLiner,
464
+ });
465
+ }
466
+ }
467
+ //# sourceMappingURL=skill-index.js.map