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.
- package/README.md +39 -375
- package/bin/cli.js +26 -67
- package/node_modules/@context-vault/core/package.json +7 -3
- package/node_modules/@context-vault/core/src/capture/file-ops.js +20 -2
- package/node_modules/@context-vault/core/src/capture/import-pipeline.js +0 -34
- package/node_modules/@context-vault/core/src/capture/importers.js +64 -37
- package/node_modules/@context-vault/core/src/capture/ingest-url.js +80 -44
- package/node_modules/@context-vault/core/src/constants.js +8 -0
- package/node_modules/@context-vault/core/src/core/config.js +65 -29
- package/node_modules/@context-vault/core/src/core/files.js +8 -15
- package/node_modules/@context-vault/core/src/core/frontmatter.js +22 -10
- package/node_modules/@context-vault/core/src/core/status.js +32 -15
- package/node_modules/@context-vault/core/src/index/db.js +47 -34
- package/node_modules/@context-vault/core/src/index/embed.js +15 -5
- package/node_modules/@context-vault/core/src/index.js +39 -6
- package/node_modules/@context-vault/core/src/retrieve/index.js +40 -8
- package/node_modules/@context-vault/core/src/server/helpers.js +8 -6
- package/node_modules/@context-vault/core/src/server/tools/context-status.js +24 -10
- package/node_modules/@context-vault/core/src/server/tools/delete-context.js +8 -3
- package/node_modules/@context-vault/core/src/server/tools/get-context.js +117 -35
- package/node_modules/@context-vault/core/src/server/tools/ingest-url.js +5 -4
- package/node_modules/@context-vault/core/src/server/tools/list-context.js +59 -18
- package/node_modules/@context-vault/core/src/server/tools/save-context.js +10 -10
- package/node_modules/@context-vault/core/src/server/tools.js +24 -28
- package/package.json +2 -2
- package/scripts/local-server.js +30 -30
- package/scripts/postinstall.js +25 -10
- package/scripts/prepack.js +18 -15
- package/src/server/index.js +78 -29
- package/app-dist/assets/index-DjXoWapE.css +0 -1
- package/app-dist/assets/index-R4n9Qz4U.js +0 -380
- package/app-dist/index.html +0 -16
- 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(
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
md = md.replace(
|
|
36
|
-
|
|
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(
|
|
40
|
-
|
|
41
|
-
|
|
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(
|
|
46
|
-
|
|
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(
|
|
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(
|
|
53
|
-
|
|
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(
|
|
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(
|
|
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
|
|
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(/'/g, "'")
|
|
88
120
|
.replace(/ /g, " ")
|
|
89
121
|
.replace(/&#(\d+);/g, (_, n) => String.fromCharCode(parseInt(n, 10)))
|
|
90
|
-
.replace(/&#x([0-9a-f]+);/gi, (_, n) =>
|
|
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
|
|
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":
|
|
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 (
|
|
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])
|
|
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
|
-
|
|
32
|
-
|
|
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) {
|
|
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(
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
if (cliArgs.
|
|
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 =
|
|
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;
|
|
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;
|
|
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(
|
|
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(
|
|
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 (
|
|
36
|
-
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
|
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
|
|
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 =
|
|
69
|
-
|
|
70
|
-
|
|
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
|
|
81
|
-
|
|
82
|
-
|
|
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
|
|
93
|
-
|
|
94
|
-
|
|
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
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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}`);
|