context-vault 2.8.0 → 2.8.3

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 (33) hide show
  1. package/README.md +39 -375
  2. package/bin/cli.js +26 -67
  3. package/node_modules/@context-vault/core/package.json +7 -3
  4. package/node_modules/@context-vault/core/src/capture/file-ops.js +20 -2
  5. package/node_modules/@context-vault/core/src/capture/import-pipeline.js +0 -34
  6. package/node_modules/@context-vault/core/src/capture/importers.js +64 -37
  7. package/node_modules/@context-vault/core/src/capture/ingest-url.js +80 -44
  8. package/node_modules/@context-vault/core/src/constants.js +8 -0
  9. package/node_modules/@context-vault/core/src/core/config.js +65 -29
  10. package/node_modules/@context-vault/core/src/core/files.js +8 -15
  11. package/node_modules/@context-vault/core/src/core/frontmatter.js +22 -10
  12. package/node_modules/@context-vault/core/src/core/status.js +32 -15
  13. package/node_modules/@context-vault/core/src/index/db.js +47 -34
  14. package/node_modules/@context-vault/core/src/index/embed.js +15 -5
  15. package/node_modules/@context-vault/core/src/index.js +39 -6
  16. package/node_modules/@context-vault/core/src/retrieve/index.js +40 -8
  17. package/node_modules/@context-vault/core/src/server/helpers.js +8 -6
  18. package/node_modules/@context-vault/core/src/server/tools/context-status.js +24 -10
  19. package/node_modules/@context-vault/core/src/server/tools/delete-context.js +8 -3
  20. package/node_modules/@context-vault/core/src/server/tools/get-context.js +117 -35
  21. package/node_modules/@context-vault/core/src/server/tools/ingest-url.js +5 -4
  22. package/node_modules/@context-vault/core/src/server/tools/list-context.js +59 -18
  23. package/node_modules/@context-vault/core/src/server/tools/save-context.js +10 -10
  24. package/node_modules/@context-vault/core/src/server/tools.js +24 -28
  25. package/package.json +2 -2
  26. package/scripts/local-server.js +30 -30
  27. package/scripts/postinstall.js +25 -10
  28. package/scripts/prepack.js +18 -15
  29. package/src/server/index.js +78 -29
  30. package/app-dist/assets/index-DjXoWapE.css +0 -1
  31. package/app-dist/assets/index-R4n9Qz4U.js +0 -380
  32. package/app-dist/index.html +0 -16
  33. package/node_modules/@context-vault/core/src/server/types.js +0 -78
