@context-vault/core 2.9.0 → 2.10.0
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/package.json +1 -1
- package/src/core/error-log.js +54 -0
- package/src/core/status.js +13 -0
- package/src/server/helpers.js +15 -2
- package/src/server/tools/context-status.js +37 -0
- package/src/server/tools/get-context.js +16 -6
- package/src/server/tools/list-context.js +30 -5
- package/src/server/tools.js +38 -3
package/package.json
CHANGED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import {
|
|
2
|
+
appendFileSync,
|
|
3
|
+
existsSync,
|
|
4
|
+
mkdirSync,
|
|
5
|
+
readFileSync,
|
|
6
|
+
statSync,
|
|
7
|
+
writeFileSync,
|
|
8
|
+
} from "node:fs";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
|
|
11
|
+
const MAX_LOG_SIZE = 1024 * 1024; // 1MB
|
|
12
|
+
|
|
13
|
+
export function errorLogPath(dataDir) {
|
|
14
|
+
return join(dataDir, "error.log");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Append a structured JSON entry to the startup error log.
|
|
19
|
+
* Rotates the file if it exceeds MAX_LOG_SIZE.
|
|
20
|
+
* Never throws — logging failures must not mask the original error.
|
|
21
|
+
*
|
|
22
|
+
* @param {string} dataDir
|
|
23
|
+
* @param {object} entry
|
|
24
|
+
*/
|
|
25
|
+
export function appendErrorLog(dataDir, entry) {
|
|
26
|
+
try {
|
|
27
|
+
mkdirSync(dataDir, { recursive: true });
|
|
28
|
+
const logPath = errorLogPath(dataDir);
|
|
29
|
+
if (existsSync(logPath) && statSync(logPath).size >= MAX_LOG_SIZE) {
|
|
30
|
+
writeFileSync(logPath, "");
|
|
31
|
+
}
|
|
32
|
+
appendFileSync(logPath, JSON.stringify(entry) + "\n");
|
|
33
|
+
} catch {
|
|
34
|
+
// intentionally swallowed
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Return number of log lines in the error log, or 0 if absent.
|
|
40
|
+
*
|
|
41
|
+
* @param {string} dataDir
|
|
42
|
+
* @returns {number}
|
|
43
|
+
*/
|
|
44
|
+
export function errorLogCount(dataDir) {
|
|
45
|
+
try {
|
|
46
|
+
const logPath = errorLogPath(dataDir);
|
|
47
|
+
if (!existsSync(logPath)) return 0;
|
|
48
|
+
return readFileSync(logPath, "utf-8")
|
|
49
|
+
.split("\n")
|
|
50
|
+
.filter((l) => l.trim()).length;
|
|
51
|
+
} catch {
|
|
52
|
+
return 0;
|
|
53
|
+
}
|
|
54
|
+
}
|
package/src/core/status.js
CHANGED
|
@@ -128,6 +128,18 @@ export function gatherVaultStatus(ctx, opts = {}) {
|
|
|
128
128
|
// Embedding model availability
|
|
129
129
|
const embedModelAvailable = isEmbedAvailable();
|
|
130
130
|
|
|
131
|
+
// Count auto-captured feedback entries (written by tracked() on unhandled errors)
|
|
132
|
+
let autoCapturedFeedbackCount = 0;
|
|
133
|
+
try {
|
|
134
|
+
autoCapturedFeedbackCount = db
|
|
135
|
+
.prepare(
|
|
136
|
+
`SELECT COUNT(*) as c FROM vault WHERE kind = 'feedback' AND tags LIKE '%"auto-captured"%' ${userAnd}`,
|
|
137
|
+
)
|
|
138
|
+
.get(...userParams).c;
|
|
139
|
+
} catch (e) {
|
|
140
|
+
errors.push(`Auto-captured feedback count failed: ${e.message}`);
|
|
141
|
+
}
|
|
142
|
+
|
|
131
143
|
return {
|
|
132
144
|
fileCount,
|
|
133
145
|
subdirs,
|
|
@@ -140,6 +152,7 @@ export function gatherVaultStatus(ctx, opts = {}) {
|
|
|
140
152
|
expiredCount,
|
|
141
153
|
embeddingStatus,
|
|
142
154
|
embedModelAvailable,
|
|
155
|
+
autoCapturedFeedbackCount,
|
|
143
156
|
resolvedFrom: config.resolvedFrom,
|
|
144
157
|
errors,
|
|
145
158
|
};
|
package/src/server/helpers.js
CHANGED
|
@@ -2,12 +2,25 @@
|
|
|
2
2
|
* helpers.js — Shared MCP response helpers and validation
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
import pkg from "../../package.json" with { type: "json" };
|
|
6
|
+
|
|
5
7
|
export function ok(text) {
|
|
6
8
|
return { content: [{ type: "text", text }] };
|
|
7
9
|
}
|
|
8
10
|
|
|
9
|
-
export function err(text, code = "UNKNOWN") {
|
|
10
|
-
return {
|
|
11
|
+
export function err(text, code = "UNKNOWN", meta = {}) {
|
|
12
|
+
return {
|
|
13
|
+
content: [{ type: "text", text }],
|
|
14
|
+
isError: true,
|
|
15
|
+
code,
|
|
16
|
+
_meta: {
|
|
17
|
+
cv_version: pkg.version,
|
|
18
|
+
node_version: process.version,
|
|
19
|
+
platform: process.platform,
|
|
20
|
+
arch: process.arch,
|
|
21
|
+
...meta,
|
|
22
|
+
},
|
|
23
|
+
};
|
|
11
24
|
}
|
|
12
25
|
|
|
13
26
|
export function ensureVaultExists(config) {
|
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
import { gatherVaultStatus } from "../../core/status.js";
|
|
2
|
+
import { errorLogPath, errorLogCount } from "../../core/error-log.js";
|
|
2
3
|
import { ok } from "../helpers.js";
|
|
3
4
|
|
|
5
|
+
function relativeTime(ts) {
|
|
6
|
+
const secs = Math.floor((Date.now() - ts) / 1000);
|
|
7
|
+
if (secs < 60) return `${secs}s ago`;
|
|
8
|
+
const mins = Math.floor(secs / 60);
|
|
9
|
+
if (mins < 60) return `${mins} minute${mins === 1 ? "" : "s"} ago`;
|
|
10
|
+
const hrs = Math.floor(mins / 60);
|
|
11
|
+
return `${hrs} hour${hrs === 1 ? "" : "s"} ago`;
|
|
12
|
+
}
|
|
13
|
+
|
|
4
14
|
export const name = "context_status";
|
|
5
15
|
|
|
6
16
|
export const description =
|
|
@@ -83,6 +93,33 @@ export function handler(_args, ctx) {
|
|
|
83
93
|
lines.push(`Auto-reindex will fix this on next search or save.`);
|
|
84
94
|
}
|
|
85
95
|
|
|
96
|
+
// Error log
|
|
97
|
+
const logPath = errorLogPath(config.dataDir);
|
|
98
|
+
const logCount = errorLogCount(config.dataDir);
|
|
99
|
+
if (logCount > 0) {
|
|
100
|
+
lines.push(``, `### Startup Error Log`);
|
|
101
|
+
lines.push(`- Path: ${logPath}`);
|
|
102
|
+
lines.push(`- Entries: ${logCount} (share this file for support)`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Health: session-level tool call stats
|
|
106
|
+
const ts = ctx.toolStats;
|
|
107
|
+
if (ts) {
|
|
108
|
+
lines.push(``, `### Health`);
|
|
109
|
+
lines.push(`- Tool calls (session): ${ts.ok} ok, ${ts.errors} errors`);
|
|
110
|
+
if (ts.lastError) {
|
|
111
|
+
const { tool, code, timestamp } = ts.lastError;
|
|
112
|
+
lines.push(
|
|
113
|
+
`- Last error: ${tool ?? "unknown"} — ${code} (${relativeTime(timestamp)})`,
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
if (status.autoCapturedFeedbackCount > 0) {
|
|
117
|
+
lines.push(
|
|
118
|
+
`- Auto-captured feedback entries: ${status.autoCapturedFeedbackCount} (run get_context with kind:feedback tags:auto-captured)`,
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
86
123
|
// Suggested actions
|
|
87
124
|
const actions = [];
|
|
88
125
|
if (status.stalePaths)
|
|
@@ -182,12 +182,21 @@ export async function handler(
|
|
|
182
182
|
for (const r of filtered) r.score = 0;
|
|
183
183
|
}
|
|
184
184
|
|
|
185
|
-
if (!filtered.length)
|
|
185
|
+
if (!filtered.length) {
|
|
186
|
+
if (autoWindowed) {
|
|
187
|
+
const days = config.eventDecayDays || 30;
|
|
188
|
+
return ok(
|
|
189
|
+
hasQuery
|
|
190
|
+
? `No results found for "${query}" in events (last ${days} days).\nTry with \`since: "YYYY-MM-DD"\` to search older events.`
|
|
191
|
+
: `No entries found matching the given filters in events (last ${days} days).\nTry with \`since: "YYYY-MM-DD"\` to search older events.`,
|
|
192
|
+
);
|
|
193
|
+
}
|
|
186
194
|
return ok(
|
|
187
195
|
hasQuery
|
|
188
196
|
? "No results found for: " + query
|
|
189
197
|
: "No entries found matching the given filters.",
|
|
190
198
|
);
|
|
199
|
+
}
|
|
191
200
|
|
|
192
201
|
// Decrypt encrypted entries if ctx.decrypt is available
|
|
193
202
|
if (ctx.decrypt) {
|
|
@@ -212,6 +221,12 @@ export async function handler(
|
|
|
212
221
|
);
|
|
213
222
|
const heading = hasQuery ? `Results for "${query}"` : "Filtered entries";
|
|
214
223
|
lines.push(`## ${heading} (${filtered.length} matches)\n`);
|
|
224
|
+
if (autoWindowed) {
|
|
225
|
+
const days = config.eventDecayDays || 30;
|
|
226
|
+
lines.push(
|
|
227
|
+
`> ℹ Event search limited to last ${days} days. Use \`since\` parameter for older results.\n`,
|
|
228
|
+
);
|
|
229
|
+
}
|
|
215
230
|
for (let i = 0; i < filtered.length; i++) {
|
|
216
231
|
const r = filtered[i];
|
|
217
232
|
const entryTags = r.tags ? JSON.parse(r.tags) : [];
|
|
@@ -229,10 +244,5 @@ export async function handler(
|
|
|
229
244
|
lines.push(r.body?.slice(0, 300) + (r.body?.length > 300 ? "..." : ""));
|
|
230
245
|
lines.push("");
|
|
231
246
|
}
|
|
232
|
-
if (autoWindowed) {
|
|
233
|
-
lines.push(
|
|
234
|
-
`_Showing events from last ${config.eventDecayDays || 30} days. Use since/until for custom range._`,
|
|
235
|
-
);
|
|
236
|
-
}
|
|
237
247
|
return ok(lines.join("\n"));
|
|
238
248
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { normalizeKind } from "../../core/files.js";
|
|
3
|
+
import { categoryFor } from "../../core/categories.js";
|
|
3
4
|
import { ok } from "../helpers.js";
|
|
4
5
|
|
|
5
6
|
export const name = "list_context";
|
|
@@ -50,6 +51,17 @@ export async function handler(
|
|
|
50
51
|
|
|
51
52
|
await ensureIndexed();
|
|
52
53
|
|
|
54
|
+
const kindFilter = kind ? normalizeKind(kind) : null;
|
|
55
|
+
const effectiveCategory =
|
|
56
|
+
category || (kindFilter ? categoryFor(kindFilter) : null);
|
|
57
|
+
let effectiveSince = since || null;
|
|
58
|
+
let autoWindowed = false;
|
|
59
|
+
if (effectiveCategory === "event" && !since && !until) {
|
|
60
|
+
const decayMs = (config.eventDecayDays || 30) * 86400000;
|
|
61
|
+
effectiveSince = new Date(Date.now() - decayMs).toISOString();
|
|
62
|
+
autoWindowed = true;
|
|
63
|
+
}
|
|
64
|
+
|
|
53
65
|
const clauses = [];
|
|
54
66
|
const params = [];
|
|
55
67
|
|
|
@@ -57,17 +69,17 @@ export async function handler(
|
|
|
57
69
|
clauses.push("user_id = ?");
|
|
58
70
|
params.push(userId);
|
|
59
71
|
}
|
|
60
|
-
if (
|
|
72
|
+
if (kindFilter) {
|
|
61
73
|
clauses.push("kind = ?");
|
|
62
|
-
params.push(
|
|
74
|
+
params.push(kindFilter);
|
|
63
75
|
}
|
|
64
76
|
if (category) {
|
|
65
77
|
clauses.push("category = ?");
|
|
66
78
|
params.push(category);
|
|
67
79
|
}
|
|
68
|
-
if (
|
|
80
|
+
if (effectiveSince) {
|
|
69
81
|
clauses.push("created_at >= ?");
|
|
70
|
-
params.push(
|
|
82
|
+
params.push(effectiveSince);
|
|
71
83
|
}
|
|
72
84
|
if (until) {
|
|
73
85
|
clauses.push("created_at <= ?");
|
|
@@ -103,8 +115,15 @@ export async function handler(
|
|
|
103
115
|
.slice(0, effectiveLimit)
|
|
104
116
|
: rows;
|
|
105
117
|
|
|
106
|
-
if (!filtered.length)
|
|
118
|
+
if (!filtered.length) {
|
|
119
|
+
if (autoWindowed) {
|
|
120
|
+
const days = config.eventDecayDays || 30;
|
|
121
|
+
return ok(
|
|
122
|
+
`No entries found matching the given filters in events (last ${days} days).\nTry with \`since: "YYYY-MM-DD"\` to search older events.`,
|
|
123
|
+
);
|
|
124
|
+
}
|
|
107
125
|
return ok("No entries found matching the given filters.");
|
|
126
|
+
}
|
|
108
127
|
|
|
109
128
|
const lines = [];
|
|
110
129
|
if (reindexFailed)
|
|
@@ -112,6 +131,12 @@ export async function handler(
|
|
|
112
131
|
`> **Warning:** Auto-reindex failed. Results may be stale. Run \`context-vault reindex\` to fix.\n`,
|
|
113
132
|
);
|
|
114
133
|
lines.push(`## Vault Entries (${filtered.length} shown, ${total} total)\n`);
|
|
134
|
+
if (autoWindowed) {
|
|
135
|
+
const days = config.eventDecayDays || 30;
|
|
136
|
+
lines.push(
|
|
137
|
+
`> ℹ Event search limited to last ${days} days. Use \`since\` parameter for older results.\n`,
|
|
138
|
+
);
|
|
139
|
+
}
|
|
115
140
|
for (const r of filtered) {
|
|
116
141
|
const entryTags = r.tags ? JSON.parse(r.tags) : [];
|
|
117
142
|
const tagStr = entryTags.length ? entryTags.join(", ") : "none";
|
package/src/server/tools.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { reindex } from "../index/index.js";
|
|
2
|
+
import { captureAndIndex } from "../capture/index.js";
|
|
2
3
|
import { err } from "./helpers.js";
|
|
4
|
+
import pkg from "../../package.json" with { type: "json" };
|
|
3
5
|
|
|
4
6
|
import * as getContext from "./tools/get-context.js";
|
|
5
7
|
import * as saveContext from "./tools/save-context.js";
|
|
@@ -24,14 +26,14 @@ const TOOL_TIMEOUT_MS = 60_000;
|
|
|
24
26
|
export function registerTools(server, ctx) {
|
|
25
27
|
const userId = ctx.userId !== undefined ? ctx.userId : undefined;
|
|
26
28
|
|
|
27
|
-
function tracked(handler) {
|
|
29
|
+
function tracked(handler, toolName) {
|
|
28
30
|
return async (...args) => {
|
|
29
31
|
if (ctx.activeOps) ctx.activeOps.count++;
|
|
30
32
|
let timer;
|
|
31
33
|
let handlerPromise;
|
|
32
34
|
try {
|
|
33
35
|
handlerPromise = Promise.resolve(handler(...args));
|
|
34
|
-
|
|
36
|
+
const result = await Promise.race([
|
|
35
37
|
handlerPromise,
|
|
36
38
|
new Promise((_, reject) => {
|
|
37
39
|
timer = setTimeout(
|
|
@@ -40,16 +42,49 @@ export function registerTools(server, ctx) {
|
|
|
40
42
|
);
|
|
41
43
|
}),
|
|
42
44
|
]);
|
|
45
|
+
if (ctx.toolStats) ctx.toolStats.ok++;
|
|
46
|
+
return result;
|
|
43
47
|
} catch (e) {
|
|
44
48
|
if (e.message === "TOOL_TIMEOUT") {
|
|
45
49
|
// Suppress any late rejection from the still-running handler to
|
|
46
50
|
// prevent unhandled promise rejection warnings in the host process.
|
|
47
51
|
handlerPromise?.catch(() => {});
|
|
52
|
+
if (ctx.toolStats) {
|
|
53
|
+
ctx.toolStats.errors++;
|
|
54
|
+
ctx.toolStats.lastError = {
|
|
55
|
+
tool: toolName,
|
|
56
|
+
code: "TIMEOUT",
|
|
57
|
+
timestamp: Date.now(),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
48
60
|
return err(
|
|
49
61
|
"Tool timed out after 60s. Try a simpler query or run `context-vault reindex` first.",
|
|
50
62
|
"TIMEOUT",
|
|
51
63
|
);
|
|
52
64
|
}
|
|
65
|
+
if (ctx.toolStats) {
|
|
66
|
+
ctx.toolStats.errors++;
|
|
67
|
+
ctx.toolStats.lastError = {
|
|
68
|
+
tool: toolName,
|
|
69
|
+
code: "UNKNOWN",
|
|
70
|
+
timestamp: Date.now(),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
try {
|
|
74
|
+
await captureAndIndex(ctx, {
|
|
75
|
+
kind: "feedback",
|
|
76
|
+
title: `Unhandled error in ${toolName ?? "tool"} call`,
|
|
77
|
+
body: `${e.message}\n\n${e.stack ?? ""}`,
|
|
78
|
+
tags: ["bug", "auto-captured"],
|
|
79
|
+
source: "auto-capture",
|
|
80
|
+
meta: {
|
|
81
|
+
tool: toolName,
|
|
82
|
+
error_type: e.constructor?.name,
|
|
83
|
+
cv_version: pkg.version,
|
|
84
|
+
auto: true,
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
} catch {} // never block on feedback capture
|
|
53
88
|
throw e;
|
|
54
89
|
} finally {
|
|
55
90
|
clearTimeout(timer);
|
|
@@ -110,7 +145,7 @@ export function registerTools(server, ctx) {
|
|
|
110
145
|
mod.name,
|
|
111
146
|
mod.description,
|
|
112
147
|
mod.inputSchema,
|
|
113
|
-
tracked((args) => mod.handler(args, ctx, shared)),
|
|
148
|
+
tracked((args) => mod.handler(args, ctx, shared), mod.name),
|
|
114
149
|
);
|
|
115
150
|
}
|
|
116
151
|
}
|