context-vault 3.1.6 → 3.1.8
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/dist/archive.d.ts +23 -0
- package/dist/archive.d.ts.map +1 -0
- package/dist/archive.js +197 -0
- package/dist/archive.js.map +1 -0
- package/dist/consolidation.d.ts +14 -0
- package/dist/consolidation.d.ts.map +1 -0
- package/dist/consolidation.js +59 -0
- package/dist/consolidation.js.map +1 -0
- package/dist/error-log.d.ts +4 -0
- package/dist/error-log.d.ts.map +1 -0
- package/dist/error-log.js +33 -0
- package/dist/error-log.js.map +1 -0
- package/dist/helpers.d.ts +10 -0
- package/dist/helpers.d.ts.map +1 -0
- package/dist/helpers.js +42 -0
- package/dist/helpers.js.map +1 -0
- package/dist/linking.d.ts +13 -0
- package/dist/linking.d.ts.map +1 -0
- package/dist/linking.js +86 -0
- package/dist/linking.js.map +1 -0
- package/dist/migrate-dirs.d.ts +16 -0
- package/dist/migrate-dirs.d.ts.map +1 -0
- package/dist/migrate-dirs.js +127 -0
- package/dist/migrate-dirs.js.map +1 -0
- package/dist/register-tools.d.ts +3 -0
- package/dist/register-tools.d.ts.map +1 -0
- package/dist/register-tools.js +161 -0
- package/dist/register-tools.js.map +1 -0
- package/dist/server.d.ts +3 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +241 -0
- package/dist/server.js.map +1 -0
- package/dist/status.d.ts +18 -0
- package/dist/status.d.ts.map +1 -0
- package/dist/status.js +265 -0
- package/dist/status.js.map +1 -0
- package/dist/telemetry.d.ts +6 -0
- package/dist/telemetry.d.ts.map +1 -0
- package/dist/telemetry.js +74 -0
- package/dist/telemetry.js.map +1 -0
- package/dist/temporal.d.ts +9 -0
- package/dist/temporal.d.ts.map +1 -0
- package/dist/temporal.js +76 -0
- package/dist/temporal.js.map +1 -0
- package/dist/tools/clear-context.d.ts +11 -0
- package/dist/tools/clear-context.d.ts.map +1 -0
- package/dist/tools/clear-context.js +28 -0
- package/dist/tools/clear-context.js.map +1 -0
- package/dist/tools/context-status.d.ts +6 -0
- package/dist/tools/context-status.d.ts.map +1 -0
- package/dist/tools/context-status.js +160 -0
- package/dist/tools/context-status.js.map +1 -0
- package/dist/tools/create-snapshot.d.ts +13 -0
- package/dist/tools/create-snapshot.d.ts.map +1 -0
- package/dist/tools/create-snapshot.js +161 -0
- package/dist/tools/create-snapshot.js.map +1 -0
- package/dist/tools/delete-context.d.ts +9 -0
- package/dist/tools/delete-context.d.ts.map +1 -0
- package/dist/tools/delete-context.js +45 -0
- package/dist/tools/delete-context.js.map +1 -0
- package/dist/tools/get-context.d.ts +85 -0
- package/dist/tools/get-context.d.ts.map +1 -0
- package/dist/tools/get-context.js +576 -0
- package/dist/tools/get-context.js.map +1 -0
- package/dist/tools/ingest-project.d.ts +11 -0
- package/dist/tools/ingest-project.d.ts.map +1 -0
- package/dist/tools/ingest-project.js +226 -0
- package/dist/tools/ingest-project.js.map +1 -0
- package/dist/tools/ingest-url.d.ts +11 -0
- package/dist/tools/ingest-url.d.ts.map +1 -0
- package/dist/tools/ingest-url.js +62 -0
- package/dist/tools/ingest-url.js.map +1 -0
- package/dist/tools/list-buckets.d.ts +9 -0
- package/dist/tools/list-buckets.d.ts.map +1 -0
- package/dist/tools/list-buckets.js +76 -0
- package/dist/tools/list-buckets.js.map +1 -0
- package/dist/tools/list-context.d.ts +19 -0
- package/dist/tools/list-context.d.ts.map +1 -0
- package/dist/tools/list-context.js +110 -0
- package/dist/tools/list-context.js.map +1 -0
- package/dist/tools/save-context.d.ts +36 -0
- package/dist/tools/save-context.d.ts.map +1 -0
- package/dist/tools/save-context.js +458 -0
- package/dist/tools/save-context.js.map +1 -0
- package/dist/tools/session-start.d.ts +11 -0
- package/dist/tools/session-start.d.ts.map +1 -0
- package/dist/tools/session-start.js +224 -0
- package/dist/tools/session-start.js.map +1 -0
- package/dist/types.d.ts +37 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- 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 +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 +10 -4
- package/scripts/postinstall.js +18 -25
- package/scripts/prepack.js +13 -19
- package/src/archive.ts +244 -0
- package/src/consolidation.ts +78 -0
- package/src/{error-log.js → error-log.ts} +10 -10
- package/src/helpers.ts +61 -0
- package/src/{linking.js → linking.ts} +22 -20
- package/src/migrate-dirs.ts +152 -0
- package/src/register-tools.ts +183 -0
- package/src/{server.js → server.ts} +89 -109
- package/src/{status.js → status.ts} +94 -108
- package/src/telemetry.ts +80 -0
- package/src/{temporal.js → temporal.ts} +29 -33
- package/src/tools/clear-context.ts +41 -0
- package/src/tools/{context-status.js → context-status.ts} +43 -66
- package/src/tools/{create-snapshot.js → create-snapshot.ts} +54 -65
- package/src/tools/delete-context.ts +53 -0
- package/src/tools/{get-context.js → get-context.ts} +142 -205
- package/src/tools/ingest-project.ts +260 -0
- package/src/tools/ingest-url.ts +74 -0
- package/src/tools/{list-buckets.js → list-buckets.ts} +27 -37
- package/src/tools/{list-context.js → list-context.ts} +46 -71
- package/src/tools/{save-context.js → save-context.ts} +148 -204
- package/src/tools/{session-start.js → session-start.ts} +72 -79
- package/src/types.ts +29 -0
- package/src/helpers.js +0 -57
- package/src/register-tools.js +0 -175
- package/src/telemetry.js +0 -80
- package/src/tools/clear-context.js +0 -47
- package/src/tools/delete-context.js +0 -54
- package/src/tools/ingest-project.js +0 -272
- package/src/tools/ingest-url.js +0 -87
package/scripts/postinstall.js
CHANGED
|
@@ -9,15 +9,15 @@
|
|
|
9
9
|
* 2. Ensures data directory exists.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import { execSync } from
|
|
13
|
-
import { existsSync, mkdirSync } from
|
|
14
|
-
import { join, dirname } from
|
|
15
|
-
import { fileURLToPath } from
|
|
16
|
-
import { homedir } from
|
|
12
|
+
import { execSync } from 'node:child_process';
|
|
13
|
+
import { existsSync, mkdirSync } from 'node:fs';
|
|
14
|
+
import { join, dirname } from 'node:path';
|
|
15
|
+
import { fileURLToPath } from 'node:url';
|
|
16
|
+
import { homedir } from 'node:os';
|
|
17
17
|
|
|
18
18
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
19
|
-
const PKG_ROOT = join(__dirname,
|
|
20
|
-
const NODE_MODULES = join(PKG_ROOT,
|
|
19
|
+
const PKG_ROOT = join(__dirname, '..');
|
|
20
|
+
const NODE_MODULES = join(PKG_ROOT, 'node_modules');
|
|
21
21
|
|
|
22
22
|
async function main() {
|
|
23
23
|
// ── 1. Install @huggingface/transformers (optional) ───────────────────
|
|
@@ -26,33 +26,26 @@ async function main() {
|
|
|
26
26
|
// context-vault only uses text embeddings, not image processing.
|
|
27
27
|
// Check the package's own node_modules (not general import resolution,
|
|
28
28
|
// which may find it in the workspace during `npm install -g ./tarball`).
|
|
29
|
-
const transformersDir = join(NODE_MODULES,
|
|
29
|
+
const transformersDir = join(NODE_MODULES, '@huggingface', 'transformers');
|
|
30
30
|
if (!existsSync(transformersDir)) {
|
|
31
|
-
console.log(
|
|
32
|
-
"[context-vault] Installing embedding support (@huggingface/transformers)...",
|
|
33
|
-
);
|
|
31
|
+
console.log('[context-vault] Installing embedding support (@huggingface/transformers)...');
|
|
34
32
|
try {
|
|
35
|
-
execSync(
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
},
|
|
42
|
-
);
|
|
43
|
-
console.log("[context-vault] Embedding support installed.");
|
|
33
|
+
execSync('npm install --no-save --ignore-scripts @huggingface/transformers@^3.0.0', {
|
|
34
|
+
stdio: 'inherit',
|
|
35
|
+
timeout: 120000,
|
|
36
|
+
cwd: PKG_ROOT,
|
|
37
|
+
});
|
|
38
|
+
console.log('[context-vault] Embedding support installed.');
|
|
44
39
|
} catch {
|
|
40
|
+
console.error('[context-vault] Warning: could not install @huggingface/transformers.');
|
|
45
41
|
console.error(
|
|
46
|
-
|
|
47
|
-
);
|
|
48
|
-
console.error(
|
|
49
|
-
"[context-vault] Semantic search will be unavailable; full-text search still works.",
|
|
42
|
+
'[context-vault] Semantic search will be unavailable; full-text search still works.'
|
|
50
43
|
);
|
|
51
44
|
}
|
|
52
45
|
}
|
|
53
46
|
|
|
54
47
|
// ── 2. Ensure data dir exists ────────────────────────────────────────
|
|
55
|
-
const DATA_DIR = join(homedir(),
|
|
48
|
+
const DATA_DIR = join(homedir(), '.context-mcp');
|
|
56
49
|
mkdirSync(DATA_DIR, { recursive: true });
|
|
57
50
|
}
|
|
58
51
|
|
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.ts
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
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
|
+
import type { LocalCtx } from './types.js';
|
|
9
|
+
import type { VaultConfig } from '@context-vault/core/types';
|
|
10
|
+
|
|
11
|
+
const VALID_TIERS = new Set(['ephemeral', 'working', 'durable']);
|
|
12
|
+
const VALID_CATEGORIES = new Set(['knowledge', 'entity', 'event']);
|
|
13
|
+
|
|
14
|
+
function resolveLifecycle(config: VaultConfig): Record<string, { archiveAfterDays?: number }> {
|
|
15
|
+
return config?.lifecycle ?? structuredClone(DEFAULT_LIFECYCLE);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function archiveDir(vaultDir: string): string {
|
|
19
|
+
return join(vaultDir, '_archive');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function findArchiveCandidates(ctx: LocalCtx): unknown[] {
|
|
23
|
+
const lifecycle = resolveLifecycle(ctx.config);
|
|
24
|
+
const now = new Date();
|
|
25
|
+
const candidates: unknown[] = [];
|
|
26
|
+
const seen = new Set<string>();
|
|
27
|
+
|
|
28
|
+
for (const [key, rules] of Object.entries(lifecycle)) {
|
|
29
|
+
if (!rules?.archiveAfterDays) continue;
|
|
30
|
+
const cutoff = new Date(now.getTime() - rules.archiveAfterDays * 86400000).toISOString();
|
|
31
|
+
|
|
32
|
+
const isTier = VALID_TIERS.has(key);
|
|
33
|
+
const isCategory = VALID_CATEGORIES.has(key);
|
|
34
|
+
if (!isTier && !isCategory) continue;
|
|
35
|
+
|
|
36
|
+
const column = isTier ? 'tier' : 'category';
|
|
37
|
+
const rows = ctx.db
|
|
38
|
+
.prepare(
|
|
39
|
+
`SELECT id, kind, category, title, tier, file_path, created_at, updated_at
|
|
40
|
+
FROM vault
|
|
41
|
+
WHERE ${column} = ? AND COALESCE(updated_at, created_at) <= ?
|
|
42
|
+
ORDER BY COALESCE(updated_at, created_at) ASC`
|
|
43
|
+
)
|
|
44
|
+
.all(key, cutoff) as Array<{ id: string; file_path: string | null; [key: string]: unknown }>;
|
|
45
|
+
|
|
46
|
+
for (const row of rows) {
|
|
47
|
+
if (seen.has(row.id)) continue;
|
|
48
|
+
seen.add(row.id);
|
|
49
|
+
candidates.push({
|
|
50
|
+
...row,
|
|
51
|
+
reason: `${column}=${key}, archiveAfterDays=${rules.archiveAfterDays}`,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return candidates;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function removeFromIndex(ctx: LocalCtx, id: string): void {
|
|
60
|
+
const rowidRow = ctx.stmts.getRowid.get(id) as { rowid?: number } | undefined;
|
|
61
|
+
if (rowidRow?.rowid) {
|
|
62
|
+
try {
|
|
63
|
+
ctx.deleteVec(Number(rowidRow.rowid));
|
|
64
|
+
} catch {}
|
|
65
|
+
}
|
|
66
|
+
ctx.stmts.deleteEntry.run(id);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export async function archiveEntries(
|
|
70
|
+
ctx: LocalCtx
|
|
71
|
+
): Promise<{ archived: unknown[]; count: number }> {
|
|
72
|
+
const candidates = findArchiveCandidates(ctx) as Array<{
|
|
73
|
+
id: string;
|
|
74
|
+
file_path: string | null;
|
|
75
|
+
[key: string]: unknown;
|
|
76
|
+
}>;
|
|
77
|
+
if (candidates.length === 0) return { archived: candidates, count: 0 };
|
|
78
|
+
|
|
79
|
+
const vaultDir = ctx.config.vaultDir;
|
|
80
|
+
const archRoot = archiveDir(vaultDir);
|
|
81
|
+
let count = 0;
|
|
82
|
+
|
|
83
|
+
for (const entry of candidates) {
|
|
84
|
+
const filePath = entry.file_path;
|
|
85
|
+
if (!filePath) {
|
|
86
|
+
removeFromIndex(ctx, entry.id);
|
|
87
|
+
count++;
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const rel = relative(vaultDir, filePath);
|
|
92
|
+
if (rel.startsWith('..') || rel.startsWith('_archive')) continue;
|
|
93
|
+
|
|
94
|
+
const destPath = join(archRoot, rel);
|
|
95
|
+
const destDir = dirname(destPath);
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
mkdirSync(destDir, { recursive: true });
|
|
99
|
+
renameSync(filePath, destPath);
|
|
100
|
+
} catch (e) {
|
|
101
|
+
if ((e as NodeJS.ErrnoException).code === 'ENOENT' && !existsSync(filePath)) {
|
|
102
|
+
// File already gone — just remove from index
|
|
103
|
+
} else {
|
|
104
|
+
throw e;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
removeFromIndex(ctx, entry.id);
|
|
109
|
+
count++;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return { archived: candidates, count };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export async function restoreEntry(
|
|
116
|
+
ctx: LocalCtx,
|
|
117
|
+
entryId: string
|
|
118
|
+
): Promise<{ restored: boolean; reason?: string; filePath?: string; kind?: string; id?: string }> {
|
|
119
|
+
const vaultDir = ctx.config.vaultDir;
|
|
120
|
+
const archRoot = archiveDir(vaultDir);
|
|
121
|
+
|
|
122
|
+
if (!existsSync(archRoot)) {
|
|
123
|
+
return { restored: false, reason: 'no _archive directory' };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const mdFiles = walkDir(archRoot);
|
|
127
|
+
let targetFile: string | null = null;
|
|
128
|
+
|
|
129
|
+
for (const { filePath } of mdFiles) {
|
|
130
|
+
try {
|
|
131
|
+
const raw = readFileSync(filePath, 'utf-8');
|
|
132
|
+
const { meta } = parseFrontmatter(raw);
|
|
133
|
+
if (meta.id === entryId) {
|
|
134
|
+
targetFile = filePath;
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
137
|
+
} catch {
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (!targetFile) {
|
|
143
|
+
return { restored: false, reason: `entry ${entryId} not found in _archive` };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const rel = relative(archRoot, targetFile);
|
|
147
|
+
const destPath = join(vaultDir, rel);
|
|
148
|
+
const destDir = dirname(destPath);
|
|
149
|
+
|
|
150
|
+
if (existsSync(destPath)) {
|
|
151
|
+
return { restored: false, reason: `destination already exists: ${destPath}` };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
mkdirSync(destDir, { recursive: true });
|
|
155
|
+
renameSync(targetFile, destPath);
|
|
156
|
+
|
|
157
|
+
const raw = readFileSync(destPath, 'utf-8');
|
|
158
|
+
const { meta, body: rawBody } = parseFrontmatter(raw);
|
|
159
|
+
|
|
160
|
+
const relFromVault = relative(vaultDir, destPath);
|
|
161
|
+
const kindDir = relFromVault.split('/').filter(Boolean);
|
|
162
|
+
let kind =
|
|
163
|
+
(meta.kind as string | undefined) ||
|
|
164
|
+
(kindDir.length >= 2 ? kindDir[kindDir.length - 2] : kindDir[0]) ||
|
|
165
|
+
'note';
|
|
166
|
+
|
|
167
|
+
const parsed = parseEntryFromMarkdown(kind, rawBody, meta);
|
|
168
|
+
const category = categoryFor(kind);
|
|
169
|
+
|
|
170
|
+
await indexEntry(ctx, {
|
|
171
|
+
id: (meta.id as string | undefined) || entryId,
|
|
172
|
+
kind,
|
|
173
|
+
category,
|
|
174
|
+
title: parsed.title,
|
|
175
|
+
body: parsed.body,
|
|
176
|
+
meta: parsed.meta ?? undefined,
|
|
177
|
+
tags: Array.isArray(meta.tags) ? (meta.tags as string[]) : null,
|
|
178
|
+
source: (meta.source as string | undefined) || 'archive-restore',
|
|
179
|
+
filePath: destPath,
|
|
180
|
+
createdAt: (meta.created as string | undefined) || new Date().toISOString(),
|
|
181
|
+
identity_key: (meta.identity_key as string | undefined) || null,
|
|
182
|
+
expires_at: (meta.expires_at as string | undefined) || null,
|
|
183
|
+
tier: (meta.tier as string | undefined) || defaultTierFor(kind),
|
|
184
|
+
source_files: null,
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
return {
|
|
188
|
+
restored: true,
|
|
189
|
+
filePath: destPath,
|
|
190
|
+
kind,
|
|
191
|
+
id: (meta.id as string | undefined) || entryId,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export function countArchivedEntries(vaultDir: string): number {
|
|
196
|
+
const archRoot = archiveDir(vaultDir);
|
|
197
|
+
if (!existsSync(archRoot)) return 0;
|
|
198
|
+
try {
|
|
199
|
+
return walkDir(archRoot).length;
|
|
200
|
+
} catch {
|
|
201
|
+
return 0;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export function listArchivedEntries(vaultDir: string): Array<{
|
|
206
|
+
id: string | null;
|
|
207
|
+
kind: string;
|
|
208
|
+
title: string;
|
|
209
|
+
tags: unknown[];
|
|
210
|
+
created: string | null;
|
|
211
|
+
filePath: string;
|
|
212
|
+
}> {
|
|
213
|
+
const archRoot = archiveDir(vaultDir);
|
|
214
|
+
if (!existsSync(archRoot)) return [];
|
|
215
|
+
|
|
216
|
+
const entries: Array<{
|
|
217
|
+
id: string | null;
|
|
218
|
+
kind: string;
|
|
219
|
+
title: string;
|
|
220
|
+
tags: unknown[];
|
|
221
|
+
created: string | null;
|
|
222
|
+
filePath: string;
|
|
223
|
+
}> = [];
|
|
224
|
+
try {
|
|
225
|
+
const mdFiles = walkDir(archRoot);
|
|
226
|
+
for (const { filePath, relDir } of mdFiles) {
|
|
227
|
+
try {
|
|
228
|
+
const raw = readFileSync(filePath, 'utf-8');
|
|
229
|
+
const { meta, body } = parseFrontmatter(raw);
|
|
230
|
+
entries.push({
|
|
231
|
+
id: (meta.id as string | null) || null,
|
|
232
|
+
kind: relDir?.split('/').pop() || 'unknown',
|
|
233
|
+
title: (meta.title as string | undefined) || body.slice(0, 80),
|
|
234
|
+
tags: Array.isArray(meta.tags) ? meta.tags : [],
|
|
235
|
+
created: (meta.created as string | null) || null,
|
|
236
|
+
filePath,
|
|
237
|
+
});
|
|
238
|
+
} catch {
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
} catch {}
|
|
243
|
+
return entries;
|
|
244
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { DatabaseSync } from 'node:sqlite';
|
|
2
|
+
|
|
3
|
+
export function findHotTags(
|
|
4
|
+
db: DatabaseSync,
|
|
5
|
+
opts: { tagThreshold?: number; maxSnapshotAgeDays?: number } = {}
|
|
6
|
+
): Array<{ tag: string; entryCount: number; lastSnapshotAge: number | null }> {
|
|
7
|
+
const tagThreshold = opts.tagThreshold ?? 10;
|
|
8
|
+
const maxSnapshotAgeDays = opts.maxSnapshotAgeDays ?? 7;
|
|
9
|
+
const cutoff = new Date(Date.now() - maxSnapshotAgeDays * 86400000).toISOString();
|
|
10
|
+
|
|
11
|
+
const rows = db
|
|
12
|
+
.prepare(
|
|
13
|
+
`SELECT tags FROM vault
|
|
14
|
+
WHERE kind != 'brief'
|
|
15
|
+
AND superseded_by IS NULL
|
|
16
|
+
AND (expires_at IS NULL OR expires_at > datetime('now'))`
|
|
17
|
+
)
|
|
18
|
+
.all();
|
|
19
|
+
|
|
20
|
+
const tagCounts = new Map<string, number>();
|
|
21
|
+
for (const row of rows as Array<{ tags: string | null }>) {
|
|
22
|
+
if (!row.tags) continue;
|
|
23
|
+
try {
|
|
24
|
+
const tags = JSON.parse(row.tags);
|
|
25
|
+
for (const tag of tags) {
|
|
26
|
+
tagCounts.set(tag, (tagCounts.get(tag) || 0) + 1);
|
|
27
|
+
}
|
|
28
|
+
} catch {
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const results = [];
|
|
34
|
+
for (const [tag, count] of tagCounts) {
|
|
35
|
+
if (count < tagThreshold) continue;
|
|
36
|
+
|
|
37
|
+
const recentBrief = db
|
|
38
|
+
.prepare(
|
|
39
|
+
`SELECT created_at FROM vault
|
|
40
|
+
WHERE kind = 'brief'
|
|
41
|
+
AND tags LIKE ?
|
|
42
|
+
ORDER BY created_at DESC LIMIT 1`
|
|
43
|
+
)
|
|
44
|
+
.get(`%"${tag}"%`);
|
|
45
|
+
|
|
46
|
+
let lastSnapshotAge: number | null = null;
|
|
47
|
+
if (recentBrief) {
|
|
48
|
+
const brief = recentBrief as { created_at: string };
|
|
49
|
+
lastSnapshotAge = Math.round((Date.now() - new Date(brief.created_at).getTime()) / 86400000);
|
|
50
|
+
if (brief.created_at >= cutoff) continue;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
results.push({ tag, entryCount: count, lastSnapshotAge });
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return results.sort((a, b) => b.entryCount - a.entryCount);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function findColdEntries(
|
|
60
|
+
db: DatabaseSync,
|
|
61
|
+
opts: { maxAgeDays?: number; maxHitCount?: number } = {}
|
|
62
|
+
): string[] {
|
|
63
|
+
const maxAgeDays = opts.maxAgeDays ?? 90;
|
|
64
|
+
const maxHitCount = opts.maxHitCount ?? 0;
|
|
65
|
+
const cutoff = new Date(Date.now() - maxAgeDays * 86400000).toISOString();
|
|
66
|
+
|
|
67
|
+
const rows = db
|
|
68
|
+
.prepare(
|
|
69
|
+
`SELECT id FROM vault
|
|
70
|
+
WHERE created_at < ?
|
|
71
|
+
AND superseded_by IS NULL
|
|
72
|
+
AND COALESCE(hit_count, 0) <= ?
|
|
73
|
+
AND (expires_at IS NULL OR expires_at > datetime('now'))`
|
|
74
|
+
)
|
|
75
|
+
.all(cutoff, maxHitCount);
|
|
76
|
+
|
|
77
|
+
return (rows as Array<{ id: string }>).map((r) => r.id);
|
|
78
|
+
}
|
|
@@ -5,34 +5,34 @@ 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
|
-
export function errorLogPath(dataDir) {
|
|
14
|
-
return join(dataDir,
|
|
13
|
+
export function errorLogPath(dataDir: string): string {
|
|
14
|
+
return join(dataDir, 'error.log');
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
export function appendErrorLog(dataDir, entry) {
|
|
17
|
+
export function appendErrorLog(dataDir: string, entry: Record<string, unknown>): void {
|
|
18
18
|
try {
|
|
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
|
}
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
export function errorLogCount(dataDir) {
|
|
30
|
+
export function errorLogCount(dataDir: string): number {
|
|
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.ts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { join, dirname } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import type { VaultConfig } from '@context-vault/core/types';
|
|
5
|
+
import type { ToolResult } from './types.js';
|
|
6
|
+
|
|
7
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
const pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf-8'));
|
|
9
|
+
|
|
10
|
+
export function ok(text: string): ToolResult {
|
|
11
|
+
return { content: [{ type: 'text', text }] };
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function err(
|
|
15
|
+
text: string,
|
|
16
|
+
code = 'UNKNOWN',
|
|
17
|
+
meta: Record<string, unknown> = {}
|
|
18
|
+
): ToolResult {
|
|
19
|
+
return {
|
|
20
|
+
content: [{ type: 'text', text }],
|
|
21
|
+
isError: true,
|
|
22
|
+
code,
|
|
23
|
+
_meta: {
|
|
24
|
+
cv_version: pkg.version,
|
|
25
|
+
node_version: process.version,
|
|
26
|
+
platform: process.platform,
|
|
27
|
+
arch: process.arch,
|
|
28
|
+
...meta,
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function errWithHint(text: string, code: string, hint?: string): ToolResult {
|
|
34
|
+
const prompt = hint
|
|
35
|
+
? `\n\n**Debug with AI:** Paste this into Claude Code or your AI assistant:\n> "${hint}"`
|
|
36
|
+
: '';
|
|
37
|
+
return err(text + prompt, code);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function ensureVaultExists(config: VaultConfig): ToolResult | null {
|
|
41
|
+
if (!config.vaultDirExists) {
|
|
42
|
+
return errWithHint(
|
|
43
|
+
`Vault directory not found: ${config.vaultDir}. Run context-status for diagnostics.`,
|
|
44
|
+
'VAULT_NOT_FOUND',
|
|
45
|
+
"My context-vault can't find the vault directory. Run `context-vault doctor` and help me fix it."
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function ensureValidKind(kind: string): ToolResult | null {
|
|
52
|
+
if (!/^[a-z][a-z0-9_-]*$/.test(kind)) {
|
|
53
|
+
return err(
|
|
54
|
+
"Required: kind (lowercase alphanumeric, e.g. 'insight', 'reference')",
|
|
55
|
+
'INVALID_KIND'
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export { pkg };
|
|
@@ -1,25 +1,27 @@
|
|
|
1
|
-
|
|
1
|
+
import type { DatabaseSync } from 'node:sqlite';
|
|
2
|
+
|
|
3
|
+
export function parseRelatedTo(raw: unknown): string[] {
|
|
2
4
|
if (!raw) return [];
|
|
3
5
|
try {
|
|
4
|
-
const parsed = JSON.parse(raw);
|
|
6
|
+
const parsed = JSON.parse(raw as string);
|
|
5
7
|
if (!Array.isArray(parsed)) return [];
|
|
6
|
-
return parsed.filter((id) => typeof id ===
|
|
8
|
+
return parsed.filter((id) => typeof id === 'string' && id.trim());
|
|
7
9
|
} catch {
|
|
8
10
|
return [];
|
|
9
11
|
}
|
|
10
12
|
}
|
|
11
13
|
|
|
12
|
-
export function resolveLinks(db, ids) {
|
|
14
|
+
export function resolveLinks(db: DatabaseSync, ids: string[]): any[] {
|
|
13
15
|
if (!ids.length) return [];
|
|
14
16
|
const unique = [...new Set(ids)];
|
|
15
|
-
const placeholders = unique.map(() =>
|
|
17
|
+
const placeholders = unique.map(() => '?').join(',');
|
|
16
18
|
try {
|
|
17
19
|
return db
|
|
18
20
|
.prepare(
|
|
19
21
|
`SELECT * FROM vault
|
|
20
22
|
WHERE id IN (${placeholders})
|
|
21
23
|
AND (expires_at IS NULL OR expires_at > datetime('now'))
|
|
22
|
-
AND superseded_by IS NULL
|
|
24
|
+
AND superseded_by IS NULL`
|
|
23
25
|
)
|
|
24
26
|
.all(...unique);
|
|
25
27
|
} catch {
|
|
@@ -27,7 +29,7 @@ export function resolveLinks(db, ids) {
|
|
|
27
29
|
}
|
|
28
30
|
}
|
|
29
31
|
|
|
30
|
-
export function resolveBacklinks(db, entryId) {
|
|
32
|
+
export function resolveBacklinks(db: DatabaseSync, entryId: string): any[] {
|
|
31
33
|
if (!entryId) return [];
|
|
32
34
|
const likePattern = `%"${entryId}"%`;
|
|
33
35
|
try {
|
|
@@ -36,7 +38,7 @@ export function resolveBacklinks(db, entryId) {
|
|
|
36
38
|
`SELECT * FROM vault
|
|
37
39
|
WHERE related_to LIKE ?
|
|
38
40
|
AND (expires_at IS NULL OR expires_at > datetime('now'))
|
|
39
|
-
AND superseded_by IS NULL
|
|
41
|
+
AND superseded_by IS NULL`
|
|
40
42
|
)
|
|
41
43
|
.all(likePattern);
|
|
42
44
|
} catch {
|
|
@@ -44,22 +46,23 @@ export function resolveBacklinks(db, entryId) {
|
|
|
44
46
|
}
|
|
45
47
|
}
|
|
46
48
|
|
|
47
|
-
export function collectLinkedEntries(
|
|
49
|
+
export function collectLinkedEntries(
|
|
50
|
+
db: DatabaseSync,
|
|
51
|
+
primaryEntries: Array<{ id: string; related_to?: string | null }>
|
|
52
|
+
): { forward: any[]; backward: any[] } {
|
|
48
53
|
const primaryIds = new Set(primaryEntries.map((e) => e.id));
|
|
49
54
|
|
|
50
|
-
const forwardIds = [];
|
|
55
|
+
const forwardIds: string[] = [];
|
|
51
56
|
for (const entry of primaryEntries) {
|
|
52
57
|
const ids = parseRelatedTo(entry.related_to);
|
|
53
58
|
for (const id of ids) {
|
|
54
59
|
if (!primaryIds.has(id)) forwardIds.push(id);
|
|
55
60
|
}
|
|
56
61
|
}
|
|
57
|
-
const forwardEntries = resolveLinks(db, forwardIds).filter(
|
|
58
|
-
(e) => !primaryIds.has(e.id),
|
|
59
|
-
);
|
|
62
|
+
const forwardEntries = resolveLinks(db, forwardIds).filter((e) => !primaryIds.has(e.id));
|
|
60
63
|
|
|
61
|
-
const backwardSeen = new Set();
|
|
62
|
-
const backwardEntries = [];
|
|
64
|
+
const backwardSeen = new Set<string>();
|
|
65
|
+
const backwardEntries: any[] = [];
|
|
63
66
|
for (const entry of primaryEntries) {
|
|
64
67
|
const backlinks = resolveBacklinks(db, entry.id);
|
|
65
68
|
for (const bl of backlinks) {
|
|
@@ -73,13 +76,12 @@ export function collectLinkedEntries(db, primaryEntries) {
|
|
|
73
76
|
return { forward: forwardEntries, backward: backwardEntries };
|
|
74
77
|
}
|
|
75
78
|
|
|
76
|
-
export function validateRelatedTo(relatedTo) {
|
|
79
|
+
export function validateRelatedTo(relatedTo: unknown): string | null {
|
|
77
80
|
if (relatedTo === undefined || relatedTo === null) return null;
|
|
78
|
-
if (!Array.isArray(relatedTo))
|
|
79
|
-
return "related_to must be an array of entry IDs";
|
|
81
|
+
if (!Array.isArray(relatedTo)) return 'related_to must be an array of entry IDs';
|
|
80
82
|
for (const id of relatedTo) {
|
|
81
|
-
if (typeof id !==
|
|
82
|
-
return
|
|
83
|
+
if (typeof id !== 'string' || !id.trim()) {
|
|
84
|
+
return 'each related_to entry must be a non-empty string ID';
|
|
83
85
|
}
|
|
84
86
|
if (id.length > 32) {
|
|
85
87
|
return `related_to ID too long (max 32 chars): "${id.slice(0, 32)}..."`;
|