@@ -1,21 +1,3 @@
1
- /**
2
- * ingest-url.js — URL fetch + HTML → markdown conversion
3
- *
4
- * Fetches a URL, extracts readable content, converts to markdown,
5
- * and returns an EntryData object ready for captureAndIndex().
6
- *
7
- * Uses Node built-in fetch() (Node 18+). No external dependencies.
8
- */
9
-
10
- // ─── HTML → Markdown ─────────────────────────────────────────────────────────
11
-
12
- /**
13
- * Convert HTML to simplified markdown.
14
- * Strips scripts/styles, converts headings/links/lists/code.
15
- *
16
- * @param {string} html
17
- * @returns {string}
18
- */
19
1
  export function htmlToMarkdown(html) {
20
2
  let md = html;
21
3
 
@@ -28,38 +10,88 @@ export function htmlToMarkdown(html) {
28
10
  md = md.replace(/<aside[\s\S]*?<\/aside>/gi, "");
29
11
 
30
12
  // Convert headings
31
- md = md.replace(/<h1[^>]*>([\s\S]*?)<\/h1>/gi, (_, c) => `\n# ${stripTags(c).trim()}\n`);
32
- md = md.replace(/<h2[^>]*>([\s\S]*?)<\/h2>/gi, (_, c) => `\n## ${stripTags(c).trim()}\n`);
33
- md = md.replace(/<h3[^>]*>([\s\S]*?)<\/h3>/gi, (_, c) => `\n### ${stripTags(c).trim()}\n`);
34
- md = md.replace(/<h4[^>]*>([\s\S]*?)<\/h4>/gi, (_, c) => `\n#### ${stripTags(c).trim()}\n`);
35
- md = md.replace(/<h5[^>]*>([\s\S]*?)<\/h5>/gi, (_, c) => `\n##### ${stripTags(c).trim()}\n`);
36
- md = md.replace(/<h6[^>]*>([\s\S]*?)<\/h6>/gi, (_, c) => `\n###### ${stripTags(c).trim()}\n`);
13
+ md = md.replace(
14
+ /<h1[^>]*>([\s\S]*?)<\/h1>/gi,
15
+ (_, c) => `\n# ${stripTags(c).trim()}\n`,
16
+ );
17
+ md = md.replace(
18
+ /<h2[^>]*>([\s\S]*?)<\/h2>/gi,
19
+ (_, c) => `\n## ${stripTags(c).trim()}\n`,
20
+ );
21
+ md = md.replace(
22
+ /<h3[^>]*>([\s\S]*?)<\/h3>/gi,
23
+ (_, c) => `\n### ${stripTags(c).trim()}\n`,
24
+ );
25
+ md = md.replace(
26
+ /<h4[^>]*>([\s\S]*?)<\/h4>/gi,
27
+ (_, c) => `\n#### ${stripTags(c).trim()}\n`,
28
+ );
29
+ md = md.replace(
30
+ /<h5[^>]*>([\s\S]*?)<\/h5>/gi,
31
+ (_, c) => `\n##### ${stripTags(c).trim()}\n`,
32
+ );
33
+ md = md.replace(
34
+ /<h6[^>]*>([\s\S]*?)<\/h6>/gi,
35
+ (_, c) => `\n###### ${stripTags(c).trim()}\n`,
36
+ );
37
37
 
38
38
  // Convert links
39
- md = md.replace(/<a[^>]*href="([^"]*)"[^>]*>([\s\S]*?)<\/a>/gi, (_, href, text) => {
40
- const cleanText = stripTags(text).trim();
41
- return cleanText ? `[${cleanText}](${href})` : "";
42
- });
39
+ md = md.replace(
40
+ /<a[^>]*href="([^"]*)"[^>]*>([\s\S]*?)<\/a>/gi,
41
+ (_, href, text) => {
42
+ const cleanText = stripTags(text).trim();
43
+ return cleanText ? `[${cleanText}](${href})` : "";
44
+ },
45
+ );
43
46
 
44
47
  // Convert code blocks
45
- md = md.replace(/<pre[^>]*><code[^>]*>([\s\S]*?)<\/code><\/pre>/gi, (_, c) => `\n\`\`\`\n${decodeEntities(c).trim()}\n\`\`\`\n`);
46
- md = md.replace(/<pre[^>]*>([\s\S]*?)<\/pre>/gi, (_, c) => `\n\`\`\`\n${decodeEntities(stripTags(c)).trim()}\n\`\`\`\n`);
48
+ md = md.replace(
49
+ /<pre[^>]*><code[^>]*>([\s\S]*?)<\/code><\/pre>/gi,
50
+ (_, c) => `\n\`\`\`\n${decodeEntities(c).trim()}\n\`\`\`\n`,
51
+ );
52
+ md = md.replace(
53
+ /<pre[^>]*>([\s\S]*?)<\/pre>/gi,
54
+ (_, c) => `\n\`\`\`\n${decodeEntities(stripTags(c)).trim()}\n\`\`\`\n`,
55
+ );
47
56
 
48
57
  // Convert inline code
49
- md = md.replace(/<code[^>]*>([\s\S]*?)<\/code>/gi, (_, c) => `\`${decodeEntities(c).trim()}\``);
58
+ md = md.replace(
59
+ /<code[^>]*>([\s\S]*?)<\/code>/gi,
60
+ (_, c) => `\`${decodeEntities(c).trim()}\``,
61
+ );
50
62
 
51
63
  // Convert strong/em
52
- md = md.replace(/<(strong|b)[^>]*>([\s\S]*?)<\/\1>/gi, (_, __, c) => `**${stripTags(c).trim()}**`);
53
- md = md.replace(/<(em|i)[^>]*>([\s\S]*?)<\/\1>/gi, (_, __, c) => `*${stripTags(c).trim()}*`);
64
+ md = md.replace(
65
+ /<(strong|b)[^>]*>([\s\S]*?)<\/\1>/gi,
66
+ (_, __, c) => `**${stripTags(c).trim()}**`,
67
+ );
68
+ md = md.replace(
69
+ /<(em|i)[^>]*>([\s\S]*?)<\/\1>/gi,
70
+ (_, __, c) => `*${stripTags(c).trim()}*`,
71
+ );
54
72
 
