context-vault 3.1.5 → 3.1.7
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/bin/cli.js +1369 -1774
- package/node_modules/@context-vault/core/dist/capture.d.ts +1 -1
- package/node_modules/@context-vault/core/dist/capture.d.ts.map +1 -1
- package/node_modules/@context-vault/core/dist/capture.js +34 -47
- package/node_modules/@context-vault/core/dist/capture.js.map +1 -1
- package/node_modules/@context-vault/core/dist/categories.js +30 -30
- package/node_modules/@context-vault/core/dist/config.d.ts +1 -1
- package/node_modules/@context-vault/core/dist/config.d.ts.map +1 -1
- package/node_modules/@context-vault/core/dist/config.js +37 -43
- package/node_modules/@context-vault/core/dist/config.js.map +1 -1
- package/node_modules/@context-vault/core/dist/constants.d.ts.map +1 -1
- package/node_modules/@context-vault/core/dist/constants.js +4 -4
- package/node_modules/@context-vault/core/dist/constants.js.map +1 -1
- package/node_modules/@context-vault/core/dist/db.d.ts +2 -2
- package/node_modules/@context-vault/core/dist/db.d.ts.map +1 -1
- package/node_modules/@context-vault/core/dist/db.js +21 -20
- package/node_modules/@context-vault/core/dist/db.js.map +1 -1
- package/node_modules/@context-vault/core/dist/embed.d.ts.map +1 -1
- package/node_modules/@context-vault/core/dist/embed.js +11 -11
- package/node_modules/@context-vault/core/dist/embed.js.map +1 -1
- package/node_modules/@context-vault/core/dist/files.d.ts.map +1 -1
- package/node_modules/@context-vault/core/dist/files.js +12 -13
- package/node_modules/@context-vault/core/dist/files.js.map +1 -1
- package/node_modules/@context-vault/core/dist/formatters.js +5 -5
- package/node_modules/@context-vault/core/dist/frontmatter.d.ts.map +1 -1
- package/node_modules/@context-vault/core/dist/frontmatter.js +23 -23
- package/node_modules/@context-vault/core/dist/frontmatter.js.map +1 -1
- package/node_modules/@context-vault/core/dist/index.d.ts +1 -1
- package/node_modules/@context-vault/core/dist/index.d.ts.map +1 -1
- package/node_modules/@context-vault/core/dist/index.js +58 -46
- package/node_modules/@context-vault/core/dist/index.js.map +1 -1
- package/node_modules/@context-vault/core/dist/ingest-url.d.ts.map +1 -1
- package/node_modules/@context-vault/core/dist/ingest-url.js +30 -33
- package/node_modules/@context-vault/core/dist/ingest-url.js.map +1 -1
- package/node_modules/@context-vault/core/dist/main.d.ts +13 -13
- package/node_modules/@context-vault/core/dist/main.d.ts.map +1 -1
- package/node_modules/@context-vault/core/dist/main.js +12 -12
- package/node_modules/@context-vault/core/dist/main.js.map +1 -1
- package/node_modules/@context-vault/core/dist/search.d.ts +1 -1
- package/node_modules/@context-vault/core/dist/search.d.ts.map +1 -1
- package/node_modules/@context-vault/core/dist/search.js +20 -22
- package/node_modules/@context-vault/core/dist/search.js.map +1 -1
- package/node_modules/@context-vault/core/dist/types.d.ts +1 -1
- package/node_modules/@context-vault/core/package.json +1 -1
- package/node_modules/@context-vault/core/src/capture.ts +44 -81
- package/node_modules/@context-vault/core/src/categories.ts +30 -30
- package/node_modules/@context-vault/core/src/config.ts +45 -60
- package/node_modules/@context-vault/core/src/constants.ts +8 -10
- package/node_modules/@context-vault/core/src/db.ts +37 -56
- package/node_modules/@context-vault/core/src/embed.ts +15 -26
- package/node_modules/@context-vault/core/src/files.ts +13 -16
- package/node_modules/@context-vault/core/src/formatters.ts +5 -5
- package/node_modules/@context-vault/core/src/frontmatter.ts +26 -30
- package/node_modules/@context-vault/core/src/index.ts +94 -100
- package/node_modules/@context-vault/core/src/ingest-url.ts +56 -93
- package/node_modules/@context-vault/core/src/main.ts +13 -18
- package/node_modules/@context-vault/core/src/search.ts +34 -56
- package/node_modules/@context-vault/core/src/types.ts +1 -1
- package/package.json +2 -2
- package/scripts/postinstall.js +18 -25
- package/scripts/prepack.js +13 -19
- package/src/archive.js +211 -0
- package/src/error-log.js +7 -7
- package/src/helpers.js +11 -13
- package/src/linking.js +8 -11
- package/src/migrate-dirs.js +139 -0
- package/src/register-tools.js +46 -48
- package/src/server.js +73 -99
- package/src/status.js +35 -71
- package/src/telemetry.js +18 -22
- package/src/temporal.js +19 -30
- package/src/tools/clear-context.js +15 -18
- package/src/tools/context-status.js +37 -57
- package/src/tools/create-snapshot.js +45 -57
- package/src/tools/delete-context.js +11 -12
- package/src/tools/get-context.js +112 -160
- package/src/tools/ingest-project.js +66 -86
- package/src/tools/ingest-url.js +25 -41
- package/src/tools/list-buckets.js +19 -25
- package/src/tools/list-context.js +35 -58
- package/src/tools/save-context.js +126 -182
- package/src/tools/session-start.js +46 -62
package/scripts/prepack.js
CHANGED
|
@@ -1,33 +1,27 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
mkdirSync,
|
|
7
|
-
readFileSync,
|
|
8
|
-
writeFileSync,
|
|
9
|
-
} from "node:fs";
|
|
10
|
-
import { join, dirname } from "node:path";
|
|
11
|
-
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { cpSync, rmSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { join, dirname } from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
12
6
|
|
|
13
7
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
14
|
-
const LOCAL_ROOT = join(__dirname,
|
|
15
|
-
const NODE_MODULES = join(LOCAL_ROOT,
|
|
16
|
-
const CORE_SRC = join(LOCAL_ROOT,
|
|
17
|
-
const CORE_DEST = join(NODE_MODULES,
|
|
8
|
+
const LOCAL_ROOT = join(__dirname, '..');
|
|
9
|
+
const NODE_MODULES = join(LOCAL_ROOT, 'node_modules');
|
|
10
|
+
const CORE_SRC = join(LOCAL_ROOT, '..', 'core');
|
|
11
|
+
const CORE_DEST = join(NODE_MODULES, '@context-vault', 'core');
|
|
18
12
|
|
|
19
13
|
// Clean node_modules to prevent workspace deps from leaking into the tarball.
|
|
20
14
|
// Only @context-vault/core should be bundled.
|
|
21
15
|
rmSync(NODE_MODULES, { recursive: true, force: true });
|
|
22
16
|
|
|
23
17
|
// Ensure target directory exists
|
|
24
|
-
mkdirSync(join(NODE_MODULES,
|
|
18
|
+
mkdirSync(join(NODE_MODULES, '@context-vault'), { recursive: true });
|
|
25
19
|
|
|
26
20
|
// Copy core package (dereference symlinks)
|
|
27
21
|
cpSync(CORE_SRC, CORE_DEST, { recursive: true, dereference: true });
|
|
28
22
|
|
|
29
23
|
// Remove nested node_modules from the copy
|
|
30
|
-
rmSync(join(CORE_DEST,
|
|
24
|
+
rmSync(join(CORE_DEST, 'node_modules'), { recursive: true, force: true });
|
|
31
25
|
|
|
32
26
|
// Strip all dependencies from the bundled core's package.json.
|
|
33
27
|
// Core's deps (sqlite-vec, MCP SDK) are hoisted to
|
|
@@ -35,9 +29,9 @@ rmSync(join(CORE_DEST, "node_modules"), { recursive: true, force: true });
|
|
|
35
29
|
// dynamically imported and installed via postinstall. Leaving them
|
|
36
30
|
// in the bundled core causes duplicate resolution that breaks native
|
|
37
31
|
// module install scripts in global npm contexts.
|
|
38
|
-
const corePkgPath = join(CORE_DEST,
|
|
39
|
-
const corePkg = JSON.parse(readFileSync(corePkgPath,
|
|
32
|
+
const corePkgPath = join(CORE_DEST, 'package.json');
|
|
33
|
+
const corePkg = JSON.parse(readFileSync(corePkgPath, 'utf8'));
|
|
40
34
|
delete corePkg.dependencies;
|
|
41
|
-
writeFileSync(corePkgPath, JSON.stringify(corePkg, null, 2) +
|
|
35
|
+
writeFileSync(corePkgPath, JSON.stringify(corePkg, null, 2) + '\n');
|
|
42
36
|
|
|
43
|
-
console.log(
|
|
37
|
+
console.log('[prepack] Bundled @context-vault/core into node_modules');
|
package/src/archive.js
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, renameSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { join, dirname, relative } from 'node:path';
|
|
3
|
+
import { categoryDirFor, categoryFor, defaultTierFor } from '@context-vault/core/categories';
|
|
4
|
+
import { parseFrontmatter, parseEntryFromMarkdown } from '@context-vault/core/frontmatter';
|
|
5
|
+
import { DEFAULT_LIFECYCLE } from '@context-vault/core/constants';
|
|
6
|
+
import { walkDir } from '@context-vault/core/files';
|
|
7
|
+
import { indexEntry } from '@context-vault/core/index';
|
|
8
|
+
|
|
9
|
+
const VALID_TIERS = new Set(['ephemeral', 'working', 'durable']);
|
|
10
|
+
const VALID_CATEGORIES = new Set(['knowledge', 'entity', 'event']);
|
|
11
|
+
|
|
12
|
+
function resolveLifecycle(config) {
|
|
13
|
+
return config?.lifecycle ?? structuredClone(DEFAULT_LIFECYCLE);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function archiveDir(vaultDir) {
|
|
17
|
+
return join(vaultDir, '_archive');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function findArchiveCandidates(ctx) {
|
|
21
|
+
const lifecycle = resolveLifecycle(ctx.config);
|
|
22
|
+
const now = new Date();
|
|
23
|
+
const candidates = [];
|
|
24
|
+
const seen = new Set();
|
|
25
|
+
|
|
26
|
+
for (const [key, rules] of Object.entries(lifecycle)) {
|
|
27
|
+
if (!rules?.archiveAfterDays) continue;
|
|
28
|
+
const cutoff = new Date(now.getTime() - rules.archiveAfterDays * 86400000).toISOString();
|
|
29
|
+
|
|
30
|
+
const isTier = VALID_TIERS.has(key);
|
|
31
|
+
const isCategory = VALID_CATEGORIES.has(key);
|
|
32
|
+
if (!isTier && !isCategory) continue;
|
|
33
|
+
|
|
34
|
+
const column = isTier ? 'tier' : 'category';
|
|
35
|
+
const rows = ctx.db
|
|
36
|
+
.prepare(
|
|
37
|
+
`SELECT id, kind, category, title, tier, file_path, created_at, updated_at
|
|
38
|
+
FROM vault
|
|
39
|
+
WHERE ${column} = ? AND COALESCE(updated_at, created_at) <= ?
|
|
40
|
+
ORDER BY COALESCE(updated_at, created_at) ASC`
|
|
41
|
+
)
|
|
42
|
+
.all(key, cutoff);
|
|
43
|
+
|
|
44
|
+
for (const row of rows) {
|
|
45
|
+
if (seen.has(row.id)) continue;
|
|
46
|
+
seen.add(row.id);
|
|
47
|
+
candidates.push({
|
|
48
|
+
...row,
|
|
49
|
+
reason: `${column}=${key}, archiveAfterDays=${rules.archiveAfterDays}`,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return candidates;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function removeFromIndex(ctx, id) {
|
|
58
|
+
const rowidRow = ctx.stmts.getRowid.get(id);
|
|
59
|
+
if (rowidRow?.rowid) {
|
|
60
|
+
try {
|
|
61
|
+
ctx.deleteVec(Number(rowidRow.rowid));
|
|
62
|
+
} catch {}
|
|
63
|
+
}
|
|
64
|
+
ctx.stmts.deleteEntry.run(id);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export async function archiveEntries(ctx) {
|
|
68
|
+
const candidates = findArchiveCandidates(ctx);
|
|
69
|
+
if (candidates.length === 0) return { archived: candidates, count: 0 };
|
|
70
|
+
|
|
71
|
+
const vaultDir = ctx.config.vaultDir;
|
|
72
|
+
const archRoot = archiveDir(vaultDir);
|
|
73
|
+
let count = 0;
|
|
74
|
+
|
|
75
|
+
for (const entry of candidates) {
|
|
76
|
+
const filePath = entry.file_path;
|
|
77
|
+
if (!filePath) {
|
|
78
|
+
removeFromIndex(ctx, entry.id);
|
|
79
|
+
count++;
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const rel = relative(vaultDir, filePath);
|
|
84
|
+
if (rel.startsWith('..') || rel.startsWith('_archive')) continue;
|
|
85
|
+
|
|
86
|
+
const destPath = join(archRoot, rel);
|
|
87
|
+
const destDir = dirname(destPath);
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
mkdirSync(destDir, { recursive: true });
|
|
91
|
+
renameSync(filePath, destPath);
|
|
92
|
+
} catch (e) {
|
|
93
|
+
if (e.code === 'ENOENT' && !existsSync(filePath)) {
|
|
94
|
+
// File already gone — just remove from index
|
|
95
|
+
} else {
|
|
96
|
+
throw e;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
removeFromIndex(ctx, entry.id);
|
|
101
|
+
count++;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return { archived: candidates, count };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export async function restoreEntry(ctx, entryId) {
|
|
108
|
+
const vaultDir = ctx.config.vaultDir;
|
|
109
|
+
const archRoot = archiveDir(vaultDir);
|
|
110
|
+
|
|
111
|
+
if (!existsSync(archRoot)) {
|
|
112
|
+
return { restored: false, reason: 'no _archive directory' };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const mdFiles = walkDir(archRoot);
|
|
116
|
+
let targetFile = null;
|
|
117
|
+
|
|
118
|
+
for (const { filePath } of mdFiles) {
|
|
119
|
+
try {
|
|
120
|
+
const raw = readFileSync(filePath, 'utf-8');
|
|
121
|
+
const { meta } = parseFrontmatter(raw);
|
|
122
|
+
if (meta.id === entryId) {
|
|
123
|
+
targetFile = filePath;
|
|
124
|
+
break;
|
|
125
|
+
}
|
|
126
|
+
} catch {
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (!targetFile) {
|
|
132
|
+
return { restored: false, reason: `entry ${entryId} not found in _archive` };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const rel = relative(archRoot, targetFile);
|
|
136
|
+
const destPath = join(vaultDir, rel);
|
|
137
|
+
const destDir = dirname(destPath);
|
|
138
|
+
|
|
139
|
+
if (existsSync(destPath)) {
|
|
140
|
+
return { restored: false, reason: `destination already exists: ${destPath}` };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
mkdirSync(destDir, { recursive: true });
|
|
144
|
+
renameSync(targetFile, destPath);
|
|
145
|
+
|
|
146
|
+
const raw = readFileSync(destPath, 'utf-8');
|
|
147
|
+
const { meta, body: rawBody } = parseFrontmatter(raw);
|
|
148
|
+
|
|
149
|
+
const relFromVault = relative(vaultDir, destPath);
|
|
150
|
+
const kindDir = relFromVault.split('/').filter(Boolean);
|
|
151
|
+
let kind =
|
|
152
|
+
meta.kind || (kindDir.length >= 2 ? kindDir[kindDir.length - 2] : kindDir[0]) || 'note';
|
|
153
|
+
|
|
154
|
+
const parsed = parseEntryFromMarkdown(kind, rawBody, meta);
|
|
155
|
+
const category = categoryFor(kind);
|
|
156
|
+
|
|
157
|
+
await indexEntry(ctx, {
|
|
158
|
+
id: meta.id || entryId,
|
|
159
|
+
kind,
|
|
160
|
+
category,
|
|
161
|
+
title: parsed.title,
|
|
162
|
+
body: parsed.body,
|
|
163
|
+
meta: parsed.meta,
|
|
164
|
+
tags: Array.isArray(meta.tags) ? meta.tags : null,
|
|
165
|
+
source: meta.source || 'archive-restore',
|
|
166
|
+
filePath: destPath,
|
|
167
|
+
createdAt: meta.created || new Date().toISOString(),
|
|
168
|
+
identity_key: meta.identity_key || null,
|
|
169
|
+
expires_at: meta.expires_at || null,
|
|
170
|
+
tier: meta.tier || defaultTierFor(kind),
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
return { restored: true, filePath: destPath, kind, id: meta.id || entryId };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export function countArchivedEntries(vaultDir) {
|
|
177
|
+
const archRoot = archiveDir(vaultDir);
|
|
178
|
+
if (!existsSync(archRoot)) return 0;
|
|
179
|
+
try {
|
|
180
|
+
return walkDir(archRoot).length;
|
|
181
|
+
} catch {
|
|
182
|
+
return 0;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export function listArchivedEntries(vaultDir) {
|
|
187
|
+
const archRoot = archiveDir(vaultDir);
|
|
188
|
+
if (!existsSync(archRoot)) return [];
|
|
189
|
+
|
|
190
|
+
const entries = [];
|
|
191
|
+
try {
|
|
192
|
+
const mdFiles = walkDir(archRoot);
|
|
193
|
+
for (const { filePath, relDir } of mdFiles) {
|
|
194
|
+
try {
|
|
195
|
+
const raw = readFileSync(filePath, 'utf-8');
|
|
196
|
+
const { meta, body } = parseFrontmatter(raw);
|
|
197
|
+
entries.push({
|
|
198
|
+
id: meta.id || null,
|
|
199
|
+
kind: relDir?.split('/').pop() || 'unknown',
|
|
200
|
+
title: meta.title || body.slice(0, 80),
|
|
201
|
+
tags: meta.tags || [],
|
|
202
|
+
created: meta.created || null,
|
|
203
|
+
filePath,
|
|
204
|
+
});
|
|
205
|
+
} catch {
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
} catch {}
|
|
210
|
+
return entries;
|
|
211
|
+
}
|
package/src/error-log.js
CHANGED
|
@@ -5,13 +5,13 @@ import {
|
|
|
5
5
|
readFileSync,
|
|
6
6
|
statSync,
|
|
7
7
|
writeFileSync,
|
|
8
|
-
} from
|
|
9
|
-
import { join } from
|
|
8
|
+
} from 'node:fs';
|
|
9
|
+
import { join } from 'node:path';
|
|
10
10
|
|
|
11
11
|
const MAX_LOG_SIZE = 1024 * 1024;
|
|
12
12
|
|
|
13
13
|
export function errorLogPath(dataDir) {
|
|
14
|
-
return join(dataDir,
|
|
14
|
+
return join(dataDir, 'error.log');
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
export function appendErrorLog(dataDir, entry) {
|
|
@@ -19,9 +19,9 @@ export function appendErrorLog(dataDir, entry) {
|
|
|
19
19
|
mkdirSync(dataDir, { recursive: true });
|
|
20
20
|
const logPath = errorLogPath(dataDir);
|
|
21
21
|
if (existsSync(logPath) && statSync(logPath).size >= MAX_LOG_SIZE) {
|
|
22
|
-
writeFileSync(logPath,
|
|
22
|
+
writeFileSync(logPath, '');
|
|
23
23
|
}
|
|
24
|
-
appendFileSync(logPath, JSON.stringify(entry) +
|
|
24
|
+
appendFileSync(logPath, JSON.stringify(entry) + '\n');
|
|
25
25
|
} catch {
|
|
26
26
|
// intentionally swallowed
|
|
27
27
|
}
|
|
@@ -31,8 +31,8 @@ export function errorLogCount(dataDir) {
|
|
|
31
31
|
try {
|
|
32
32
|
const logPath = errorLogPath(dataDir);
|
|
33
33
|
if (!existsSync(logPath)) return 0;
|
|
34
|
-
return readFileSync(logPath,
|
|
35
|
-
.split(
|
|
34
|
+
return readFileSync(logPath, 'utf-8')
|
|
35
|
+
.split('\n')
|
|
36
36
|
.filter((l) => l.trim()).length;
|
|
37
37
|
} catch {
|
|
38
38
|
return 0;
|
package/src/helpers.js
CHANGED
|
@@ -1,19 +1,17 @@
|
|
|
1
|
-
import { readFileSync } from
|
|
2
|
-
import { join, dirname } from
|
|
3
|
-
import { fileURLToPath } from
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { join, dirname } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
4
|
|
|
5
5
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
6
|
-
const pkg = JSON.parse(
|
|
7
|
-
readFileSync(join(__dirname, "..", "package.json"), "utf-8"),
|
|
8
|
-
);
|
|
6
|
+
const pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf-8'));
|
|
9
7
|
|
|
10
8
|
export function ok(text) {
|
|
11
|
-
return { content: [{ type:
|
|
9
|
+
return { content: [{ type: 'text', text }] };
|
|
12
10
|
}
|
|
13
11
|
|
|
14
|
-
export function err(text, code =
|
|
12
|
+
export function err(text, code = 'UNKNOWN', meta = {}) {
|
|
15
13
|
return {
|
|
16
|
-
content: [{ type:
|
|
14
|
+
content: [{ type: 'text', text }],
|
|
17
15
|
isError: true,
|
|
18
16
|
code,
|
|
19
17
|
_meta: {
|
|
@@ -29,7 +27,7 @@ export function err(text, code = "UNKNOWN", meta = {}) {
|
|
|
29
27
|
export function errWithHint(text, code, hint) {
|
|
30
28
|
const prompt = hint
|
|
31
29
|
? `\n\n**Debug with AI:** Paste this into Claude Code or your AI assistant:\n> "${hint}"`
|
|
32
|
-
:
|
|
30
|
+
: '';
|
|
33
31
|
return err(text + prompt, code);
|
|
34
32
|
}
|
|
35
33
|
|
|
@@ -37,8 +35,8 @@ export function ensureVaultExists(config) {
|
|
|
37
35
|
if (!config.vaultDirExists) {
|
|
38
36
|
return errWithHint(
|
|
39
37
|
`Vault directory not found: ${config.vaultDir}. Run context-status for diagnostics.`,
|
|
40
|
-
|
|
41
|
-
"My context-vault can't find the vault directory. Run `context-vault doctor` and help me fix it."
|
|
38
|
+
'VAULT_NOT_FOUND',
|
|
39
|
+
"My context-vault can't find the vault directory. Run `context-vault doctor` and help me fix it."
|
|
42
40
|
);
|
|
43
41
|
}
|
|
44
42
|
return null;
|
|
@@ -48,7 +46,7 @@ export function ensureValidKind(kind) {
|
|
|
48
46
|
if (!/^[a-z][a-z0-9_-]*$/.test(kind)) {
|
|
49
47
|
return err(
|
|
50
48
|
"Required: kind (lowercase alphanumeric, e.g. 'insight', 'reference')",
|
|
51
|
-
|
|
49
|
+
'INVALID_KIND'
|
|
52
50
|
);
|
|
53
51
|
}
|
|
54
52
|
return null;
|
package/src/linking.js
CHANGED
|
@@ -3,7 +3,7 @@ export function parseRelatedTo(raw) {
|
|
|
3
3
|
try {
|
|
4
4
|
const parsed = JSON.parse(raw);
|
|
5
5
|
if (!Array.isArray(parsed)) return [];
|
|
6
|
-
return parsed.filter((id) => typeof id ===
|
|
6
|
+
return parsed.filter((id) => typeof id === 'string' && id.trim());
|
|
7
7
|
} catch {
|
|
8
8
|
return [];
|
|
9
9
|
}
|
|
@@ -12,14 +12,14 @@ export function parseRelatedTo(raw) {
|
|
|
12
12
|
export function resolveLinks(db, ids) {
|
|
13
13
|
if (!ids.length) return [];
|
|
14
14
|
const unique = [...new Set(ids)];
|
|
15
|
-
const placeholders = unique.map(() =>
|
|
15
|
+
const placeholders = unique.map(() => '?').join(',');
|
|
16
16
|
try {
|
|
17
17
|
return db
|
|
18
18
|
.prepare(
|
|
19
19
|
`SELECT * FROM vault
|
|
20
20
|
WHERE id IN (${placeholders})
|
|
21
21
|
AND (expires_at IS NULL OR expires_at > datetime('now'))
|
|
22
|
-
AND superseded_by IS NULL
|
|
22
|
+
AND superseded_by IS NULL`
|
|
23
23
|
)
|
|
24
24
|
.all(...unique);
|
|
25
25
|
} catch {
|
|
@@ -36,7 +36,7 @@ export function resolveBacklinks(db, entryId) {
|
|
|
36
36
|
`SELECT * FROM vault
|
|
37
37
|
WHERE related_to LIKE ?
|
|
38
38
|
AND (expires_at IS NULL OR expires_at > datetime('now'))
|
|
39
|
-
AND superseded_by IS NULL
|
|
39
|
+
AND superseded_by IS NULL`
|
|
40
40
|
)
|
|
41
41
|
.all(likePattern);
|
|
42
42
|
} catch {
|
|
@@ -54,9 +54,7 @@ export function collectLinkedEntries(db, primaryEntries) {
|
|
|
54
54
|
if (!primaryIds.has(id)) forwardIds.push(id);
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
|
-
const forwardEntries = resolveLinks(db, forwardIds).filter(
|
|
58
|
-
(e) => !primaryIds.has(e.id),
|
|
59
|
-
);
|
|
57
|
+
const forwardEntries = resolveLinks(db, forwardIds).filter((e) => !primaryIds.has(e.id));
|
|
60
58
|
|
|
61
59
|
const backwardSeen = new Set();
|
|
62
60
|
const backwardEntries = [];
|
|
@@ -75,11 +73,10 @@ export function collectLinkedEntries(db, primaryEntries) {
|
|
|
75
73
|
|
|
76
74
|
export function validateRelatedTo(relatedTo) {
|
|
77
75
|
if (relatedTo === undefined || relatedTo === null) return null;
|
|
78
|
-
if (!Array.isArray(relatedTo))
|
|
79
|
-
return "related_to must be an array of entry IDs";
|
|
76
|
+
if (!Array.isArray(relatedTo)) return 'related_to must be an array of entry IDs';
|
|
80
77
|
for (const id of relatedTo) {
|
|
81
|
-
if (typeof id !==
|
|
82
|
-
return
|
|
78
|
+
if (typeof id !== 'string' || !id.trim()) {
|
|
79
|
+
return 'each related_to entry must be a non-empty string ID';
|
|
83
80
|
}
|
|
84
81
|
if (id.length > 32) {
|
|
85
82
|
return `related_to ID too long (max 32 chars): "${id.slice(0, 32)}..."`;
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import {
|
|
2
|
+
existsSync,
|
|
3
|
+
readdirSync,
|
|
4
|
+
mkdirSync,
|
|
5
|
+
renameSync,
|
|
6
|
+
copyFileSync,
|
|
7
|
+
rmSync,
|
|
8
|
+
statSync,
|
|
9
|
+
} from 'node:fs';
|
|
10
|
+
import { join } from 'node:path';
|
|
11
|
+
|
|
12
|
+
export const PLURAL_TO_SINGULAR = {
|
|
13
|
+
insights: 'insight',
|
|
14
|
+
decisions: 'decision',
|
|
15
|
+
patterns: 'pattern',
|
|
16
|
+
statuses: 'status',
|
|
17
|
+
analyses: 'analysis',
|
|
18
|
+
contacts: 'contact',
|
|
19
|
+
projects: 'project',
|
|
20
|
+
tools: 'tool',
|
|
21
|
+
sources: 'source',
|
|
22
|
+
conversations: 'conversation',
|
|
23
|
+
messages: 'message',
|
|
24
|
+
sessions: 'session',
|
|
25
|
+
logs: 'log',
|
|
26
|
+
feedbacks: 'feedback',
|
|
27
|
+
notes: 'note',
|
|
28
|
+
prompts: 'prompt',
|
|
29
|
+
documents: 'document',
|
|
30
|
+
references: 'reference',
|
|
31
|
+
tasks: 'task',
|
|
32
|
+
buckets: 'bucket',
|
|
33
|
+
architectures: 'architecture',
|
|
34
|
+
briefs: 'brief',
|
|
35
|
+
companies: 'company',
|
|
36
|
+
discoveries: 'discovery',
|
|
37
|
+
events: 'event',
|
|
38
|
+
ideas: 'idea',
|
|
39
|
+
issues: 'issue',
|
|
40
|
+
agents: 'agent',
|
|
41
|
+
'session-summaries': 'session-summary',
|
|
42
|
+
'session-reviews': 'session-review',
|
|
43
|
+
'user-prompts': 'user-prompt',
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const CATEGORY_DIRS = ['knowledge', 'entities', 'events'];
|
|
47
|
+
|
|
48
|
+
function countMdFiles(dir) {
|
|
49
|
+
let count = 0;
|
|
50
|
+
try {
|
|
51
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
52
|
+
if (entry.isDirectory()) {
|
|
53
|
+
count += countMdFiles(join(dir, entry.name));
|
|
54
|
+
} else if (entry.isFile() && entry.name.endsWith('.md')) {
|
|
55
|
+
count++;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
} catch {}
|
|
59
|
+
return count;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function planMigration(vaultDir) {
|
|
63
|
+
const ops = [];
|
|
64
|
+
|
|
65
|
+
for (const catName of CATEGORY_DIRS) {
|
|
66
|
+
const catDir = join(vaultDir, catName);
|
|
67
|
+
if (!existsSync(catDir) || !statSync(catDir).isDirectory()) continue;
|
|
68
|
+
|
|
69
|
+
let entries;
|
|
70
|
+
try {
|
|
71
|
+
entries = readdirSync(catDir, { withFileTypes: true });
|
|
72
|
+
} catch {
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
for (const entry of entries) {
|
|
77
|
+
if (!entry.isDirectory()) continue;
|
|
78
|
+
const dirName = entry.name;
|
|
79
|
+
const singular = PLURAL_TO_SINGULAR[dirName];
|
|
80
|
+
if (!singular) continue;
|
|
81
|
+
|
|
82
|
+
const pluralDir = join(catDir, dirName);
|
|
83
|
+
const singularDir = join(catDir, singular);
|
|
84
|
+
if (pluralDir === singularDir) continue;
|
|
85
|
+
|
|
86
|
+
const fileCount = countMdFiles(pluralDir);
|
|
87
|
+
const singularExists = existsSync(singularDir);
|
|
88
|
+
|
|
89
|
+
ops.push({
|
|
90
|
+
action: singularExists ? 'merge' : 'rename',
|
|
91
|
+
pluralDir,
|
|
92
|
+
singularDir,
|
|
93
|
+
pluralName: dirName,
|
|
94
|
+
singularName: singular,
|
|
95
|
+
fileCount,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return ops;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function mergeDir(src, dst) {
|
|
104
|
+
mkdirSync(dst, { recursive: true });
|
|
105
|
+
for (const entry of readdirSync(src, { withFileTypes: true })) {
|
|
106
|
+
const srcPath = join(src, entry.name);
|
|
107
|
+
const dstPath = join(dst, entry.name);
|
|
108
|
+
if (entry.isDirectory()) {
|
|
109
|
+
mergeDir(srcPath, dstPath);
|
|
110
|
+
} else if (entry.isFile()) {
|
|
111
|
+
if (!existsSync(dstPath)) {
|
|
112
|
+
copyFileSync(srcPath, dstPath);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function executeMigration(ops) {
|
|
119
|
+
let renamed = 0;
|
|
120
|
+
let merged = 0;
|
|
121
|
+
const errors = [];
|
|
122
|
+
|
|
123
|
+
for (const op of ops) {
|
|
124
|
+
try {
|
|
125
|
+
if (op.action === 'rename') {
|
|
126
|
+
renameSync(op.pluralDir, op.singularDir);
|
|
127
|
+
renamed++;
|
|
128
|
+
} else {
|
|
129
|
+
mergeDir(op.pluralDir, op.singularDir);
|
|
130
|
+
rmSync(op.pluralDir, { recursive: true, force: true });
|
|
131
|
+
merged++;
|
|
132
|
+
}
|
|
133
|
+
} catch (e) {
|
|
134
|
+
errors.push(`${op.pluralName} → ${op.singularName}: ${e.message}`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return { renamed, merged, errors };
|
|
139
|
+
}
|