context-vault 3.13.0 → 3.16.1
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 +263 -414
- package/dist/error-log.d.ts +2 -0
- package/dist/error-log.d.ts.map +1 -1
- package/dist/error-log.js +31 -1
- package/dist/error-log.js.map +1 -1
- package/dist/register-tools.d.ts.map +1 -1
- package/dist/register-tools.js +4 -0
- package/dist/register-tools.js.map +1 -1
- package/dist/server.js +23 -426
- package/dist/server.js.map +1 -1
- package/dist/status.d.ts.map +1 -1
- package/dist/status.js +17 -0
- package/dist/status.js.map +1 -1
- package/dist/tools/context-status.d.ts.map +1 -1
- package/dist/tools/context-status.js +26 -1
- package/dist/tools/context-status.js.map +1 -1
- package/dist/tools/delete-context.d.ts +1 -1
- package/dist/tools/delete-context.d.ts.map +1 -1
- package/dist/tools/delete-context.js +15 -2
- package/dist/tools/delete-context.js.map +1 -1
- package/dist/tools/get-context.d.ts.map +1 -1
- package/dist/tools/get-context.js +3 -2
- package/dist/tools/get-context.js.map +1 -1
- package/dist/tools/list-context.d.ts +7 -15
- package/dist/tools/list-context.d.ts.map +1 -1
- package/dist/tools/list-context.js +570 -111
- package/dist/tools/list-context.js.map +1 -1
- package/dist/tools/publish-to-team.js +1 -1
- package/dist/tools/publish-to-team.js.map +1 -1
- package/dist/tools/save-context.js +2 -2
- package/dist/tools/save-context.js.map +1 -1
- package/dist/tools/session-start.d.ts +20 -7
- package/dist/tools/session-start.d.ts.map +1 -1
- package/dist/tools/session-start.js +406 -439
- package/dist/tools/session-start.js.map +1 -1
- package/node_modules/@context-vault/core/dist/capture.d.ts.map +1 -1
- package/node_modules/@context-vault/core/dist/capture.js +4 -0
- package/node_modules/@context-vault/core/dist/capture.js.map +1 -1
- package/node_modules/@context-vault/core/dist/categories.d.ts.map +1 -1
- package/node_modules/@context-vault/core/dist/categories.js +8 -0
- package/node_modules/@context-vault/core/dist/categories.js.map +1 -1
- package/node_modules/@context-vault/core/dist/compact.d.ts +38 -0
- package/node_modules/@context-vault/core/dist/compact.d.ts.map +1 -0
- package/node_modules/@context-vault/core/dist/compact.js +127 -0
- package/node_modules/@context-vault/core/dist/compact.js.map +1 -0
- package/node_modules/@context-vault/core/dist/config.d.ts.map +1 -1
- package/node_modules/@context-vault/core/dist/config.js +12 -0
- package/node_modules/@context-vault/core/dist/config.js.map +1 -1
- package/node_modules/@context-vault/core/dist/db.d.ts +1 -1
- package/node_modules/@context-vault/core/dist/db.d.ts.map +1 -1
- package/node_modules/@context-vault/core/dist/db.js +40 -4
- package/node_modules/@context-vault/core/dist/db.js.map +1 -1
- package/node_modules/@context-vault/core/dist/main.d.ts +6 -2
- package/node_modules/@context-vault/core/dist/main.d.ts.map +1 -1
- package/node_modules/@context-vault/core/dist/main.js +5 -1
- package/node_modules/@context-vault/core/dist/main.js.map +1 -1
- package/node_modules/@context-vault/core/dist/search.d.ts +13 -1
- package/node_modules/@context-vault/core/dist/search.d.ts.map +1 -1
- package/node_modules/@context-vault/core/dist/search.js +50 -5
- package/node_modules/@context-vault/core/dist/search.js.map +1 -1
- package/node_modules/@context-vault/core/dist/tier-analysis.d.ts +36 -0
- package/node_modules/@context-vault/core/dist/tier-analysis.d.ts.map +1 -0
- package/node_modules/@context-vault/core/dist/tier-analysis.js +227 -0
- package/node_modules/@context-vault/core/dist/tier-analysis.js.map +1 -0
- package/node_modules/@context-vault/core/dist/types.d.ts +12 -0
- package/node_modules/@context-vault/core/dist/types.d.ts.map +1 -1
- package/node_modules/@context-vault/core/dist/watch.d.ts +21 -0
- package/node_modules/@context-vault/core/dist/watch.d.ts.map +1 -0
- package/node_modules/@context-vault/core/dist/watch.js +230 -0
- package/node_modules/@context-vault/core/dist/watch.js.map +1 -0
- package/node_modules/@context-vault/core/package.json +13 -1
- package/node_modules/@context-vault/core/src/capture.ts +4 -0
- package/node_modules/@context-vault/core/src/categories.ts +8 -0
- package/node_modules/@context-vault/core/src/compact.ts +183 -0
- package/node_modules/@context-vault/core/src/config.ts +8 -0
- package/node_modules/@context-vault/core/src/db.ts +40 -4
- package/node_modules/@context-vault/core/src/main.ts +10 -0
- package/node_modules/@context-vault/core/src/search.ts +55 -4
- package/node_modules/@context-vault/core/src/tier-analysis.ts +299 -0
- package/node_modules/@context-vault/core/src/types.ts +10 -0
- package/node_modules/@context-vault/core/src/watch.ts +269 -0
- package/package.json +2 -2
- package/scripts/postinstall.js +26 -1
- package/src/error-log.ts +30 -0
- package/src/register-tools.ts +4 -0
- package/src/server.ts +23 -423
- package/src/status.ts +17 -0
- package/src/tools/context-status.ts +30 -1
- package/src/tools/delete-context.ts +10 -5
- package/src/tools/get-context.ts +3 -2
- package/src/tools/list-context.ts +620 -119
- package/src/tools/publish-to-team.ts +1 -1
- package/src/tools/save-context.ts +2 -2
- package/src/tools/session-start.ts +444 -484
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import { watch, readFileSync, existsSync, statSync, writeFileSync, unlinkSync, mkdirSync } from 'node:fs';
|
|
2
|
+
import { join, relative, extname, basename, dirname } from 'node:path';
|
|
3
|
+
import type { FSWatcher } from 'node:fs';
|
|
4
|
+
import type { BaseCtx, IndexingConfig } from './types.js';
|
|
5
|
+
import { parseFrontmatter, parseEntryFromMarkdown } from './frontmatter.js';
|
|
6
|
+
import { indexEntry } from './index.js';
|
|
7
|
+
import { categoryFor, defaultTierFor, CATEGORY_DIRS } from './categories.js';
|
|
8
|
+
import { dirToKind } from './files.js';
|
|
9
|
+
import { shouldIndex } from './indexing.js';
|
|
10
|
+
|
|
11
|
+
export interface WatcherOptions {
|
|
12
|
+
vaultDir: string;
|
|
13
|
+
debounceMs?: number;
|
|
14
|
+
indexingConfig?: IndexingConfig;
|
|
15
|
+
onIndex?: (filePath: string, action: 'upsert' | 'delete') => void;
|
|
16
|
+
onError?: (err: Error) => void;
|
|
17
|
+
/** Max concurrent file processing operations. Default 3. */
|
|
18
|
+
maxConcurrency?: number;
|
|
19
|
+
/** Max events queued before dropping oldest. Default 200. */
|
|
20
|
+
maxQueueSize?: number;
|
|
21
|
+
/** Data directory for the lock file (e.g. ~/.context-mcp). */
|
|
22
|
+
dataDir?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface VaultWatcher {
|
|
26
|
+
close: () => void;
|
|
27
|
+
/** Mark a path as self-written so the watcher skips it. TTL: 2 seconds. */
|
|
28
|
+
markSelfWrite: (filePath: string) => void;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const EXCLUDED_DIRS = new Set(['_archive', 'projects']);
|
|
32
|
+
const SELF_WRITE_TTL_MS = 2_000;
|
|
33
|
+
const LOCK_STALE_MS = 60_000;
|
|
34
|
+
|
|
35
|
+
function isExcluded(relPath: string): boolean {
|
|
36
|
+
if (relPath.startsWith('.')) return true;
|
|
37
|
+
const parts = relPath.split('/');
|
|
38
|
+
for (const part of parts) {
|
|
39
|
+
if (part.startsWith('.')) return true;
|
|
40
|
+
if (EXCLUDED_DIRS.has(part)) return true;
|
|
41
|
+
}
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Acquire a watcher lock file so only one instance watches per vault.
|
|
47
|
+
* Returns a release function, or null if another instance holds the lock.
|
|
48
|
+
*/
|
|
49
|
+
function acquireWatcherLock(dataDir: string): (() => void) | null {
|
|
50
|
+
const lockPath = join(dataDir, 'watcher.lock');
|
|
51
|
+
mkdirSync(dataDir, { recursive: true });
|
|
52
|
+
|
|
53
|
+
if (existsSync(lockPath)) {
|
|
54
|
+
try {
|
|
55
|
+
const content = readFileSync(lockPath, 'utf-8').trim();
|
|
56
|
+
const [pidStr, tsStr] = content.split(':');
|
|
57
|
+
const pid = parseInt(pidStr, 10);
|
|
58
|
+
const ts = parseInt(tsStr, 10);
|
|
59
|
+
|
|
60
|
+
// Check if the lock holder is still alive
|
|
61
|
+
if (pid && !isNaN(pid)) {
|
|
62
|
+
try {
|
|
63
|
+
process.kill(pid, 0); // signal 0 = check if alive
|
|
64
|
+
// Process is alive. Check if lock is stale (>60s without refresh).
|
|
65
|
+
if (Date.now() - ts < LOCK_STALE_MS) {
|
|
66
|
+
return null; // another live instance holds the lock
|
|
67
|
+
}
|
|
68
|
+
// Stale lock from a live process that stopped refreshing. Take over.
|
|
69
|
+
} catch {
|
|
70
|
+
// Process is dead. Safe to take over the lock.
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
} catch {
|
|
74
|
+
// Unreadable lock file. Overwrite it.
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const writeLock = () => {
|
|
79
|
+
try { writeFileSync(lockPath, `${process.pid}:${Date.now()}`); } catch {}
|
|
80
|
+
};
|
|
81
|
+
writeLock();
|
|
82
|
+
|
|
83
|
+
// Refresh lock periodically so other instances know we're alive
|
|
84
|
+
const refreshInterval = setInterval(writeLock, 15_000);
|
|
85
|
+
refreshInterval.unref();
|
|
86
|
+
|
|
87
|
+
return () => {
|
|
88
|
+
clearInterval(refreshInterval);
|
|
89
|
+
try { unlinkSync(lockPath); } catch {}
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function processFile(ctx: BaseCtx, filePath: string, opts: WatcherOptions): Promise<void> {
|
|
94
|
+
try {
|
|
95
|
+
if (!existsSync(filePath)) return;
|
|
96
|
+
const stat = statSync(filePath);
|
|
97
|
+
if (!stat.isFile()) return;
|
|
98
|
+
|
|
99
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
100
|
+
const { meta: fmMeta, body: rawBody } = parseFrontmatter(content);
|
|
101
|
+
|
|
102
|
+
const id = fmMeta.id as string | undefined;
|
|
103
|
+
if (!id) return; // no frontmatter id = not a vault entry
|
|
104
|
+
|
|
105
|
+
const relPath = relative(opts.vaultDir, filePath);
|
|
106
|
+
const topDir = relPath.split('/')[0];
|
|
107
|
+
const kind = (fmMeta.kind as string) || dirToKind(topDir) || 'reference';
|
|
108
|
+
const category = categoryFor(kind);
|
|
109
|
+
const tier = (fmMeta.tier as string) || defaultTierFor(kind);
|
|
110
|
+
|
|
111
|
+
const { title, body, meta } = parseEntryFromMarkdown(kind, rawBody, fmMeta);
|
|
112
|
+
|
|
113
|
+
const tags = Array.isArray(fmMeta.tags) ? (fmMeta.tags as string[]) : null;
|
|
114
|
+
const source = typeof fmMeta.source === 'string' ? fmMeta.source : null;
|
|
115
|
+
const identity_key = typeof fmMeta.identity_key === 'string' ? fmMeta.identity_key : null;
|
|
116
|
+
const expires_at = typeof fmMeta.expires_at === 'string' ? fmMeta.expires_at : null;
|
|
117
|
+
const createdAt = typeof fmMeta.created === 'string' ? fmMeta.created : new Date().toISOString();
|
|
118
|
+
|
|
119
|
+
const source_files = fmMeta.source_files
|
|
120
|
+
? (fmMeta.source_files as Array<{ path: string; hash: string }>)
|
|
121
|
+
: null;
|
|
122
|
+
|
|
123
|
+
const explicitIndexed = fmMeta.indexed != null ? fmMeta.indexed === true || fmMeta.indexed === 'true' : undefined;
|
|
124
|
+
const indexed = shouldIndex(
|
|
125
|
+
{ kind, category, bodyLength: body.length, explicitIndexed },
|
|
126
|
+
opts.indexingConfig
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
const supersedes = Array.isArray(fmMeta.supersedes) ? (fmMeta.supersedes as string[]) : null;
|
|
130
|
+
const related_to = Array.isArray(fmMeta.related_to) ? (fmMeta.related_to as string[]) : null;
|
|
131
|
+
|
|
132
|
+
await indexEntry(ctx, {
|
|
133
|
+
id,
|
|
134
|
+
kind,
|
|
135
|
+
category,
|
|
136
|
+
title,
|
|
137
|
+
body,
|
|
138
|
+
meta: meta || undefined,
|
|
139
|
+
tags,
|
|
140
|
+
source,
|
|
141
|
+
filePath,
|
|
142
|
+
createdAt,
|
|
143
|
+
identity_key,
|
|
144
|
+
expires_at,
|
|
145
|
+
source_files,
|
|
146
|
+
tier,
|
|
147
|
+
indexed,
|
|
148
|
+
supersedes,
|
|
149
|
+
related_to,
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
opts.onIndex?.(filePath, 'upsert');
|
|
153
|
+
} catch (err) {
|
|
154
|
+
opts.onError?.(err as Error);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function processDelete(ctx: BaseCtx, filePath: string, opts: WatcherOptions): void {
|
|
159
|
+
try {
|
|
160
|
+
const row = ctx.db.prepare('SELECT rowid, id FROM vault WHERE file_path = ?').get(filePath) as
|
|
161
|
+
| { rowid: number; id: string }
|
|
162
|
+
| undefined;
|
|
163
|
+
if (!row) return;
|
|
164
|
+
|
|
165
|
+
ctx.deleteVec(row.rowid);
|
|
166
|
+
ctx.deleteCtxVec(row.rowid);
|
|
167
|
+
ctx.stmts.deleteEntry.run(row.id);
|
|
168
|
+
|
|
169
|
+
opts.onIndex?.(filePath, 'delete');
|
|
170
|
+
} catch (err) {
|
|
171
|
+
opts.onError?.(err as Error);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function startWatcher(ctx: BaseCtx, opts: WatcherOptions): VaultWatcher {
|
|
176
|
+
const debounceMs = opts.debounceMs ?? 500;
|
|
177
|
+
const maxConcurrency = opts.maxConcurrency ?? 3;
|
|
178
|
+
const maxQueueSize = opts.maxQueueSize ?? 200;
|
|
179
|
+
|
|
180
|
+
// Singleton lock: only one watcher per vault directory
|
|
181
|
+
const dataDir = opts.dataDir || dirname(opts.vaultDir);
|
|
182
|
+
const releaseLock = acquireWatcherLock(dataDir);
|
|
183
|
+
if (!releaseLock) {
|
|
184
|
+
throw new Error('Another instance is already watching this vault. Skipping watcher.');
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const pending = new Map<string, NodeJS.Timeout>();
|
|
188
|
+
const selfWrites = new Map<string, number>(); // path -> expiry timestamp
|
|
189
|
+
let activeOps = 0;
|
|
190
|
+
const queue: Array<{ fullPath: string; isDelete: boolean }> = [];
|
|
191
|
+
let watcher: FSWatcher;
|
|
192
|
+
|
|
193
|
+
function drainQueue(): void {
|
|
194
|
+
while (activeOps < maxConcurrency && queue.length > 0) {
|
|
195
|
+
const item = queue.shift()!;
|
|
196
|
+
activeOps++;
|
|
197
|
+
if (item.isDelete) {
|
|
198
|
+
processDelete(ctx, item.fullPath, opts);
|
|
199
|
+
activeOps--;
|
|
200
|
+
drainQueue();
|
|
201
|
+
} else {
|
|
202
|
+
processFile(ctx, item.fullPath, opts)
|
|
203
|
+
.finally(() => {
|
|
204
|
+
activeOps--;
|
|
205
|
+
drainQueue();
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function enqueue(fullPath: string, isDelete: boolean): void {
|
|
212
|
+
// Drop oldest if queue is full (back-pressure under bulk operations)
|
|
213
|
+
if (queue.length >= maxQueueSize) {
|
|
214
|
+
const dropped = queue.length - maxQueueSize + 1;
|
|
215
|
+
queue.splice(0, dropped);
|
|
216
|
+
}
|
|
217
|
+
queue.push({ fullPath, isDelete });
|
|
218
|
+
drainQueue();
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
try {
|
|
222
|
+
watcher = watch(opts.vaultDir, { recursive: true }, (eventType, filename) => {
|
|
223
|
+
if (!filename) return;
|
|
224
|
+
|
|
225
|
+
const relPath = filename.replace(/\\/g, '/');
|
|
226
|
+
if (extname(relPath) !== '.md') return;
|
|
227
|
+
if (isExcluded(relPath)) return;
|
|
228
|
+
|
|
229
|
+
const fullPath = join(opts.vaultDir, relPath);
|
|
230
|
+
|
|
231
|
+
// Skip self-written files (written by this MCP server via save_context)
|
|
232
|
+
const selfExpiry = selfWrites.get(fullPath);
|
|
233
|
+
if (selfExpiry && Date.now() < selfExpiry) {
|
|
234
|
+
selfWrites.delete(fullPath);
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (pending.has(fullPath)) clearTimeout(pending.get(fullPath)!);
|
|
239
|
+
pending.set(
|
|
240
|
+
fullPath,
|
|
241
|
+
setTimeout(() => {
|
|
242
|
+
pending.delete(fullPath);
|
|
243
|
+
const isDelete = !existsSync(fullPath);
|
|
244
|
+
enqueue(fullPath, isDelete);
|
|
245
|
+
}, debounceMs)
|
|
246
|
+
);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
watcher.on('error', (err) => {
|
|
250
|
+
opts.onError?.(err);
|
|
251
|
+
});
|
|
252
|
+
} catch (err) {
|
|
253
|
+
releaseLock();
|
|
254
|
+
throw new Error(`Failed to start vault watcher: ${(err as Error).message}`);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return {
|
|
258
|
+
close() {
|
|
259
|
+
for (const timer of pending.values()) clearTimeout(timer);
|
|
260
|
+
pending.clear();
|
|
261
|
+
queue.length = 0;
|
|
262
|
+
watcher.close();
|
|
263
|
+
releaseLock();
|
|
264
|
+
},
|
|
265
|
+
markSelfWrite(filePath: string) {
|
|
266
|
+
selfWrites.set(filePath, Date.now() + SELF_WRITE_TTL_MS);
|
|
267
|
+
},
|
|
268
|
+
};
|
|
269
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "context-vault",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.16.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Persistent memory for AI agents — saves and searches knowledge across sessions",
|
|
6
6
|
"bin": {
|
|
@@ -67,7 +67,7 @@
|
|
|
67
67
|
"@context-vault/core"
|
|
68
68
|
],
|
|
69
69
|
"dependencies": {
|
|
70
|
-
"@context-vault/core": "^3.
|
|
70
|
+
"@context-vault/core": "^3.16.1",
|
|
71
71
|
"@modelcontextprotocol/sdk": "^1.26.0",
|
|
72
72
|
"adm-zip": "^0.5.16",
|
|
73
73
|
"sqlite-vec": "^0.1.0"
|
package/scripts/postinstall.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import { execSync } from 'node:child_process';
|
|
13
|
-
import { existsSync, mkdirSync } from 'node:fs';
|
|
13
|
+
import { existsSync, mkdirSync, unlinkSync, readFileSync } from 'node:fs';
|
|
14
14
|
import { join, dirname } from 'node:path';
|
|
15
15
|
import { fileURLToPath } from 'node:url';
|
|
16
16
|
import { homedir } from 'node:os';
|
|
@@ -47,6 +47,31 @@ async function main() {
|
|
|
47
47
|
// ── 2. Ensure data dir exists ────────────────────────────────────────
|
|
48
48
|
const DATA_DIR = join(homedir(), '.context-mcp');
|
|
49
49
|
mkdirSync(DATA_DIR, { recursive: true });
|
|
50
|
+
|
|
51
|
+
// ── 3. Clean up legacy daemon if present ────────────────────────────
|
|
52
|
+
// v3.16.1 removed the HTTP daemon. Clean up LaunchAgent and PID file
|
|
53
|
+
// left by previous versions.
|
|
54
|
+
if (process.platform === 'darwin') {
|
|
55
|
+
const plistPath = join(homedir(), 'Library', 'LaunchAgents', 'com.context-vault.daemon.plist');
|
|
56
|
+
if (existsSync(plistPath)) {
|
|
57
|
+
try { execSync('launchctl unload "' + plistPath + '" 2>/dev/null', { stdio: 'pipe' }); } catch {}
|
|
58
|
+
try { unlinkSync(plistPath); } catch {}
|
|
59
|
+
console.log('[context-vault] Removed legacy daemon LaunchAgent.');
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
const pidPath = join(homedir(), '.context-mcp', 'daemon.pid');
|
|
63
|
+
if (existsSync(pidPath)) {
|
|
64
|
+
try {
|
|
65
|
+
const { pid } = JSON.parse(readFileSync(pidPath, 'utf-8'));
|
|
66
|
+
process.kill(pid, 'SIGTERM');
|
|
67
|
+
} catch {}
|
|
68
|
+
try { unlinkSync(pidPath); } catch {}
|
|
69
|
+
console.log('[context-vault] Removed legacy daemon PID file.');
|
|
70
|
+
}
|
|
71
|
+
const daemonLog = join(homedir(), '.context-mcp', 'daemon.log');
|
|
72
|
+
if (existsSync(daemonLog)) {
|
|
73
|
+
try { unlinkSync(daemonLog); } catch {}
|
|
74
|
+
}
|
|
50
75
|
}
|
|
51
76
|
|
|
52
77
|
main().catch(() => {});
|
package/src/error-log.ts
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import {
|
|
2
2
|
appendFileSync,
|
|
3
|
+
closeSync,
|
|
3
4
|
existsSync,
|
|
4
5
|
mkdirSync,
|
|
6
|
+
openSync,
|
|
5
7
|
readFileSync,
|
|
8
|
+
readSync,
|
|
6
9
|
statSync,
|
|
7
10
|
writeFileSync,
|
|
8
11
|
} from 'node:fs';
|
|
@@ -38,3 +41,30 @@ export function errorLogCount(dataDir: string): number {
|
|
|
38
41
|
return 0;
|
|
39
42
|
}
|
|
40
43
|
}
|
|
44
|
+
|
|
45
|
+
/** Scan the tail of error.log for embedding-related lines (support + large-vault triage). */
|
|
46
|
+
export function embedRelatedLogTail(dataDir: string, maxBytes = 16_384, maxMatches = 8): string[] {
|
|
47
|
+
try {
|
|
48
|
+
const logPath = errorLogPath(dataDir);
|
|
49
|
+
if (!existsSync(logPath)) return [];
|
|
50
|
+
const size = statSync(logPath).size;
|
|
51
|
+
let raw: string;
|
|
52
|
+
if (size <= maxBytes) {
|
|
53
|
+
raw = readFileSync(logPath, 'utf-8');
|
|
54
|
+
} else {
|
|
55
|
+
const buf = Buffer.alloc(maxBytes);
|
|
56
|
+
const fd = openSync(logPath, 'r');
|
|
57
|
+
try {
|
|
58
|
+
readSync(fd, buf, 0, maxBytes, size - maxBytes);
|
|
59
|
+
} finally {
|
|
60
|
+
closeSync(fd);
|
|
61
|
+
}
|
|
62
|
+
raw = buf.toString('utf-8');
|
|
63
|
+
}
|
|
64
|
+
const embedRe = /\bembed(ding|dings)?\b/i;
|
|
65
|
+
const lines = raw.split('\n').filter((l) => l.trim() && embedRe.test(l));
|
|
66
|
+
return lines.slice(-maxMatches);
|
|
67
|
+
} catch {
|
|
68
|
+
return [];
|
|
69
|
+
}
|
|
70
|
+
}
|
package/src/register-tools.ts
CHANGED
|
@@ -84,7 +84,11 @@ let reindexAttempts = 0;
|
|
|
84
84
|
let reindexFailed = false;
|
|
85
85
|
const MAX_REINDEX_ATTEMPTS = 2;
|
|
86
86
|
|
|
87
|
+
const registeredServers = new WeakSet<object>();
|
|
88
|
+
|
|
87
89
|
export function registerTools(server: any, ctx: LocalCtx): void {
|
|
90
|
+
if (registeredServers.has(server)) return;
|
|
91
|
+
registeredServers.add(server);
|
|
88
92
|
function tracked(
|
|
89
93
|
handler: (...args: any[]) => Promise<ToolResult>,
|
|
90
94
|
toolName: string
|