55
73
  // Convert list items
56
- md = md.replace(/<li[^>]*>([\s\S]*?)<\/li>/gi, (_, c) => `- ${stripTags(c).trim()}\n`);
74
+ md = md.replace(
75
+ /<li[^>]*>([\s\S]*?)<\/li>/gi,
76
+ (_, c) => `- ${stripTags(c).trim()}\n`,
77
+ );
57
78
 
58
79
  // Convert paragraphs and line breaks
59
80
  md = md.replace(/<br\s*\/?>/gi, "\n");
60
- md = md.replace(/<p[^>]*>([\s\S]*?)<\/p>/gi, (_, c) => `\n${stripTags(c).trim()}\n`);
81
+ md = md.replace(
82
+ /<p[^>]*>([\s\S]*?)<\/p>/gi,
83
+ (_, c) => `\n${stripTags(c).trim()}\n`,
84
+ );
61
85
  md = md.replace(/<blockquote[^>]*>([\s\S]*?)<\/blockquote>/gi, (_, c) => {
62
- return "\n" + stripTags(c).trim().split("\n").map((l) => `> ${l}`).join("\n") + "\n";
86
+ return (
87
+ "\n" +
88
+ stripTags(c)
89
+ .trim()
90
+ .split("\n")
91
+ .map((l) => `> ${l}`)
92
+ .join("\n") +
93
+ "\n"
94
+ );
63
95
  });
64
96
 
65
97
  // Remove remaining HTML tags
@@ -87,11 +119,11 @@ function decodeEntities(text) {
87
119
  .replace(/&#39;/g, "'")
88
120
  .replace(/&nbsp;/g, " ")
89
121
  .replace(/&#(\d+);/g, (_, n) => String.fromCharCode(parseInt(n, 10)))
90
- .replace(/&#x([0-9a-f]+);/gi, (_, n) => String.fromCharCode(parseInt(n, 16)));
122
+ .replace(/&#x([0-9a-f]+);/gi, (_, n) =>
123
+ String.fromCharCode(parseInt(n, 16)),
124
+ );
91
125
  }
92
126
 
93
- // ─── HTML Content Extraction ─────────────────────────────────────────────────
94
-
95
127
  /**
96
128
  * Extract the main readable content from an HTML page.
97
129
  * Prefers <article> or <main>, falls back to <body>.
@@ -103,7 +135,9 @@ function decodeEntities(text) {
103
135
  export function extractHtmlContent(html, url) {
104
136
  // Extract <title>
105
137
  const titleMatch = html.match(/<title[^>]*>([\s\S]*?)<\/title>/i);
106
- const title = titleMatch ? stripTags(decodeEntities(titleMatch[1])).trim() : "";
138
+ const title = titleMatch
139
+ ? stripTags(decodeEntities(titleMatch[1])).trim()
140
+ : "";
107
141
 
108
142
  // Try to extract main content area
109
143
  let contentHtml = "";
@@ -126,8 +160,6 @@ export function extractHtmlContent(html, url) {
126
160
  return { title, body };
127
161
  }
128
162
 
129
- // ─── URL Ingestion ───────────────────────────────────────────────────────────
130
-
131
163
  /**
132
164
  * Fetch a URL, extract readable content, and return an EntryData object.
133
165
  *
@@ -159,7 +191,8 @@ export async function ingestUrl(url, opts = {}) {
159
191
  response = await fetch(url, {
160
192
  signal: controller.signal,
161
193
  headers: {
162
- "User-Agent": "ContextVault/1.0 (+https://github.com/fellanH/context-vault)",
194
+ "User-Agent":
195
+ "ContextVault/1.0 (+https://github.com/fellanH/context-vault)",
163
196
  Accept: "text/html,application/xhtml+xml,text/plain,*/*",
164
197
  },
165
198
  });
