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
|
@@ -1,14 +1,15 @@
|
|
|
1
|
-
import { z } from
|
|
2
|
-
import { createHash } from
|
|
3
|
-
import { readFileSync, existsSync } from
|
|
4
|
-
import { resolve } from
|
|
5
|
-
import { hybridSearch } from
|
|
6
|
-
import { categoryFor } from
|
|
7
|
-
import { normalizeKind } from
|
|
8
|
-
import { resolveTemporalParams } from
|
|
9
|
-
import { collectLinkedEntries } from
|
|
10
|
-
import { ok, err, errWithHint } from
|
|
11
|
-
import { isEmbedAvailable } from
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
4
|
+
import { resolve } from 'node:path';
|
|
5
|
+
import { hybridSearch } from '@context-vault/core/search';
|
|
6
|
+
import { categoryFor } from '@context-vault/core/categories';
|
|
7
|
+
import { normalizeKind } from '@context-vault/core/files';
|
|
8
|
+
import { resolveTemporalParams } from '../temporal.js';
|
|
9
|
+
import { collectLinkedEntries } from '../linking.js';
|
|
10
|
+
import { ok, err, errWithHint } from '../helpers.js';
|
|
11
|
+
import { isEmbedAvailable } from '@context-vault/core/embed';
|
|
12
|
+
import type { LocalCtx, SharedCtx, ToolResult } from '../types.js';
|
|
12
13
|
|
|
13
14
|
const STALE_DUPLICATE_DAYS = 7;
|
|
14
15
|
const DEFAULT_PIVOT_COUNT = 2;
|
|
@@ -21,22 +22,19 @@ const BRIEF_SCORE_BOOST = 0.05;
|
|
|
21
22
|
* Truncate a body string to ~SKELETON_BODY_CHARS, breaking at sentence or
|
|
22
23
|
* word boundary. Returns the truncated string with "..." appended.
|
|
23
24
|
*/
|
|
24
|
-
export function skeletonBody(body) {
|
|
25
|
-
if (!body) return
|
|
25
|
+
export function skeletonBody(body: string | null | undefined): string {
|
|
26
|
+
if (!body) return '';
|
|
26
27
|
if (body.length <= SKELETON_BODY_CHARS) return body;
|
|
27
28
|
const slice = body.slice(0, SKELETON_BODY_CHARS);
|
|
28
|
-
const sentenceEnd = Math.max(
|
|
29
|
-
slice.lastIndexOf(". "),
|
|
30
|
-
slice.lastIndexOf(".\n"),
|
|
31
|
-
);
|
|
29
|
+
const sentenceEnd = Math.max(slice.lastIndexOf('. '), slice.lastIndexOf('.\n'));
|
|
32
30
|
if (sentenceEnd > SKELETON_BODY_CHARS * 0.4) {
|
|
33
|
-
return slice.slice(0, sentenceEnd + 1) +
|
|
31
|
+
return slice.slice(0, sentenceEnd + 1) + '...';
|
|
34
32
|
}
|
|
35
|
-
const wordEnd = slice.lastIndexOf(
|
|
33
|
+
const wordEnd = slice.lastIndexOf(' ');
|
|
36
34
|
if (wordEnd > SKELETON_BODY_CHARS * 0.4) {
|
|
37
|
-
return slice.slice(0, wordEnd) +
|
|
35
|
+
return slice.slice(0, wordEnd) + '...';
|
|
38
36
|
}
|
|
39
|
-
return slice +
|
|
37
|
+
return slice + '...';
|
|
40
38
|
}
|
|
41
39
|
|
|
42
40
|
/**
|
|
@@ -53,10 +51,13 @@ export function skeletonBody(body) {
|
|
|
53
51
|
* rows already fetched from the DB.
|
|
54
52
|
*
|
|
55
53
|
* @param {Array} entries - Result rows (as returned by hybridSearch / filter-only mode)
|
|
56
|
-
* @param {import('
|
|
54
|
+
* @param {import('@context-vault/core/types').BaseCtx} _ctx
|
|
57
55
|
* @returns {Array<{entry_a_id: string, entry_b_id: string, reason: string, recommendation: string}>}
|
|
58
56
|
*/
|
|
59
|
-
export function detectConflicts(
|
|
57
|
+
export function detectConflicts(
|
|
58
|
+
entries: any[],
|
|
59
|
+
_ctx: any
|
|
60
|
+
): Array<{ entry_a_id: string; entry_b_id: string; reason: string; recommendation: string }> {
|
|
60
61
|
const conflicts = [];
|
|
61
62
|
const idSet = new Set(entries.map((e) => e.id));
|
|
62
63
|
|
|
@@ -65,15 +66,13 @@ export function detectConflicts(entries, _ctx) {
|
|
|
65
66
|
conflicts.push({
|
|
66
67
|
entry_a_id: entry.id,
|
|
67
68
|
entry_b_id: entry.superseded_by,
|
|
68
|
-
reason:
|
|
69
|
+
reason: 'superseded',
|
|
69
70
|
recommendation: `Discard \`${entry.id}\` — it has been explicitly superseded by \`${entry.superseded_by}\`.`,
|
|
70
71
|
});
|
|
71
72
|
}
|
|
72
73
|
}
|
|
73
74
|
|
|
74
|
-
const supersededConflictPairs = new Set(
|
|
75
|
-
conflicts.map((c) => `${c.entry_a_id}|${c.entry_b_id}`),
|
|
76
|
-
);
|
|
75
|
+
const supersededConflictPairs = new Set(conflicts.map((c) => `${c.entry_a_id}|${c.entry_b_id}`));
|
|
77
76
|
|
|
78
77
|
for (let i = 0; i < entries.length; i++) {
|
|
79
78
|
for (let j = i + 1; j < entries.length; j++) {
|
|
@@ -95,21 +94,21 @@ export function detectConflicts(entries, _ctx) {
|
|
|
95
94
|
if (!tagsA.length || !tagsB.length) continue;
|
|
96
95
|
|
|
97
96
|
const tagsSetA = new Set(tagsA);
|
|
98
|
-
const sharedTag = tagsB.some((t) => tagsSetA.has(t));
|
|
97
|
+
const sharedTag = tagsB.some((t: any) => tagsSetA.has(t));
|
|
99
98
|
if (!sharedTag) continue;
|
|
100
99
|
|
|
101
100
|
const dateA = new Date(a.updated_at || a.created_at);
|
|
102
101
|
const dateB = new Date(b.updated_at || b.created_at);
|
|
103
102
|
if (isNaN(dateA.getTime()) || isNaN(dateB.getTime())) continue;
|
|
104
103
|
|
|
105
|
-
const diffDays = Math.abs(dateA - dateB) / 86400000;
|
|
104
|
+
const diffDays = Math.abs(dateA.getTime() - dateB.getTime()) / 86400000;
|
|
106
105
|
if (diffDays <= STALE_DUPLICATE_DAYS) continue;
|
|
107
106
|
|
|
108
107
|
const [older, newer] = dateA < dateB ? [a, b] : [b, a];
|
|
109
108
|
conflicts.push({
|
|
110
109
|
entry_a_id: older.id,
|
|
111
110
|
entry_b_id: newer.id,
|
|
112
|
-
reason:
|
|
111
|
+
reason: 'stale_duplicate',
|
|
113
112
|
recommendation: `Verify \`${older.id}\` is still accurate — it shares kind "${older.kind}" and tags with \`${newer.id}\` but was last updated ${Math.round(diffDays)} days earlier.`,
|
|
114
113
|
});
|
|
115
114
|
}
|
|
@@ -134,13 +133,17 @@ export function detectConflicts(entries, _ctx) {
|
|
|
134
133
|
* @param {{ tagThreshold?: number, maxAgeDays?: number }} opts - Configurable thresholds
|
|
135
134
|
* @returns {Array<{tag: string, entry_count: number, last_snapshot_age_days: number|null}>}
|
|
136
135
|
*/
|
|
137
|
-
export function detectConsolidationHints(
|
|
136
|
+
export function detectConsolidationHints(
|
|
137
|
+
entries: any[],
|
|
138
|
+
db: any,
|
|
139
|
+
opts: { tagThreshold?: number; maxAgeDays?: number } = {}
|
|
140
|
+
): Array<{ tag: string; entry_count: number; last_snapshot_age_days: number | null }> {
|
|
138
141
|
const tagThreshold = opts.tagThreshold ?? CONSOLIDATION_TAG_THRESHOLD;
|
|
139
142
|
const maxAgeDays = opts.maxAgeDays ?? CONSOLIDATION_SNAPSHOT_MAX_AGE_DAYS;
|
|
140
143
|
|
|
141
|
-
const candidateTags = new Set();
|
|
144
|
+
const candidateTags = new Set<string>();
|
|
142
145
|
for (const entry of entries) {
|
|
143
|
-
if (entry.kind ===
|
|
146
|
+
if (entry.kind === 'brief') continue;
|
|
144
147
|
const entryTags = entry.tags ? JSON.parse(entry.tags) : [];
|
|
145
148
|
for (const tag of entryTags) candidateTags.add(tag);
|
|
146
149
|
}
|
|
@@ -153,15 +156,11 @@ export function detectConsolidationHints(entries, db, opts = {}) {
|
|
|
153
156
|
for (const tag of candidateTags) {
|
|
154
157
|
let vaultCount = 0;
|
|
155
158
|
try {
|
|
156
|
-
// When userId is defined (hosted mode), scope to that user.
|
|
157
|
-
// When userId is undefined (local mode), no user scoping — column may not exist.
|
|
158
|
-
const userClause = "";
|
|
159
|
-
const countParams = false ? [`%"${tag}"%`] : [`%"${tag}"%`];
|
|
160
159
|
const countRow = db
|
|
161
160
|
.prepare(
|
|
162
|
-
`SELECT COUNT(*) as c FROM vault WHERE kind != 'brief' AND tags LIKE
|
|
161
|
+
`SELECT COUNT(*) as c FROM vault WHERE kind != 'brief' AND tags LIKE ? AND (expires_at IS NULL OR expires_at > datetime('now')) AND superseded_by IS NULL`
|
|
163
162
|
)
|
|
164
|
-
.get(
|
|
163
|
+
.get(`%"${tag}"%`);
|
|
165
164
|
vaultCount = countRow?.c ?? 0;
|
|
166
165
|
} catch {
|
|
167
166
|
continue;
|
|
@@ -171,17 +170,15 @@ export function detectConsolidationHints(entries, db, opts = {}) {
|
|
|
171
170
|
|
|
172
171
|
let lastSnapshotAgeDays = null;
|
|
173
172
|
try {
|
|
174
|
-
const userClause = "";
|
|
175
|
-
const params = false ? [`%"${tag}"%`] : [`%"${tag}"%`];
|
|
176
173
|
const recentBrief = db
|
|
177
174
|
.prepare(
|
|
178
|
-
`SELECT created_at FROM vault WHERE kind = 'brief' AND tags LIKE
|
|
175
|
+
`SELECT created_at FROM vault WHERE kind = 'brief' AND tags LIKE ? ORDER BY created_at DESC LIMIT 1`
|
|
179
176
|
)
|
|
180
|
-
.get(
|
|
177
|
+
.get(`%"${tag}"%`);
|
|
181
178
|
|
|
182
179
|
if (recentBrief) {
|
|
183
180
|
lastSnapshotAgeDays = Math.round(
|
|
184
|
-
(Date.now() - new Date(recentBrief.created_at).getTime()) / 86400000
|
|
181
|
+
(Date.now() - new Date(recentBrief.created_at).getTime()) / 86400000
|
|
185
182
|
);
|
|
186
183
|
if (recentBrief.created_at >= cutoff) continue;
|
|
187
184
|
}
|
|
@@ -207,7 +204,7 @@ export function detectConsolidationHints(entries, db, opts = {}) {
|
|
|
207
204
|
* @param {object} entry - DB row with source_files JSON column
|
|
208
205
|
* @returns {{ stale: boolean, stale_reason: string } | null}
|
|
209
206
|
*/
|
|
210
|
-
function checkStaleness(entry) {
|
|
207
|
+
function checkStaleness(entry: any): { stale: boolean; stale_reason: string } | null {
|
|
211
208
|
if (!entry.source_files) return null;
|
|
212
209
|
let sourceFiles;
|
|
213
210
|
try {
|
|
@@ -219,18 +216,16 @@ function checkStaleness(entry) {
|
|
|
219
216
|
|
|
220
217
|
for (const sf of sourceFiles) {
|
|
221
218
|
try {
|
|
222
|
-
const absPath = sf.path.startsWith(
|
|
223
|
-
? sf.path
|
|
224
|
-
: resolve(process.cwd(), sf.path);
|
|
219
|
+
const absPath = sf.path.startsWith('/') ? sf.path : resolve(process.cwd(), sf.path);
|
|
225
220
|
if (!existsSync(absPath)) {
|
|
226
|
-
return { stale: true, stale_reason:
|
|
221
|
+
return { stale: true, stale_reason: 'source file not found' };
|
|
227
222
|
}
|
|
228
223
|
const contents = readFileSync(absPath);
|
|
229
|
-
const currentHash = createHash(
|
|
224
|
+
const currentHash = createHash('sha256').update(contents).digest('hex');
|
|
230
225
|
if (currentHash !== sf.hash) {
|
|
231
226
|
return {
|
|
232
227
|
stale: true,
|
|
233
|
-
stale_reason:
|
|
228
|
+
stale_reason: 'source file modified since observation',
|
|
234
229
|
};
|
|
235
230
|
}
|
|
236
231
|
} catch {
|
|
@@ -240,115 +235,98 @@ function checkStaleness(entry) {
|
|
|
240
235
|
return null;
|
|
241
236
|
}
|
|
242
237
|
|
|
243
|
-
export const name =
|
|
238
|
+
export const name = 'get_context';
|
|
244
239
|
|
|
245
240
|
export const description =
|
|
246
|
-
|
|
241
|
+
'Search your knowledge vault. Returns entries ranked by relevance using hybrid full-text + semantic search. Use this to find insights, decisions, patterns, or any saved context. Each result includes an `id` you can use with save_context or delete_context.';
|
|
247
242
|
|
|
248
243
|
export const inputSchema = {
|
|
249
244
|
query: z
|
|
250
245
|
.string()
|
|
251
246
|
.optional()
|
|
252
247
|
.describe(
|
|
253
|
-
|
|
248
|
+
'Search query (natural language or keywords). Optional if filters (tags, kind, category) are provided.'
|
|
254
249
|
),
|
|
255
|
-
kind: z
|
|
256
|
-
|
|
257
|
-
.optional()
|
|
258
|
-
.describe("Filter by kind (e.g. 'insight', 'decision', 'pattern')"),
|
|
259
|
-
category: z
|
|
260
|
-
.enum(["knowledge", "entity", "event"])
|
|
261
|
-
.optional()
|
|
262
|
-
.describe("Filter by category"),
|
|
250
|
+
kind: z.string().optional().describe("Filter by kind (e.g. 'insight', 'decision', 'pattern')"),
|
|
251
|
+
category: z.enum(['knowledge', 'entity', 'event']).optional().describe('Filter by category'),
|
|
263
252
|
identity_key: z
|
|
264
253
|
.string()
|
|
265
254
|
.optional()
|
|
266
|
-
.describe(
|
|
255
|
+
.describe('For entity lookup: exact match on identity key. Requires kind.'),
|
|
267
256
|
tags: z
|
|
268
257
|
.array(z.string())
|
|
269
258
|
.optional()
|
|
270
259
|
.describe(
|
|
271
|
-
"Filter by tags (entries must match at least one). Use 'bucket:' prefixed tags for project-scoped retrieval (e.g., ['bucket:autohub'])."
|
|
260
|
+
"Filter by tags (entries must match at least one). Use 'bucket:' prefixed tags for project-scoped retrieval (e.g., ['bucket:autohub'])."
|
|
272
261
|
),
|
|
273
262
|
buckets: z
|
|
274
263
|
.array(z.string())
|
|
275
264
|
.optional()
|
|
276
265
|
.describe(
|
|
277
|
-
"Filter by project-scoped buckets. Each name expands to a 'bucket:<name>' tag. Composes with 'tags' via OR (entries matching any tag or any bucket are included)."
|
|
266
|
+
"Filter by project-scoped buckets. Each name expands to a 'bucket:<name>' tag. Composes with 'tags' via OR (entries matching any tag or any bucket are included)."
|
|
278
267
|
),
|
|
279
268
|
since: z
|
|
280
269
|
.string()
|
|
281
270
|
.optional()
|
|
282
271
|
.describe(
|
|
283
|
-
"Return entries created after this date. Accepts ISO date strings (e.g. '2025-01-01') or natural shortcuts: 'today', 'yesterday', 'this_week', 'this_month', 'last_3_days', 'last_2_weeks', 'last_1_month'. Spaces and underscores are interchangeable."
|
|
272
|
+
"Return entries created after this date. Accepts ISO date strings (e.g. '2025-01-01') or natural shortcuts: 'today', 'yesterday', 'this_week', 'this_month', 'last_3_days', 'last_2_weeks', 'last_1_month'. Spaces and underscores are interchangeable."
|
|
284
273
|
),
|
|
285
274
|
until: z
|
|
286
275
|
.string()
|
|
287
276
|
.optional()
|
|
288
277
|
.describe(
|
|
289
|
-
"Return entries created before this date. Accepts ISO date strings or the same natural shortcuts as `since`. When `since` is 'yesterday' and `until` is omitted, `until` is automatically set to the end of yesterday."
|
|
278
|
+
"Return entries created before this date. Accepts ISO date strings or the same natural shortcuts as `since`. When `since` is 'yesterday' and `until` is omitted, `until` is automatically set to the end of yesterday."
|
|
290
279
|
),
|
|
291
|
-
limit: z
|
|
292
|
-
.number()
|
|
293
|
-
.max(500)
|
|
294
|
-
.optional()
|
|
295
|
-
.describe("Max results to return (default 10)"),
|
|
280
|
+
limit: z.number().max(500).optional().describe('Max results to return (default 10)'),
|
|
296
281
|
include_superseded: z
|
|
297
282
|
.boolean()
|
|
298
283
|
.optional()
|
|
299
|
-
.describe(
|
|
300
|
-
"If true, include entries that have been superseded by newer ones. Default: false.",
|
|
301
|
-
),
|
|
284
|
+
.describe('If true, include entries that have been superseded by newer ones. Default: false.'),
|
|
302
285
|
detect_conflicts: z
|
|
303
286
|
.boolean()
|
|
304
287
|
.optional()
|
|
305
288
|
.describe(
|
|
306
|
-
|
|
289
|
+
'If true, compare results for contradicting entries and append a conflicts array. Flags superseded entries still in results and stale duplicates (same kind+tags, updated_at >7 days apart). No LLM calls — pure DB logic.'
|
|
307
290
|
),
|
|
308
291
|
max_tokens: z
|
|
309
292
|
.number()
|
|
310
293
|
.max(100000)
|
|
311
294
|
.optional()
|
|
312
295
|
.describe(
|
|
313
|
-
|
|
296
|
+
'Limit output to entries that fit within this token budget (rough estimate: 1 token ≈ 4 chars). Entries are packed greedily by relevance rank. At least 1 result is always returned. Response metadata includes tokens_used and tokens_budget.'
|
|
314
297
|
),
|
|
315
298
|
pivot_count: z
|
|
316
299
|
.number()
|
|
317
300
|
.optional()
|
|
318
301
|
.describe(
|
|
319
|
-
|
|
302
|
+
'Skeleton mode: top pivot_count entries by relevance are returned with full body. Remaining entries are returned as skeletons (title + tags + first ~100 chars of body). Default: 2. Set to 0 to skeleton all results, or a high number to disable.'
|
|
320
303
|
),
|
|
321
304
|
include_ephemeral: z
|
|
322
305
|
.boolean()
|
|
323
306
|
.optional()
|
|
324
307
|
.describe(
|
|
325
|
-
|
|
308
|
+
'If true, include ephemeral tier entries in results. Default: false — only working and durable tiers are returned.'
|
|
326
309
|
),
|
|
327
310
|
include_events: z
|
|
328
311
|
.boolean()
|
|
329
312
|
.optional()
|
|
330
313
|
.describe(
|
|
331
|
-
|
|
314
|
+
'If true, include event category entries in semantic search results. Default: false — events are excluded from query-based search but remain accessible via category/tag filters. Deprecated: prefer scope parameter.'
|
|
332
315
|
),
|
|
333
316
|
scope: z
|
|
334
|
-
.enum([
|
|
317
|
+
.enum(['hot', 'events', 'all'])
|
|
335
318
|
.optional()
|
|
336
319
|
.describe(
|
|
337
|
-
"Index scope: 'hot' (default) — knowledge + entity entries only; 'events' — event entries only (cold index); 'all' — entire vault including events. Overrides include_events when set."
|
|
320
|
+
"Index scope: 'hot' (default) — knowledge + entity entries only; 'events' — event entries only (cold index); 'all' — entire vault including events. Overrides include_events when set."
|
|
338
321
|
),
|
|
339
322
|
follow_links: z
|
|
340
323
|
.boolean()
|
|
341
324
|
.optional()
|
|
342
325
|
.describe(
|
|
343
|
-
|
|
326
|
+
'If true, follow related_to links from result entries and include linked entries (forward links) and backlinks (entries that reference the results). Enables bidirectional graph traversal.'
|
|
344
327
|
),
|
|
345
328
|
};
|
|
346
329
|
|
|
347
|
-
/**
|
|
348
|
-
* @param {object} args
|
|
349
|
-
* @param {import('../types.js').BaseCtx & Partial<import('../types.js').HostedCtxExtensions>} ctx
|
|
350
|
-
* @param {import('../types.js').ToolShared} shared
|
|
351
|
-
*/
|
|
352
330
|
export async function handler(
|
|
353
331
|
{
|
|
354
332
|
query,
|
|
@@ -368,10 +346,10 @@ export async function handler(
|
|
|
368
346
|
include_events,
|
|
369
347
|
scope,
|
|
370
348
|
follow_links,
|
|
371
|
-
},
|
|
372
|
-
ctx,
|
|
373
|
-
{ ensureIndexed, reindexFailed }
|
|
374
|
-
) {
|
|
349
|
+
}: Record<string, any>,
|
|
350
|
+
ctx: LocalCtx,
|
|
351
|
+
{ ensureIndexed, reindexFailed }: SharedCtx
|
|
352
|
+
): Promise<ToolResult> {
|
|
375
353
|
const { config } = ctx;
|
|
376
354
|
|
|
377
355
|
// Resolve natural-language temporal shortcuts → ISO date strings
|
|
@@ -387,28 +365,21 @@ export async function handler(
|
|
|
387
365
|
// scope "all": no category restriction — full vault
|
|
388
366
|
let effectiveScope = scope;
|
|
389
367
|
if (!effectiveScope) {
|
|
390
|
-
effectiveScope = include_events ?
|
|
368
|
+
effectiveScope = include_events ? 'all' : 'hot';
|
|
391
369
|
}
|
|
392
370
|
|
|
393
371
|
// Scope "events" forces category to "event" unless caller already set a narrower category
|
|
394
|
-
const scopedCategory =
|
|
395
|
-
|
|
396
|
-
const shouldExcludeEvents =
|
|
397
|
-
hasQuery && effectiveScope === "hot" && !scopedCategory;
|
|
372
|
+
const scopedCategory = !category && effectiveScope === 'events' ? 'event' : category;
|
|
373
|
+
const shouldExcludeEvents = hasQuery && effectiveScope === 'hot' && !scopedCategory;
|
|
398
374
|
// Expand buckets to bucket: prefixed tags and merge with explicit tags
|
|
399
|
-
const bucketTags = buckets?.length ? buckets.map((b) => `bucket:${b}`) : [];
|
|
375
|
+
const bucketTags = buckets?.length ? buckets.map((b: string) => `bucket:${b}`) : [];
|
|
400
376
|
const effectiveTags = [...(tags ?? []), ...bucketTags];
|
|
401
377
|
const hasFilters =
|
|
402
|
-
kind ||
|
|
403
|
-
scopedCategory ||
|
|
404
|
-
effectiveTags.length ||
|
|
405
|
-
since ||
|
|
406
|
-
until ||
|
|
407
|
-
identity_key;
|
|
378
|
+
kind || scopedCategory || effectiveTags.length || since || until || identity_key;
|
|
408
379
|
if (!hasQuery && !hasFilters)
|
|
409
380
|
return err(
|
|
410
|
-
|
|
411
|
-
|
|
381
|
+
'Required: query or at least one filter (kind, category, tags, since, until, identity_key)',
|
|
382
|
+
'INVALID_INPUT'
|
|
412
383
|
);
|
|
413
384
|
await ensureIndexed();
|
|
414
385
|
|
|
@@ -416,34 +387,32 @@ export async function handler(
|
|
|
416
387
|
|
|
417
388
|
// Gap 1: Entity exact-match by identity_key
|
|
418
389
|
if (identity_key) {
|
|
419
|
-
if (!kindFilter)
|
|
420
|
-
|
|
421
|
-
const match = ctx.stmts.getByIdentityKey.get(kindFilter, identity_key);
|
|
390
|
+
if (!kindFilter) return err('identity_key requires kind to be specified', 'INVALID_INPUT');
|
|
391
|
+
const match = ctx.stmts.getByIdentityKey.get(kindFilter, identity_key) as any;
|
|
422
392
|
if (match) {
|
|
423
393
|
const entryTags = match.tags ? JSON.parse(match.tags) : [];
|
|
424
|
-
const tagStr = entryTags.length ? entryTags.join(
|
|
394
|
+
const tagStr = entryTags.length ? entryTags.join(', ') : 'none';
|
|
425
395
|
const relPath =
|
|
426
396
|
match.file_path && config.vaultDir
|
|
427
|
-
? match.file_path.replace(config.vaultDir +
|
|
428
|
-
: match.file_path ||
|
|
397
|
+
? match.file_path.replace(config.vaultDir + '/', '')
|
|
398
|
+
: match.file_path || 'n/a';
|
|
429
399
|
const lines = [
|
|
430
400
|
`## Entity Match (exact)\n`,
|
|
431
|
-
`### ${match.title ||
|
|
401
|
+
`### ${match.title || '(untitled)'} [${match.kind}/${match.category}]`,
|
|
432
402
|
`1.000 · ${tagStr} · ${relPath} · id: \`${match.id}\``,
|
|
433
|
-
match.body?.slice(0, 300) + (match.body?.length > 300 ?
|
|
403
|
+
match.body?.slice(0, 300) + (match.body?.length > 300 ? '...' : ''),
|
|
434
404
|
];
|
|
435
|
-
return ok(lines.join(
|
|
405
|
+
return ok(lines.join('\n'));
|
|
436
406
|
}
|
|
437
407
|
// Fall through to semantic search as fallback
|
|
438
408
|
}
|
|
439
409
|
|
|
440
410
|
// Gap 2: Event default time-window
|
|
441
|
-
const effectiveCategory =
|
|
442
|
-
scopedCategory || (kindFilter ? categoryFor(kindFilter) : null);
|
|
411
|
+
const effectiveCategory = scopedCategory || (kindFilter ? categoryFor(kindFilter) : null);
|
|
443
412
|
let effectiveSince = since || null;
|
|
444
413
|
let effectiveUntil = until || null;
|
|
445
414
|
let autoWindowed = false;
|
|
446
|
-
if (effectiveCategory ===
|
|
415
|
+
if (effectiveCategory === 'event' && !since && !until) {
|
|
447
416
|
const decayMs = (config.eventDecayDays || 30) * 86400000;
|
|
448
417
|
effectiveSince = new Date(Date.now() - decayMs).toISOString();
|
|
449
418
|
autoWindowed = true;
|
|
@@ -456,7 +425,7 @@ export async function handler(
|
|
|
456
425
|
? Math.min(effectiveLimit * 10, MAX_FETCH_LIMIT)
|
|
457
426
|
: effectiveLimit;
|
|
458
427
|
|
|
459
|
-
let filtered;
|
|
428
|
+
let filtered: any[];
|
|
460
429
|
if (hasQuery) {
|
|
461
430
|
// Hybrid search mode
|
|
462
431
|
const sorted = await hybridSearch(ctx, query, {
|
|
@@ -487,48 +456,46 @@ export async function handler(
|
|
|
487
456
|
if (false) {
|
|
488
457
|
}
|
|
489
458
|
if (kindFilter) {
|
|
490
|
-
clauses.push(
|
|
459
|
+
clauses.push('kind = ?');
|
|
491
460
|
params.push(kindFilter);
|
|
492
461
|
}
|
|
493
462
|
if (scopedCategory) {
|
|
494
|
-
clauses.push(
|
|
463
|
+
clauses.push('category = ?');
|
|
495
464
|
params.push(scopedCategory);
|
|
496
465
|
}
|
|
497
466
|
if (effectiveSince) {
|
|
498
|
-
clauses.push(
|
|
467
|
+
clauses.push('created_at >= ?');
|
|
499
468
|
params.push(effectiveSince);
|
|
500
469
|
}
|
|
501
470
|
if (effectiveUntil) {
|
|
502
|
-
clauses.push(
|
|
471
|
+
clauses.push('created_at <= ?');
|
|
503
472
|
params.push(effectiveUntil);
|
|
504
473
|
}
|
|
505
474
|
clauses.push("(expires_at IS NULL OR expires_at > datetime('now'))");
|
|
506
475
|
if (!include_superseded) {
|
|
507
|
-
clauses.push(
|
|
476
|
+
clauses.push('superseded_by IS NULL');
|
|
508
477
|
}
|
|
509
|
-
const where = clauses.length ? `WHERE ${clauses.join(
|
|
478
|
+
const where = clauses.length ? `WHERE ${clauses.join(' AND ')}` : '';
|
|
510
479
|
params.push(fetchLimit);
|
|
511
480
|
let rows;
|
|
512
481
|
try {
|
|
513
482
|
rows = ctx.db
|
|
514
|
-
.prepare(
|
|
515
|
-
`SELECT * FROM vault ${where} ORDER BY created_at DESC LIMIT ?`,
|
|
516
|
-
)
|
|
483
|
+
.prepare(`SELECT * FROM vault ${where} ORDER BY created_at DESC LIMIT ?`)
|
|
517
484
|
.all(...params);
|
|
518
485
|
} catch (e) {
|
|
519
486
|
return errWithHint(
|
|
520
|
-
e.message,
|
|
521
|
-
|
|
522
|
-
|
|
487
|
+
e instanceof Error ? e.message : String(e),
|
|
488
|
+
'DB_ERROR',
|
|
489
|
+
'context-vault get_context DB_ERROR. Check `cat ~/.context-mcp/error.log | tail -5` and help me debug.'
|
|
523
490
|
);
|
|
524
491
|
}
|
|
525
492
|
|
|
526
493
|
// Post-filter by tags if provided, then apply requested limit
|
|
527
494
|
filtered = effectiveTags.length
|
|
528
495
|
? rows
|
|
529
|
-
.filter((r) => {
|
|
496
|
+
.filter((r: any) => {
|
|
530
497
|
const entryTags = r.tags ? JSON.parse(r.tags) : [];
|
|
531
|
-
return effectiveTags.some((t) => entryTags.includes(t));
|
|
498
|
+
return effectiveTags.some((t: string) => entryTags.includes(t));
|
|
532
499
|
})
|
|
533
500
|
.slice(0, effectiveLimit)
|
|
534
501
|
: rows;
|
|
@@ -540,18 +507,18 @@ export async function handler(
|
|
|
540
507
|
// Brief score boost: briefs rank slightly higher so consolidated snapshots
|
|
541
508
|
// surface above the individual entries they summarize.
|
|
542
509
|
for (const r of filtered) {
|
|
543
|
-
if (r.kind ===
|
|
510
|
+
if (r.kind === 'brief') r.score = (r.score || 0) + BRIEF_SCORE_BOOST;
|
|
544
511
|
}
|
|
545
|
-
filtered.sort((a, b) => b.score - a.score);
|
|
512
|
+
filtered.sort((a: any, b: any) => b.score - a.score);
|
|
546
513
|
|
|
547
514
|
// Tier filter: exclude ephemeral entries by default (NULL tier treated as working)
|
|
548
515
|
if (!include_ephemeral) {
|
|
549
|
-
filtered = filtered.filter((r) => r.tier !==
|
|
516
|
+
filtered = filtered.filter((r: any) => r.tier !== 'ephemeral');
|
|
550
517
|
}
|
|
551
518
|
|
|
552
519
|
// Event category filter: exclude events from semantic search by default
|
|
553
520
|
if (shouldExcludeEvents) {
|
|
554
|
-
filtered = filtered.filter((r) => r.category !==
|
|
521
|
+
filtered = filtered.filter((r: any) => r.category !== 'event');
|
|
555
522
|
}
|
|
556
523
|
|
|
557
524
|
if (!filtered.length) {
|
|
@@ -560,28 +527,14 @@ export async function handler(
|
|
|
560
527
|
return ok(
|
|
561
528
|
hasQuery
|
|
562
529
|
? `No results found for "${query}" in events (last ${days} days).\nTry with \`since: "YYYY-MM-DD"\` to search older events.`
|
|
563
|
-
: `No entries found matching the given filters in events (last ${days} days).\nTry with \`since: "YYYY-MM-DD"\` to search older events
|
|
530
|
+
: `No entries found matching the given filters in events (last ${days} days).\nTry with \`since: "YYYY-MM-DD"\` to search older events.`
|
|
564
531
|
);
|
|
565
532
|
}
|
|
566
533
|
return ok(
|
|
567
|
-
hasQuery
|
|
568
|
-
? "No results found for: " + query
|
|
569
|
-
: "No entries found matching the given filters.",
|
|
534
|
+
hasQuery ? 'No results found for: ' + query : 'No entries found matching the given filters.'
|
|
570
535
|
);
|
|
571
536
|
}
|
|
572
537
|
|
|
573
|
-
// Decrypt encrypted entries if ctx.decrypt is available
|
|
574
|
-
if (ctx.decrypt) {
|
|
575
|
-
for (const r of filtered) {
|
|
576
|
-
if (r.body_encrypted) {
|
|
577
|
-
const decrypted = await ctx.decrypt(r);
|
|
578
|
-
r.body = decrypted.body;
|
|
579
|
-
if (decrypted.title) r.title = decrypted.title;
|
|
580
|
-
if (decrypted.meta) r.meta = JSON.stringify(decrypted.meta);
|
|
581
|
-
}
|
|
582
|
-
}
|
|
583
|
-
}
|
|
584
|
-
|
|
585
538
|
// Token-budgeted packing
|
|
586
539
|
let tokensBudget = null;
|
|
587
540
|
let tokensUsed = null;
|
|
@@ -602,8 +555,7 @@ export async function handler(
|
|
|
602
555
|
}
|
|
603
556
|
|
|
604
557
|
// Skeleton mode: determine pivot threshold
|
|
605
|
-
const effectivePivot =
|
|
606
|
-
pivot_count != null ? pivot_count : DEFAULT_PIVOT_COUNT;
|
|
558
|
+
const effectivePivot = pivot_count != null ? pivot_count : DEFAULT_PIVOT_COUNT;
|
|
607
559
|
|
|
608
560
|
// Conflict detection
|
|
609
561
|
const conflicts = detect_conflicts ? detectConflicts(filtered, ctx) : [];
|
|
@@ -611,45 +563,43 @@ export async function handler(
|
|
|
611
563
|
const lines = [];
|
|
612
564
|
if (reindexFailed)
|
|
613
565
|
lines.push(
|
|
614
|
-
`> **Warning:** Auto-reindex failed. Results may be stale. Run \`context-vault reindex\` to fix.\n
|
|
566
|
+
`> **Warning:** Auto-reindex failed. Results may be stale. Run \`context-vault reindex\` to fix.\n`
|
|
615
567
|
);
|
|
616
568
|
if (hasQuery && isEmbedAvailable() === false)
|
|
617
569
|
lines.push(
|
|
618
|
-
`> **Note:** Semantic search unavailable — results ranked by keyword match only. Run \`context-vault setup\` to download the embedding model.\n
|
|
570
|
+
`> **Note:** Semantic search unavailable — results ranked by keyword match only. Run \`context-vault setup\` to download the embedding model.\n`
|
|
619
571
|
);
|
|
620
|
-
const heading = hasQuery ? `Results for "${query}"` :
|
|
572
|
+
const heading = hasQuery ? `Results for "${query}"` : 'Filtered entries';
|
|
621
573
|
lines.push(`## ${heading} (${filtered.length} matches)\n`);
|
|
622
574
|
if (tokensBudget != null) {
|
|
623
|
-
lines.push(
|
|
624
|
-
`> Token budget: ${tokensUsed} / ${tokensBudget} tokens used.\n`,
|
|
625
|
-
);
|
|
575
|
+
lines.push(`> Token budget: ${tokensUsed} / ${tokensBudget} tokens used.\n`);
|
|
626
576
|
}
|
|
627
577
|
if (autoWindowed) {
|
|
628
578
|
const days = config.eventDecayDays || 30;
|
|
629
579
|
lines.push(
|
|
630
|
-
`> ℹ Event search limited to last ${days} days. Use \`since\` parameter for older results.\n
|
|
580
|
+
`> ℹ Event search limited to last ${days} days. Use \`since\` parameter for older results.\n`
|
|
631
581
|
);
|
|
632
582
|
}
|
|
633
583
|
for (let i = 0; i < filtered.length; i++) {
|
|
634
584
|
const r = filtered[i];
|
|
635
585
|
const isSkeleton = i >= effectivePivot;
|
|
636
586
|
const entryTags = r.tags ? JSON.parse(r.tags) : [];
|
|
637
|
-
const tagStr = entryTags.length ? entryTags.join(
|
|
587
|
+
const tagStr = entryTags.length ? entryTags.join(', ') : 'none';
|
|
638
588
|
const relPath =
|
|
639
589
|
r.file_path && config.vaultDir
|
|
640
|
-
? r.file_path.replace(config.vaultDir +
|
|
641
|
-
: r.file_path ||
|
|
642
|
-
const skeletonLabel = isSkeleton ?
|
|
590
|
+
? r.file_path.replace(config.vaultDir + '/', '')
|
|
591
|
+
: r.file_path || 'n/a';
|
|
592
|
+
const skeletonLabel = isSkeleton ? ' ⊘ skeleton' : '';
|
|
643
593
|
lines.push(
|
|
644
|
-
`### [${i + 1}/${filtered.length}] ${r.title ||
|
|
594
|
+
`### [${i + 1}/${filtered.length}] ${r.title || '(untitled)'} [${r.kind}/${r.category}]${skeletonLabel}`
|
|
645
595
|
);
|
|
646
596
|
const dateStr =
|
|
647
597
|
r.updated_at && r.updated_at !== r.created_at
|
|
648
598
|
? `${r.created_at} (updated ${r.updated_at})`
|
|
649
|
-
: r.created_at ||
|
|
650
|
-
const tierStr = r.tier ? ` · tier: ${r.tier}` :
|
|
599
|
+
: r.created_at || '';
|
|
600
|
+
const tierStr = r.tier ? ` · tier: ${r.tier}` : '';
|
|
651
601
|
lines.push(
|
|
652
|
-
`${r.score.toFixed(3)} · ${tagStr} · ${relPath} · ${dateStr} · skeleton: ${isSkeleton}${tierStr} · id: \`${r.id}
|
|
602
|
+
`${r.score.toFixed(3)} · ${tagStr} · ${relPath} · ${dateStr} · skeleton: ${isSkeleton}${tierStr} · id: \`${r.id}\``
|
|
653
603
|
);
|
|
654
604
|
const stalenessResult = checkStaleness(r);
|
|
655
605
|
if (stalenessResult) {
|
|
@@ -660,34 +610,30 @@ export async function handler(
|
|
|
660
610
|
if (isSkeleton) {
|
|
661
611
|
lines.push(skeletonBody(r.body));
|
|
662
612
|
} else {
|
|
663
|
-
lines.push(r.body?.slice(0, 300) + (r.body?.length > 300 ?
|
|
613
|
+
lines.push(r.body?.slice(0, 300) + (r.body?.length > 300 ? '...' : ''));
|
|
664
614
|
}
|
|
665
|
-
lines.push(
|
|
615
|
+
lines.push('');
|
|
666
616
|
}
|
|
667
617
|
|
|
668
618
|
if (detect_conflicts) {
|
|
669
619
|
if (conflicts.length === 0) {
|
|
670
|
-
lines.push(
|
|
671
|
-
`## Conflict Detection\n\nNo conflicts detected among results.\n`,
|
|
672
|
-
);
|
|
620
|
+
lines.push(`## Conflict Detection\n\nNo conflicts detected among results.\n`);
|
|
673
621
|
} else {
|
|
674
622
|
lines.push(`## Conflict Detection (${conflicts.length} flagged)\n`);
|
|
675
623
|
for (const c of conflicts) {
|
|
676
|
-
lines.push(
|
|
677
|
-
`- **${c.reason}**: \`${c.entry_a_id}\` vs \`${c.entry_b_id}\``,
|
|
678
|
-
);
|
|
624
|
+
lines.push(`- **${c.reason}**: \`${c.entry_a_id}\` vs \`${c.entry_b_id}\``);
|
|
679
625
|
lines.push(` Recommendation: ${c.recommendation}`);
|
|
680
626
|
}
|
|
681
|
-
lines.push(
|
|
627
|
+
lines.push('');
|
|
682
628
|
}
|
|
683
629
|
}
|
|
684
630
|
|
|
685
631
|
// Graph traversal: follow related_to links bidirectionally
|
|
686
632
|
if (follow_links) {
|
|
687
|
-
const { forward, backward } = collectLinkedEntries(ctx.db, filtered);
|
|
688
|
-
const allLinked = [...forward, ...backward];
|
|
633
|
+
const { forward, backward } = collectLinkedEntries(ctx.db, filtered as any);
|
|
634
|
+
const allLinked: any[] = [...(forward as any[]), ...(backward as any[])];
|
|
689
635
|
const seen = new Set();
|
|
690
|
-
const uniqueLinked = allLinked.filter((e) => {
|
|
636
|
+
const uniqueLinked = allLinked.filter((e: any) => {
|
|
691
637
|
if (seen.has(e.id)) return false;
|
|
692
638
|
seen.add(e.id);
|
|
693
639
|
return true;
|
|
@@ -696,21 +642,17 @@ export async function handler(
|
|
|
696
642
|
if (uniqueLinked.length > 0) {
|
|
697
643
|
lines.push(`## Linked Entries (${uniqueLinked.length} via related_to)\n`);
|
|
698
644
|
for (const r of uniqueLinked) {
|
|
699
|
-
const direction = forward.some((f) => f.id === r.id)
|
|
700
|
-
? "→ forward"
|
|
701
|
-
: "← backlink";
|
|
645
|
+
const direction = forward.some((f: any) => f.id === r.id) ? '→ forward' : '← backlink';
|
|
702
646
|
const entryTags = r.tags ? JSON.parse(r.tags) : [];
|
|
703
|
-
const tagStr = entryTags.length ? entryTags.join(
|
|
647
|
+
const tagStr = entryTags.length ? entryTags.join(', ') : 'none';
|
|
704
648
|
const relPath =
|
|
705
649
|
r.file_path && config.vaultDir
|
|
706
|
-
? r.file_path.replace(config.vaultDir +
|
|
707
|
-
: r.file_path ||
|
|
708
|
-
lines.push(
|
|
709
|
-
`### ${r.title || "(untitled)"} [${r.kind}/${r.category}] ${direction}`,
|
|
710
|
-
);
|
|
650
|
+
? r.file_path.replace(config.vaultDir + '/', '')
|
|
651
|
+
: r.file_path || 'n/a';
|
|
652
|
+
lines.push(`### ${r.title || '(untitled)'} [${r.kind}/${r.category}] ${direction}`);
|
|
711
653
|
lines.push(`${tagStr} · ${relPath} · id: \`${r.id}\``);
|
|
712
|
-
lines.push(r.body?.slice(0, 200) + (r.body?.length > 200 ?
|
|
713
|
-
lines.push(
|
|
654
|
+
lines.push(r.body?.slice(0, 200) + (r.body?.length > 200 ? '...' : ''));
|
|
655
|
+
lines.push('');
|
|
714
656
|
}
|
|
715
657
|
} else {
|
|
716
658
|
lines.push(`## Linked Entries\n\nNo related entries found.\n`);
|
|
@@ -719,24 +661,19 @@ export async function handler(
|
|
|
719
661
|
|
|
720
662
|
// Consolidation suggestion detection — lazy, opportunistic, vault-wide
|
|
721
663
|
const consolidationOpts = {
|
|
722
|
-
tagThreshold:
|
|
723
|
-
|
|
724
|
-
maxAgeDays:
|
|
725
|
-
config.consolidation?.maxAgeDays ?? CONSOLIDATION_SNAPSHOT_MAX_AGE_DAYS,
|
|
664
|
+
tagThreshold: config.consolidation?.tagThreshold ?? CONSOLIDATION_TAG_THRESHOLD,
|
|
665
|
+
maxAgeDays: config.consolidation?.maxAgeDays ?? CONSOLIDATION_SNAPSHOT_MAX_AGE_DAYS,
|
|
726
666
|
};
|
|
727
667
|
const consolidationSuggestions = detectConsolidationHints(
|
|
728
668
|
filtered,
|
|
729
669
|
ctx.db,
|
|
730
670
|
|
|
731
|
-
consolidationOpts
|
|
671
|
+
consolidationOpts
|
|
732
672
|
);
|
|
733
673
|
|
|
734
674
|
// Auto-consolidate: fire-and-forget create_snapshot for eligible tags
|
|
735
|
-
if (
|
|
736
|
-
|
|
737
|
-
consolidationSuggestions.length > 0
|
|
738
|
-
) {
|
|
739
|
-
const { handler: snapshotHandler } = await import("./create-snapshot.js");
|
|
675
|
+
if (config.consolidation?.autoConsolidate && consolidationSuggestions.length > 0) {
|
|
676
|
+
const { handler: snapshotHandler } = await import('./create-snapshot.js');
|
|
740
677
|
for (const suggestion of consolidationSuggestions) {
|
|
741
678
|
snapshotHandler({ topic: suggestion.tag, tags: [suggestion.tag] }, ctx, {
|
|
742
679
|
ensureIndexed: async () => {},
|
|
@@ -744,8 +681,8 @@ export async function handler(
|
|
|
744
681
|
}
|
|
745
682
|
}
|
|
746
683
|
|
|
747
|
-
const result = ok(lines.join(
|
|
748
|
-
const meta = {};
|
|
684
|
+
const result: ToolResult = ok(lines.join('\n'));
|
|
685
|
+
const meta: Record<string, unknown> = {};
|
|
749
686
|
meta.scope = effectiveScope;
|
|
750
687
|
if (tokensBudget != null) {
|
|
751
688
|
meta.tokens_used = tokensUsed;
|