context-vault 3.4.4 → 3.5.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/assets/agent-rules.md +50 -0
- package/assets/setup-prompt.md +58 -0
- package/assets/skills/vault-setup/skill.md +81 -0
- package/bin/cli.js +753 -126
- package/dist/helpers.d.ts +2 -0
- package/dist/helpers.d.ts.map +1 -1
- package/dist/helpers.js +23 -0
- package/dist/helpers.js.map +1 -1
- package/dist/server.js +26 -2
- package/dist/server.js.map +1 -1
- package/dist/status.d.ts.map +1 -1
- package/dist/status.js +29 -0
- package/dist/status.js.map +1 -1
- package/dist/tools/context-status.d.ts.map +1 -1
- package/dist/tools/context-status.js +64 -29
- package/dist/tools/context-status.js.map +1 -1
- package/dist/tools/get-context.js +23 -18
- package/dist/tools/get-context.js.map +1 -1
- package/dist/tools/list-context.d.ts +2 -1
- package/dist/tools/list-context.d.ts.map +1 -1
- package/dist/tools/list-context.js +27 -10
- package/dist/tools/list-context.js.map +1 -1
- package/dist/tools/save-context.d.ts +2 -1
- package/dist/tools/save-context.d.ts.map +1 -1
- package/dist/tools/save-context.js +95 -26
- package/dist/tools/save-context.js.map +1 -1
- package/dist/tools/session-start.d.ts.map +1 -1
- package/dist/tools/session-start.js +230 -11
- 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 +13 -0
- package/node_modules/@context-vault/core/dist/capture.js.map +1 -1
- package/node_modules/@context-vault/core/dist/config.d.ts +8 -0
- package/node_modules/@context-vault/core/dist/config.d.ts.map +1 -1
- package/node_modules/@context-vault/core/dist/config.js +47 -2
- package/node_modules/@context-vault/core/dist/config.js.map +1 -1
- package/node_modules/@context-vault/core/dist/constants.d.ts +13 -0
- package/node_modules/@context-vault/core/dist/constants.d.ts.map +1 -1
- package/node_modules/@context-vault/core/dist/constants.js +13 -0
- package/node_modules/@context-vault/core/dist/constants.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 +73 -9
- package/node_modules/@context-vault/core/dist/db.js.map +1 -1
- package/node_modules/@context-vault/core/dist/frontmatter.d.ts.map +1 -1
- package/node_modules/@context-vault/core/dist/frontmatter.js +2 -0
- package/node_modules/@context-vault/core/dist/frontmatter.js.map +1 -1
- package/node_modules/@context-vault/core/dist/index.d.ts +4 -1
- package/node_modules/@context-vault/core/dist/index.d.ts.map +1 -1
- package/node_modules/@context-vault/core/dist/index.js +58 -10
- package/node_modules/@context-vault/core/dist/index.js.map +1 -1
- package/node_modules/@context-vault/core/dist/indexing.d.ts +8 -0
- package/node_modules/@context-vault/core/dist/indexing.d.ts.map +1 -0
- package/node_modules/@context-vault/core/dist/indexing.js +22 -0
- package/node_modules/@context-vault/core/dist/indexing.js.map +1 -0
- package/node_modules/@context-vault/core/dist/main.d.ts +3 -2
- package/node_modules/@context-vault/core/dist/main.d.ts.map +1 -1
- package/node_modules/@context-vault/core/dist/main.js +3 -1
- package/node_modules/@context-vault/core/dist/main.js.map +1 -1
- package/node_modules/@context-vault/core/dist/search.d.ts +2 -0
- package/node_modules/@context-vault/core/dist/search.d.ts.map +1 -1
- package/node_modules/@context-vault/core/dist/search.js +82 -6
- package/node_modules/@context-vault/core/dist/search.js.map +1 -1
- package/node_modules/@context-vault/core/dist/types.d.ts +24 -0
- package/node_modules/@context-vault/core/dist/types.d.ts.map +1 -1
- package/node_modules/@context-vault/core/package.json +5 -1
- package/node_modules/@context-vault/core/src/capture.ts +11 -0
- package/node_modules/@context-vault/core/src/config.ts +40 -2
- package/node_modules/@context-vault/core/src/constants.ts +15 -0
- package/node_modules/@context-vault/core/src/db.ts +73 -9
- package/node_modules/@context-vault/core/src/frontmatter.ts +2 -0
- package/node_modules/@context-vault/core/src/index.ts +65 -11
- package/node_modules/@context-vault/core/src/indexing.ts +35 -0
- package/node_modules/@context-vault/core/src/main.ts +5 -0
- package/node_modules/@context-vault/core/src/search.ts +96 -6
- package/node_modules/@context-vault/core/src/types.ts +26 -0
- package/package.json +2 -2
- package/scripts/prepack.js +17 -0
- package/src/helpers.ts +25 -0
- package/src/server.ts +28 -2
- package/src/status.ts +35 -0
- package/src/tools/context-status.ts +65 -30
- package/src/tools/get-context.ts +24 -24
- package/src/tools/list-context.ts +25 -13
- package/src/tools/save-context.ts +106 -29
- package/src/tools/session-start.ts +257 -13
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { execSync } from 'node:child_process';
|
|
3
|
-
import {
|
|
3
|
+
import { readFileSync, readdirSync, existsSync } from 'node:fs';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { homedir } from 'node:os';
|
|
6
|
+
import { ok, err, ensureVaultExists, kindIcon, fmtDate } from '../helpers.js';
|
|
4
7
|
import type { LocalCtx, SharedCtx, ToolResult } from '../types.js';
|
|
5
8
|
|
|
6
9
|
const DEFAULT_MAX_TOKENS = 4000;
|
|
@@ -9,6 +12,115 @@ const MAX_BODY_PER_ENTRY = 400;
|
|
|
9
12
|
const PRIORITY_KINDS = ['decision', 'insight', 'pattern'];
|
|
10
13
|
const SESSION_SUMMARY_KIND = 'session';
|
|
11
14
|
|
|
15
|
+
interface AutoMemoryEntry {
|
|
16
|
+
file: string;
|
|
17
|
+
name: string;
|
|
18
|
+
description: string;
|
|
19
|
+
type: string;
|
|
20
|
+
body: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface AutoMemoryResult {
|
|
24
|
+
detected: boolean;
|
|
25
|
+
path: string | null;
|
|
26
|
+
entries: AutoMemoryEntry[];
|
|
27
|
+
linesUsed: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Detect the Claude Code auto-memory directory for the current project.
|
|
32
|
+
* Convention: ~/.claude/projects/-<cwd-with-slashes-replaced-by-dashes>/memory/
|
|
33
|
+
*/
|
|
34
|
+
function detectAutoMemoryPath(): string | null {
|
|
35
|
+
try {
|
|
36
|
+
const cwd = process.cwd();
|
|
37
|
+
// Claude Code project key: absolute path with / replaced by -, leading - kept
|
|
38
|
+
const projectKey = cwd.replace(/\//g, '-');
|
|
39
|
+
const memoryDir = join(homedir(), '.claude', 'projects', projectKey, 'memory');
|
|
40
|
+
const memoryIndex = join(memoryDir, 'MEMORY.md');
|
|
41
|
+
if (existsSync(memoryIndex)) return memoryDir;
|
|
42
|
+
} catch {}
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Parse YAML-ish frontmatter from a memory file.
|
|
48
|
+
* Returns { name, description, type } and the body after frontmatter.
|
|
49
|
+
*/
|
|
50
|
+
function parseMemoryFile(content: string): { name: string; description: string; type: string; body: string } {
|
|
51
|
+
const result = { name: '', description: '', type: '', body: content };
|
|
52
|
+
const fmMatch = content.match(/^---\s*\n([\s\S]*?)\n---\s*\n([\s\S]*)$/);
|
|
53
|
+
if (!fmMatch) return result;
|
|
54
|
+
|
|
55
|
+
const frontmatter = fmMatch[1];
|
|
56
|
+
result.body = fmMatch[2].trim();
|
|
57
|
+
|
|
58
|
+
for (const line of frontmatter.split('\n')) {
|
|
59
|
+
const kv = line.match(/^(\w+)\s*:\s*(.+)$/);
|
|
60
|
+
if (!kv) continue;
|
|
61
|
+
const [, key, val] = kv;
|
|
62
|
+
if (key === 'name') result.name = val.trim();
|
|
63
|
+
else if (key === 'description') result.description = val.trim();
|
|
64
|
+
else if (key === 'type') result.type = val.trim();
|
|
65
|
+
}
|
|
66
|
+
return result;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Read and parse all auto-memory entries from a memory directory.
|
|
71
|
+
*/
|
|
72
|
+
function readAutoMemory(memoryDir: string): AutoMemoryResult {
|
|
73
|
+
const indexPath = join(memoryDir, 'MEMORY.md');
|
|
74
|
+
let linesUsed = 0;
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
const indexContent = readFileSync(indexPath, 'utf-8');
|
|
78
|
+
linesUsed = indexContent.split('\n').length;
|
|
79
|
+
} catch {
|
|
80
|
+
return { detected: true, path: memoryDir, entries: [], linesUsed: 0 };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const entries: AutoMemoryEntry[] = [];
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
const files = readdirSync(memoryDir).filter(
|
|
87
|
+
(f) => f.endsWith('.md') && f !== 'MEMORY.md'
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
for (const file of files) {
|
|
91
|
+
try {
|
|
92
|
+
const content = readFileSync(join(memoryDir, file), 'utf-8');
|
|
93
|
+
const parsed = parseMemoryFile(content);
|
|
94
|
+
entries.push({
|
|
95
|
+
file,
|
|
96
|
+
name: parsed.name || file.replace('.md', ''),
|
|
97
|
+
description: parsed.description,
|
|
98
|
+
type: parsed.type,
|
|
99
|
+
body: parsed.body,
|
|
100
|
+
});
|
|
101
|
+
} catch {}
|
|
102
|
+
}
|
|
103
|
+
} catch {}
|
|
104
|
+
|
|
105
|
+
return { detected: true, path: memoryDir, entries, linesUsed };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Build a search context string from auto-memory entries.
|
|
110
|
+
* Used to boost vault retrieval relevance.
|
|
111
|
+
*/
|
|
112
|
+
function buildAutoMemoryContext(entries: AutoMemoryEntry[]): string {
|
|
113
|
+
if (entries.length === 0) return '';
|
|
114
|
+
const parts = entries
|
|
115
|
+
.map((e) => {
|
|
116
|
+
const desc = e.description ? `: ${e.description}` : '';
|
|
117
|
+
// Include a snippet of the body for richer context
|
|
118
|
+
const bodySnippet = e.body ? ` -- ${e.body.slice(0, 200)}` : '';
|
|
119
|
+
return `${e.name}${desc}${bodySnippet}`;
|
|
120
|
+
});
|
|
121
|
+
return parts.join('. ');
|
|
122
|
+
}
|
|
123
|
+
|
|
12
124
|
export const name = 'session_start';
|
|
13
125
|
|
|
14
126
|
export const description =
|
|
@@ -67,11 +179,13 @@ function estimateTokens(text: string | null | undefined): number {
|
|
|
67
179
|
|
|
68
180
|
function formatEntry(entry: any): string {
|
|
69
181
|
const tags = entry.tags ? JSON.parse(entry.tags) : [];
|
|
70
|
-
const tagStr = tags.length ? tags.join(', ') : '
|
|
71
|
-
const date = entry.updated_at || entry.created_at
|
|
182
|
+
const tagStr = tags.length ? tags.join(', ') : '';
|
|
183
|
+
const date = fmtDate(entry.updated_at || entry.created_at);
|
|
184
|
+
const icon = kindIcon(entry.kind);
|
|
185
|
+
const meta = [`\`${entry.kind}\``, tagStr, date].filter(Boolean).join(' · ');
|
|
72
186
|
return [
|
|
73
|
-
`- **${entry.title || '(untitled)'}
|
|
74
|
-
`
|
|
187
|
+
`- ${icon} **${entry.title || '(untitled)'}**`,
|
|
188
|
+
` ${meta} · \`${entry.id}\``,
|
|
75
189
|
` ${truncateBody(entry.body).replace(/\n+/g, ' ').trim()}`,
|
|
76
190
|
].join('\n');
|
|
77
191
|
}
|
|
@@ -88,6 +202,30 @@ export async function handler(
|
|
|
88
202
|
|
|
89
203
|
await ensureIndexed();
|
|
90
204
|
|
|
205
|
+
// Sanity check: compare DB entries vs disk files
|
|
206
|
+
let indexWarning = '';
|
|
207
|
+
try {
|
|
208
|
+
const dbCount = (ctx.db.prepare('SELECT COUNT(*) as cnt FROM vault').get() as any)?.cnt ?? 0;
|
|
209
|
+
let diskCount = 0;
|
|
210
|
+
const walk = (dir: string, depth = 0) => {
|
|
211
|
+
if (depth > 3 || diskCount >= 100) return;
|
|
212
|
+
try {
|
|
213
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
214
|
+
if (diskCount >= 100) return;
|
|
215
|
+
if (entry.isDirectory() && !entry.name.startsWith('.') && entry.name !== '_archive') {
|
|
216
|
+
walk(`${dir}/${entry.name}`, depth + 1);
|
|
217
|
+
} else if (entry.name.endsWith('.md')) {
|
|
218
|
+
diskCount++;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
} catch {}
|
|
222
|
+
};
|
|
223
|
+
walk(config.vaultDir);
|
|
224
|
+
if (diskCount >= 100 && dbCount < diskCount / 10) {
|
|
225
|
+
indexWarning = `\n> **WARNING:** Vault has significantly more files on disk (~${diskCount}+) than indexed entries (${dbCount}). The search index may be out of sync. Run \`context-vault reconnect\` to fix.\n`;
|
|
226
|
+
}
|
|
227
|
+
} catch {}
|
|
228
|
+
|
|
91
229
|
const effectiveProject = project?.trim() || detectProject();
|
|
92
230
|
const tokenBudget = max_tokens || DEFAULT_MAX_TOKENS;
|
|
93
231
|
|
|
@@ -96,13 +234,23 @@ export async function handler(
|
|
|
96
234
|
|
|
97
235
|
const sinceDate = new Date(Date.now() - RECENT_DAYS * 86400000).toISOString();
|
|
98
236
|
|
|
237
|
+
// Auto-detect Claude Code auto-memory
|
|
238
|
+
const autoMemoryPath = detectAutoMemoryPath();
|
|
239
|
+
const autoMemory: AutoMemoryResult = autoMemoryPath
|
|
240
|
+
? readAutoMemory(autoMemoryPath)
|
|
241
|
+
: { detected: false, path: null, entries: [], linesUsed: 0 };
|
|
242
|
+
const autoMemoryContext = buildAutoMemoryContext(autoMemory.entries);
|
|
243
|
+
|
|
99
244
|
const sections = [];
|
|
100
245
|
let tokensUsed = 0;
|
|
101
246
|
|
|
102
|
-
sections.push(`# Session Brief${effectiveProject ? `
|
|
247
|
+
sections.push(`# Session Brief${effectiveProject ? ` -- ${effectiveProject}` : ''}`);
|
|
103
248
|
const bucketsLabel = buckets?.length ? ` | buckets: ${buckets.join(', ')}` : '';
|
|
249
|
+
const autoMemoryLabel = autoMemory.detected
|
|
250
|
+
? ` | auto-memory: ${autoMemory.entries.length} entries detected, used as search context`
|
|
251
|
+
: '';
|
|
104
252
|
sections.push(
|
|
105
|
-
`_Generated ${new Date().toISOString().slice(0, 10)} | budget: ${tokenBudget} tokens${bucketsLabel}_\n`
|
|
253
|
+
`_Generated ${new Date().toISOString().slice(0, 10)} | budget: ${tokenBudget} tokens${bucketsLabel}${autoMemoryLabel}_\n`
|
|
106
254
|
);
|
|
107
255
|
tokensUsed += estimateTokens(sections.join('\n'));
|
|
108
256
|
|
|
@@ -118,12 +266,13 @@ export async function handler(
|
|
|
118
266
|
}
|
|
119
267
|
}
|
|
120
268
|
|
|
269
|
+
// When auto-memory context is available, boost decisions query with FTS
|
|
121
270
|
const decisions = queryByKinds(
|
|
122
271
|
ctx,
|
|
123
272
|
PRIORITY_KINDS,
|
|
124
273
|
sinceDate,
|
|
125
|
-
|
|
126
|
-
|
|
274
|
+
effectiveTags,
|
|
275
|
+
autoMemoryContext
|
|
127
276
|
);
|
|
128
277
|
if (decisions.length > 0) {
|
|
129
278
|
const header = '## Active Decisions, Insights & Patterns\n';
|
|
@@ -175,6 +324,10 @@ export async function handler(
|
|
|
175
324
|
return true;
|
|
176
325
|
}).length;
|
|
177
326
|
|
|
327
|
+
if (indexWarning) {
|
|
328
|
+
sections.push(indexWarning);
|
|
329
|
+
}
|
|
330
|
+
|
|
178
331
|
sections.push('---');
|
|
179
332
|
sections.push(
|
|
180
333
|
`_${tokensUsed} / ${tokenBudget} tokens used | project: ${effectiveProject || 'unscoped'}_`
|
|
@@ -191,6 +344,11 @@ export async function handler(
|
|
|
191
344
|
decisions: decisions.length,
|
|
192
345
|
recent: deduped.length,
|
|
193
346
|
},
|
|
347
|
+
auto_memory: {
|
|
348
|
+
detected: autoMemory.detected,
|
|
349
|
+
entries: autoMemory.entries.length,
|
|
350
|
+
lines_used: autoMemory.linesUsed,
|
|
351
|
+
},
|
|
194
352
|
};
|
|
195
353
|
return result;
|
|
196
354
|
}
|
|
@@ -223,7 +381,8 @@ function queryByKinds(
|
|
|
223
381
|
ctx: LocalCtx,
|
|
224
382
|
kinds: string[],
|
|
225
383
|
since: string,
|
|
226
|
-
effectiveTags: string[]
|
|
384
|
+
effectiveTags: string[],
|
|
385
|
+
autoMemoryContext = ''
|
|
227
386
|
): any[] {
|
|
228
387
|
const kindPlaceholders = kinds.map(() => '?').join(',');
|
|
229
388
|
const clauses = [`kind IN (${kindPlaceholders})`];
|
|
@@ -238,20 +397,105 @@ function queryByKinds(
|
|
|
238
397
|
clauses.push('superseded_by IS NULL');
|
|
239
398
|
|
|
240
399
|
const where = `WHERE ${clauses.join(' AND ')}`;
|
|
241
|
-
|
|
400
|
+
let rows = ctx.db
|
|
242
401
|
.prepare(`SELECT * FROM vault ${where} ORDER BY created_at DESC LIMIT 50`)
|
|
243
|
-
.all(...params);
|
|
402
|
+
.all(...params) as any[];
|
|
244
403
|
|
|
245
404
|
if (effectiveTags.length) {
|
|
246
405
|
const tagged = rows.filter((r: any) => {
|
|
247
406
|
const tags = r.tags ? JSON.parse(r.tags) : [];
|
|
248
407
|
return effectiveTags.some((t: string) => tags.includes(t));
|
|
249
408
|
});
|
|
250
|
-
if (tagged.length > 0)
|
|
409
|
+
if (tagged.length > 0) rows = tagged;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// When auto-memory context is available, boost results by FTS relevance
|
|
413
|
+
// to surface vault entries that match what the user's auto-memory says they care about
|
|
414
|
+
if (autoMemoryContext && rows.length > 1) {
|
|
415
|
+
rows = boostByAutoMemory(ctx, rows, autoMemoryContext);
|
|
251
416
|
}
|
|
417
|
+
|
|
252
418
|
return rows;
|
|
253
419
|
}
|
|
254
420
|
|
|
421
|
+
/**
|
|
422
|
+
* Re-rank vault entries by FTS relevance to auto-memory context.
|
|
423
|
+
* Entries matching auto-memory topics float to the top while preserving
|
|
424
|
+
* all original entries (non-matching ones keep their original order at the end).
|
|
425
|
+
*/
|
|
426
|
+
function boostByAutoMemory(ctx: LocalCtx, rows: any[], context: string): any[] {
|
|
427
|
+
// Extract meaningful keywords from auto-memory context for FTS
|
|
428
|
+
const keywords = extractKeywords(context);
|
|
429
|
+
if (keywords.length === 0) return rows;
|
|
430
|
+
|
|
431
|
+
// Build FTS query from auto-memory keywords
|
|
432
|
+
const ftsTerms = keywords.slice(0, 10).map((k) => `"${k}"`).join(' OR ');
|
|
433
|
+
const rowIds = new Set(rows.map((r: any) => r.id));
|
|
434
|
+
|
|
435
|
+
try {
|
|
436
|
+
const ftsRows = ctx.db
|
|
437
|
+
.prepare(
|
|
438
|
+
`SELECT e.id, rank FROM vault_fts f JOIN vault e ON f.rowid = e.rowid WHERE vault_fts MATCH ? ORDER BY rank LIMIT 50`
|
|
439
|
+
)
|
|
440
|
+
.all(ftsTerms) as { id: string; rank: number }[];
|
|
441
|
+
|
|
442
|
+
// Build a boost map: entries matching auto-memory context get priority
|
|
443
|
+
const boostMap = new Map<string, number>();
|
|
444
|
+
for (const fr of ftsRows) {
|
|
445
|
+
if (rowIds.has(fr.id)) {
|
|
446
|
+
boostMap.set(fr.id, fr.rank);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
if (boostMap.size === 0) return rows;
|
|
451
|
+
|
|
452
|
+
// Sort: boosted entries first (by FTS rank), then unboosted in original order
|
|
453
|
+
const boosted = rows.filter((r: any) => boostMap.has(r.id));
|
|
454
|
+
const unboosted = rows.filter((r: any) => !boostMap.has(r.id));
|
|
455
|
+
boosted.sort((a: any, b: any) => (boostMap.get(a.id) ?? 0) - (boostMap.get(b.id) ?? 0));
|
|
456
|
+
return [...boosted, ...unboosted];
|
|
457
|
+
} catch {
|
|
458
|
+
// FTS errors are non-fatal; return original order
|
|
459
|
+
return rows;
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* Extract meaningful keywords from auto-memory context text.
|
|
465
|
+
* Filters out common stop words and short tokens.
|
|
466
|
+
*/
|
|
467
|
+
function extractKeywords(text: string): string[] {
|
|
468
|
+
const stopWords = new Set([
|
|
469
|
+
'the', 'a', 'an', 'is', 'are', 'was', 'were', 'be', 'been', 'being',
|
|
470
|
+
'have', 'has', 'had', 'do', 'does', 'did', 'will', 'would', 'could',
|
|
471
|
+
'should', 'may', 'might', 'shall', 'can', 'need', 'must', 'ought',
|
|
472
|
+
'and', 'but', 'or', 'nor', 'not', 'so', 'yet', 'for', 'with',
|
|
473
|
+
'from', 'into', 'during', 'before', 'after', 'above', 'below',
|
|
474
|
+
'to', 'of', 'in', 'on', 'at', 'by', 'about', 'between', 'through',
|
|
475
|
+
'this', 'that', 'these', 'those', 'it', 'its', 'they', 'them',
|
|
476
|
+
'their', 'we', 'our', 'you', 'your', 'he', 'she', 'him', 'her',
|
|
477
|
+
'all', 'each', 'every', 'both', 'few', 'more', 'most', 'other',
|
|
478
|
+
'some', 'such', 'no', 'only', 'own', 'same', 'than', 'too', 'very',
|
|
479
|
+
'just', 'also', 'now', 'then', 'here', 'there', 'when', 'where',
|
|
480
|
+
'how', 'what', 'which', 'who', 'whom', 'why', 'if', 'because',
|
|
481
|
+
'as', 'until', 'while', 'use', 'using', 'used',
|
|
482
|
+
]);
|
|
483
|
+
|
|
484
|
+
const words = text
|
|
485
|
+
.toLowerCase()
|
|
486
|
+
.replace(/[^a-z0-9\s-]/g, ' ')
|
|
487
|
+
.split(/\s+/)
|
|
488
|
+
.filter((w) => w.length >= 3 && !stopWords.has(w));
|
|
489
|
+
|
|
490
|
+
// Deduplicate while preserving order
|
|
491
|
+
const seen = new Set<string>();
|
|
492
|
+
return words.filter((w) => {
|
|
493
|
+
if (seen.has(w)) return false;
|
|
494
|
+
seen.add(w);
|
|
495
|
+
return true;
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
|
|
255
499
|
function queryRecent(ctx: LocalCtx, since: string, effectiveTags: string[]): any[] {
|
|
256
500
|
const clauses = ['created_at >= ?'];
|
|
257
501
|
const params = [since];
|