@@ -181,7 +214,10 @@ export async function ingestUrl(url, opts = {}) {
181
214
 
182
215
  let title, body;
183
216
 
184
- if (contentType.includes("text/html") || contentType.includes("application/xhtml")) {
217
+ if (
218
+ contentType.includes("text/html") ||
219
+ contentType.includes("application/xhtml")
220
+ ) {
185
221
  const extracted = extractHtmlContent(html, url);
186
222
  title = extracted.title;
187
223
  body = extracted.body;
@@ -0,0 +1,8 @@
1
+ export const MAX_BODY_LENGTH = 100 * 1024; // 100KB
2
+ export const MAX_TITLE_LENGTH = 500;
3
+ export const MAX_KIND_LENGTH = 64;
4
+ export const MAX_TAG_LENGTH = 100;
5
+ export const MAX_TAGS_COUNT = 20;
6
+ export const MAX_META_LENGTH = 10 * 1024; // 10KB
7
+ export const MAX_SOURCE_LENGTH = 200;
8
+ export const MAX_IDENTITY_KEY_LENGTH = 200;
@@ -1,13 +1,3 @@
1
- /**
2
- * config.js — CLI argument parsing and configuration resolution
3
- *
4
- * Resolution chain (highest priority last):
5
- * 1. Convention defaults
6
- * 2. Config file (~/.context-mcp/config.json)
7
- * 3. Environment variables (CONTEXT_VAULT_* or CONTEXT_MCP_*)
8
- * 4. CLI arguments
9
- */
10
-
11
1
  import { existsSync, readFileSync } from "node:fs";
12
2
  import { join, resolve } from "node:path";
13
3
  import { homedir } from "node:os";
@@ -19,7 +9,8 @@ export function parseArgs(argv) {
19
9
  else if (argv[i] === "--data-dir" && argv[i + 1]) args.dataDir = argv[++i];
20
10
  else if (argv[i] === "--db-path" && argv[i + 1]) args.dbPath = argv[++i];
21
11
  else if (argv[i] === "--dev-dir" && argv[i + 1]) args.devDir = argv[++i];
22
- else if (argv[i] === "--event-decay-days" && argv[i + 1]) args.eventDecayDays = Number(argv[++i]);
12
+ else if (argv[i] === "--event-decay-days" && argv[i + 1])
13
+ args.eventDecayDays = Number(argv[++i]);
23
14
  }
24
15
  return args;
25
16
  }
@@ -28,8 +19,12 @@ export function resolveConfig() {
28
19
  const HOME = homedir();
29
20
  const cliArgs = parseArgs(process.argv);
30
21
 
31
- // 1. Convention defaults
32
- const dataDir = resolve(cliArgs.dataDir || process.env.CONTEXT_VAULT_DATA_DIR || process.env.CONTEXT_MCP_DATA_DIR || join(HOME, ".context-mcp"));
22
+ const dataDir = resolve(
23
+ cliArgs.dataDir ||
24
+ process.env.CONTEXT_VAULT_DATA_DIR ||
25
+ process.env.CONTEXT_MCP_DATA_DIR ||
26
+ join(HOME, ".context-mcp"),
27
+ );
33
28
  const config = {
34
29
  vaultDir: join(HOME, "vault"),
35
30
  dataDir,
@@ -39,13 +34,15 @@ export function resolveConfig() {
39
34
  resolvedFrom: "defaults",
40
35
  };
41
36
 
42
- // 2. Config file
43
37
  const configPath = join(dataDir, "config.json");
44
38
  if (existsSync(configPath)) {
45
39
  try {
46
40
  const fc = JSON.parse(readFileSync(configPath, "utf-8"));
47
41
  if (fc.vaultDir) config.vaultDir = fc.vaultDir;
48
- if (fc.dataDir) { config.dataDir = fc.dataDir; config.dbPath = join(resolve(fc.dataDir), "vault.db"); }
42
+ if (fc.dataDir) {
43
+ config.dataDir = fc.dataDir;
44
+ config.dbPath = join(resolve(fc.dataDir), "vault.db");
45
+ }
49
46
  if (fc.dbPath) config.dbPath = fc.dbPath;
50
47
  if (fc.devDir) config.devDir = fc.devDir;
51
48
  if (fc.eventDecayDays != null) config.eventDecayDays = fc.eventDecayDays;
@@ -57,26 +54,65 @@ export function resolveConfig() {
57
54
  if (fc.linkedAt) config.linkedAt = fc.linkedAt;
58
55
  config.resolvedFrom = "config file";
59
56
  } catch (e) {
60
- throw new Error(`[context-vault] Invalid config at ${configPath}: ${e.message}`);
57
+ throw new Error(
58
+ `[context-vault] Invalid config at ${configPath}: ${e.message}`,
59
+ );
61
60
  }
62
61
  }
63
62
  config.configPath = configPath;
64
63
 
65
- // 3. Environment variable overrides (CONTEXT_VAULT_* takes priority over CONTEXT_MCP_*)
66
- if (process.env.CONTEXT_VAULT_VAULT_DIR || process.env.CONTEXT_MCP_VAULT_DIR) { config.vaultDir = process.env.CONTEXT_VAULT_VAULT_DIR || process.env.CONTEXT_MCP_VAULT_DIR; config.resolvedFrom = "env"; }
67
- if (process.env.CONTEXT_VAULT_DB_PATH || process.env.CONTEXT_MCP_DB_PATH) { config.dbPath = process.env.CONTEXT_VAULT_DB_PATH || process.env.CONTEXT_MCP_DB_PATH; config.resolvedFrom = "env"; }
68
- if (process.env.CONTEXT_VAULT_DEV_DIR || process.env.CONTEXT_MCP_DEV_DIR) { config.devDir = process.env.CONTEXT_VAULT_DEV_DIR || process.env.CONTEXT_MCP_DEV_DIR; config.resolvedFrom = "env"; }
69
- if (process.env.CONTEXT_VAULT_EVENT_DECAY_DAYS != null || process.env.CONTEXT_MCP_EVENT_DECAY_DAYS != null) { config.eventDecayDays = Number(process.env.CONTEXT_VAULT_EVENT_DECAY_DAYS ?? process.env.CONTEXT_MCP_EVENT_DECAY_DAYS); config.resolvedFrom = "env"; }
64
+ if (
65
+ process.env.CONTEXT_VAULT_VAULT_DIR ||
66
+ process.env.CONTEXT_MCP_VAULT_DIR
67
+ ) {
68
+ config.vaultDir =
69
+ process.env.CONTEXT_VAULT_VAULT_DIR || process.env.CONTEXT_MCP_VAULT_DIR;
70
+ config.resolvedFrom = "env";
71
+ }
72
+ if (process.env.CONTEXT_VAULT_DB_PATH || process.env.CONTEXT_MCP_DB_PATH) {
73
+ config.dbPath =
74
+ process.env.CONTEXT_VAULT_DB_PATH || process.env.CONTEXT_MCP_DB_PATH;
75
+ config.resolvedFrom = "env";
76
+ }
77
+ if (process.env.CONTEXT_VAULT_DEV_DIR || process.env.CONTEXT_MCP_DEV_DIR) {
78
+ config.devDir =
79
+ process.env.CONTEXT_VAULT_DEV_DIR || process.env.CONTEXT_MCP_DEV_DIR;
80
+ config.resolvedFrom = "env";
81
+ }
82
+ if (
83
+ process.env.CONTEXT_VAULT_EVENT_DECAY_DAYS != null ||
84
+ process.env.CONTEXT_MCP_EVENT_DECAY_DAYS != null
85
+ ) {
86
+ config.eventDecayDays = Number(
87
+ process.env.CONTEXT_VAULT_EVENT_DECAY_DAYS ??
88
+ process.env.CONTEXT_MCP_EVENT_DECAY_DAYS,
89
+ );
90
+ config.resolvedFrom = "env";
91
+ }
70
92
 
71
- // 3b. Hosted account env overrides
72
- if (process.env.CONTEXT_VAULT_API_KEY) { config.apiKey = process.env.CONTEXT_VAULT_API_KEY; }
73
- if (process.env.CONTEXT_VAULT_HOSTED_URL) { config.hostedUrl = process.env.CONTEXT_VAULT_HOSTED_URL; }
93
+ if (process.env.CONTEXT_VAULT_API_KEY) {
94
+ config.apiKey = process.env.CONTEXT_VAULT_API_KEY;
95
+ }
96
+ if (process.env.CONTEXT_VAULT_HOSTED_URL) {
97
+ config.hostedUrl = process.env.CONTEXT_VAULT_HOSTED_URL;
98
+ }
74
99
 
75
- // 4. CLI arg overrides (highest priority)
76
- if (cliArgs.vaultDir) { config.vaultDir = cliArgs.vaultDir; config.resolvedFrom = "CLI args"; }
77
- if (cliArgs.dbPath) { config.dbPath = cliArgs.dbPath; config.resolvedFrom = "CLI args"; }
78
- if (cliArgs.devDir) { config.devDir = cliArgs.devDir; config.resolvedFrom = "CLI args"; }
79
- if (cliArgs.eventDecayDays != null) { config.eventDecayDays = cliArgs.eventDecayDays; config.resolvedFrom = "CLI args"; }
100
+ if (cliArgs.vaultDir) {
101
+ config.vaultDir = cliArgs.vaultDir;
102
+ config.resolvedFrom = "CLI args";
103
+ }
104
+ if (cliArgs.dbPath) {
105
+ config.dbPath = cliArgs.dbPath;
106
+ config.resolvedFrom = "CLI args";
107
+ }
108
+ if (cliArgs.devDir) {
109
+ config.devDir = cliArgs.devDir;
110
+ config.resolvedFrom = "CLI args";
111
+ }
112
+ if (cliArgs.eventDecayDays != null) {
113
+ config.eventDecayDays = cliArgs.eventDecayDays;
114
+ config.resolvedFrom = "CLI args";
115
+ }
80
116
 
81
117
  // Resolve all paths to absolute
82
118
  config.vaultDir = resolve(config.vaultDir);
@@ -8,8 +8,6 @@ import { readdirSync } from "node:fs";
8
8
  import { join, resolve, sep } from "node:path";
9
9
  import { categoryDirFor } from "./categories.js";
10
10
 
11
- // ─── ULID Generator (Crockford Base32) ────────────────────────────────────────
12
-
13
11
  const CROCKFORD = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
14
12
 
15
13
  export function ulid() {
@@ -27,21 +25,18 @@ export function ulid() {
27
25
  return ts + rand;
28
26
  }
29
27
 
30
- // ─── Slugify ──────────────────────────────────────────────────────────────────
31
-
32
28
  export function slugify(text, maxLen = 60) {
33
29
  let slug = text
34
30
  .toLowerCase()
35
31
  .replace(/[^a-z0-9]+/g, "-")
36
32
  .replace(/^-+|-+$/g, "");
37
33
  if (slug.length > maxLen) {
38
- slug = slug.slice(0, maxLen).replace(/-[^-]*$/, "") || slug.slice(0, maxLen);
34
+ slug =
35
+ slug.slice(0, maxLen).replace(/-[^-]*$/, "") || slug.slice(0, maxLen);
39
36
  }
40
37
  return slug;
41
38
  }
42
39
 
43
- // ─── Kind ↔ Directory Mapping ────────────────────────────────────────────────
44
-
45
40
  const PLURAL_MAP = {
46
41
  insight: "insights",
47
42
  decision: "decisions",
@@ -60,7 +55,7 @@ const PLURAL_MAP = {
60
55
  };
61
56
 
62
57
  const SINGULAR_MAP = Object.fromEntries(
63
- Object.entries(PLURAL_MAP).map(([k, v]) => [v, k])
58
+ Object.entries(PLURAL_MAP).map(([k, v]) => [v, k]),
64
59
  );
65
60
 
66
61
  export function kindToDir(kind) {
@@ -75,9 +70,9 @@ export function dirToKind(dirName) {
75
70
 
76
71
  /** Normalize a kind input (singular or plural) to its canonical singular form. */
77
72
  export function normalizeKind(input) {
78
- if (PLURAL_MAP[input]) return input; // Already a known singular kind
73
+ if (PLURAL_MAP[input]) return input; // Already a known singular kind
79
74
  if (SINGULAR_MAP[input]) return SINGULAR_MAP[input]; // Known plural → singular
80
- return input; // Unknown — use as-is (don't strip 's')
75
+ return input; // Unknown — use as-is (don't strip 's')
81
76
  }
82
77
 
83
78
  /** Returns relative path from vault root → kind dir: "knowledge/insights", "events/sessions", etc. */
@@ -85,19 +80,17 @@ export function kindToPath(kind) {
85
80
  return `${categoryDirFor(kind)}/${kindToDir(kind)}`;
86
81
  }
87
82
 
88
- // ─── Safe Path Join ─────────────────────────────────────────────────────────
89
-
90
83
  export function safeJoin(base, ...parts) {
91
84
  const resolvedBase = resolve(base);
92
85
  const result = resolve(join(base, ...parts));
93
86
  if (!result.startsWith(resolvedBase + sep) && result !== resolvedBase) {
94
- throw new Error(`Path traversal blocked: resolved path escapes base directory`);
87
+ throw new Error(
88
+ `Path traversal blocked: resolved path escapes base directory`,
89
+ );
95
90
  }
96
91
  return result;
97
92
  }
98
93
 
99
- // ─── Recursive Directory Walk ────────────────────────────────────────────────
100
-
101
94
  export function walkDir(dir) {
102
95
  const results = [];
103
96
  function walk(currentDir, relDir) {
@@ -2,8 +2,6 @@
2
2
  * frontmatter.js — YAML frontmatter parsing and formatting
3
3
  */
4
4
 
5
- // ─── YAML Frontmatter Helpers ────────────────────────────────────────────────
6
-
7
5
  const NEEDS_QUOTING = /[:#'"{}[\],>|&*?!@`]/;
8
6
 
9
7
  export function formatFrontmatter(meta) {
@@ -14,7 +12,9 @@ export function formatFrontmatter(meta) {
14
12
  lines.push(`${k}: [${v.map((i) => JSON.stringify(i)).join(", ")}]`);
15
13
  } else {
16
14
  const str = String(v);
17
- lines.push(`${k}: ${NEEDS_QUOTING.test(str) ? JSON.stringify(str) : str}`);
15
+ lines.push(
16
+ `${k}: ${NEEDS_QUOTING.test(str) ? JSON.stringify(str) : str}`,
17
+ );
18
18
  }
19
19
  }
20
20
  lines.push("---");
@@ -32,8 +32,17 @@ export function parseFrontmatter(text) {
32
32
  const key = line.slice(0, idx).trim();
33
33
  let val = line.slice(idx + 1).trim();
34
34
  // Unquote JSON-quoted strings from formatFrontmatter
35
- if (val.length >= 2 && val.startsWith('"') && val.endsWith('"') && !val.startsWith('["')) {
36
- try { val = JSON.parse(val); } catch { /* keep as-is */ }
35
+ if (
36
+ val.length >= 2 &&
37
+ val.startsWith('"') &&
38
+ val.endsWith('"') &&
39
+ !val.startsWith('["')
40
+ ) {
41
+ try {
42
+ val = JSON.parse(val);
43
+ } catch {
44
+ /* keep as-is */
45
+ }
37
46
  }
38
47
  // Parse arrays: [a, b, c]
39
48
  if (val.startsWith("[") && val.endsWith("]")) {
@@ -51,9 +60,14 @@ export function parseFrontmatter(text) {
51
60
  return { meta, body: match[2].trim() };
52
61
  }
53
62
 
54
- // ─── Extract Custom Meta ────────────────────────────────────────────────────
55
-
56
- const RESERVED_FM_KEYS = new Set(["id", "tags", "source", "created", "identity_key", "expires_at"]);
63
+ const RESERVED_FM_KEYS = new Set([
64
+ "id",
65
+ "tags",
66
+ "source",
67
+ "created",
68
+ "identity_key",
69
+ "expires_at",
70
+ ]);
57
71
 
58
72
  export function extractCustomMeta(fmMeta) {
59
73
  const custom = {};
@@ -63,8 +77,6 @@ export function extractCustomMeta(fmMeta) {
63
77
  return Object.keys(custom).length ? custom : null;
64
78
  }
65
79
 
66
- // ─── Parse Entry From Markdown ──────────────────────────────────────────────
67
-
68
80
  export function parseEntryFromMarkdown(kind, body, fmMeta) {
69
81
  if (kind === "insight") {
70
82
  return {
@@ -46,7 +46,11 @@ export function gatherVaultStatus(ctx, opts = {}) {
46
46
  // Count DB rows by kind
47
47
  let kindCounts = [];
48
48
  try {
49
- kindCounts = db.prepare(`SELECT kind, COUNT(*) as c FROM vault ${userWhere} GROUP BY kind`).all(...userParams);
49
+ kindCounts = db
50
+ .prepare(
51
+ `SELECT kind, COUNT(*) as c FROM vault ${userWhere} GROUP BY kind`,
52
+ )
53
+ .all(...userParams);
50
54
  } catch (e) {
51
55
  errors.push(`Kind count query failed: ${e.message}`);
52
56
  }
@@ -54,7 +58,11 @@ export function gatherVaultStatus(ctx, opts = {}) {
54
58
  // Count DB rows by category
55
59
  let categoryCounts = [];
56
60
  try {
57
- categoryCounts = db.prepare(`SELECT category, COUNT(*) as c FROM vault ${userWhere} GROUP BY category`).all(...userParams);
61
+ categoryCounts = db
62
+ .prepare(
63
+ `SELECT category, COUNT(*) as c FROM vault ${userWhere} GROUP BY category`,
64
+ )
65
+ .all(...userParams);
58
66
  } catch (e) {
59
67
  errors.push(`Category count query failed: ${e.message}`);
60
68
  }
@@ -65,9 +73,10 @@ export function gatherVaultStatus(ctx, opts = {}) {
65
73
  try {
66
74
  if (existsSync(config.dbPath)) {
67
75
  dbSizeBytes = statSync(config.dbPath).size;
68
- dbSize = dbSizeBytes > 1024 * 1024
69
- ? `${(dbSizeBytes / 1024 / 1024).toFixed(1)}MB`
70
- : `${(dbSizeBytes / 1024).toFixed(1)}KB`;
76
+ dbSize =
77
+ dbSizeBytes > 1024 * 1024
78
+ ? `${(dbSizeBytes / 1024 / 1024).toFixed(1)}MB`
79
+ : `${(dbSizeBytes / 1024).toFixed(1)}KB`;
71
80
  }
72
81
  } catch (e) {
73
82
  errors.push(`DB size check failed: ${e.message}`);
@@ -77,9 +86,11 @@ export function gatherVaultStatus(ctx, opts = {}) {
77
86
  let stalePaths = false;
78
87
  let staleCount = 0;
79
88
  try {
80
- const result = db.prepare(
81
- `SELECT COUNT(*) as c FROM vault WHERE file_path NOT LIKE ? || '%' ${userAnd}`
82
- ).get(config.vaultDir, ...userParams);
89
+ const result = db
90
+ .prepare(
91
+ `SELECT COUNT(*) as c FROM vault WHERE file_path NOT LIKE ? || '%' ${userAnd}`,
92
+ )
93
+ .get(config.vaultDir, ...userParams);
83
94
  staleCount = result.c;
84
95
  stalePaths = staleCount > 0;
85
96
  } catch (e) {
@@ -89,9 +100,11 @@ export function gatherVaultStatus(ctx, opts = {}) {
89
100
  // Count expired entries pending pruning
90
101
  let expiredCount = 0;
91
102
  try {
92
- expiredCount = db.prepare(
93
- `SELECT COUNT(*) as c FROM vault WHERE expires_at IS NOT NULL AND expires_at <= datetime('now') ${userAnd}`
94
- ).get(...userParams).c;
103
+ expiredCount = db
104
+ .prepare(
105
+ `SELECT COUNT(*) as c FROM vault WHERE expires_at IS NOT NULL AND expires_at <= datetime('now') ${userAnd}`,
106
+ )
107
+ .get(...userParams).c;
95
108
  } catch (e) {
96
109
  errors.push(`Expired count failed: ${e.message}`);
97
110
  }
@@ -99,10 +112,14 @@ export function gatherVaultStatus(ctx, opts = {}) {
99
112
  // Embedding/vector status
100
113
  let embeddingStatus = null;
101
114
  try {
102
- const total = db.prepare(`SELECT COUNT(*) as c FROM vault ${userWhere}`).get(...userParams).c;
103
- const indexed = db.prepare(
104
- `SELECT COUNT(*) as c FROM vault WHERE rowid IN (SELECT rowid FROM vault_vec) ${userAnd}`
105
- ).get(...userParams).c;
115
+ const total = db
116
+ .prepare(`SELECT COUNT(*) as c FROM vault ${userWhere}`)
117
+ .get(...userParams).c;
118
+ const indexed = db
119
+ .prepare(
120
+ `SELECT COUNT(*) as c FROM vault WHERE rowid IN (SELECT rowid FROM vault_vec) ${userAnd}`,
121
+ )
122
+ .get(...userParams).c;
106
123
  embeddingStatus = { indexed, total, missing: total - indexed };
107
124
  } catch (e) {
108
125
  errors.push(`Embedding status check failed: ${e.message}`);