metadata.mjs
232 lines 8.1 KB
Raw
sha256:65ccb454656ea5acdea0a10e559b78bcde1eb6ff753ecc2911bc99d1c3d7cadd feat(calendar): enforce agent context tiers in retrieval AP… Human minor ⚠ breaking 17 hours ago
1 /**
2 * Structured metadata MCP resources (Issue #1 Phase A3).
3 */
4
5 import fs from 'fs';
6 import path from 'path';
7 import { createVectorStore } from '../../lib/vector-store.mjs';
8 import { listMarkdownFiles, readNote, normalizeTags, normalizeSlug } from '../../lib/vault.mjs';
9 import { getMemory, createMemoryManager } from '../../lib/memory.mjs';
10
11 const SENSITIVE_KEY = /(api[_-]?key|secret|password|token|credential|authorization|bearer)/i;
12
13 function redactWalk(obj, depth = 0) {
14 if (depth > 8 || obj == null) return obj;
15 if (typeof obj !== 'object') return obj;
16 if (Array.isArray(obj)) return obj.map((x) => redactWalk(x, depth + 1));
17 const out = {};
18 for (const [k, v] of Object.entries(obj)) {
19 if (SENSITIVE_KEY.test(k)) out[k] = '[redacted]';
20 else if (typeof v === 'object' && v !== null) out[k] = redactWalk(v, depth + 1);
21 else out[k] = v;
22 }
23 return out;
24 }
25
26 /**
27 * @param {import('../../lib/config.mjs').loadConfig extends () => infer R ? R : never} config
28 */
29 export function redactConfig(config) {
30 return redactWalk({
31 vault_path: config.vault_path,
32 data_dir: config.data_dir,
33 vector_store: config.vector_store,
34 embedding: config.embedding,
35 memory: config.memory ? { enabled: config.memory.enabled, provider: config.memory.provider } : undefined,
36 air: config.air ? { enabled: config.air.enabled } : undefined,
37 indexer: config.indexer,
38 ignore: config.ignore,
39 qdrant_configured: Boolean(config.qdrant_url),
40 });
41 }
42
43 /**
44 * @param {import('../../lib/config.mjs').loadConfig extends () => infer R ? R : never} config
45 */
46 export async function buildIndexStats(config) {
47 const mdPaths = listMarkdownFiles(config.vault_path, { ignore: config.ignore });
48 let chunksIndexed = 0;
49 let lastIndexed = null;
50 try {
51 const store = await createVectorStore(config);
52 if (typeof store.count === 'function') {
53 chunksIndexed = await store.count();
54 }
55 } catch (_) {
56 chunksIndexed = 0;
57 }
58 const dbPath = path.join(config.data_dir, 'knowtation_vectors.db');
59 try {
60 if (fs.existsSync(dbPath)) {
61 lastIndexed = fs.statSync(dbPath).mtime.toISOString();
62 }
63 } catch (_) {}
64
65 return {
66 notes_indexed: mdPaths.length,
67 chunks_indexed: chunksIndexed,
68 last_indexed: lastIndexed,
69 vector_store: config.vector_store || 'qdrant',
70 embedding_provider: config.embedding?.provider ?? null,
71 embedding_model: config.embedding?.model ?? null,
72 };
73 }
74
75 /**
76 * @param {import('../../lib/config.mjs').loadConfig extends () => infer R ? R : never} config
77 */
78 export function buildTagsResource(config) {
79 const paths = listMarkdownFiles(config.vault_path, { ignore: config.ignore });
80 const tagMap = new Map();
81 for (const p of paths) {
82 try {
83 const note = readNote(config.vault_path, p);
84 const tags = note.tags?.length ? note.tags : normalizeTags(note.frontmatter?.tags);
85 const proj = note.project || null;
86 for (const t of tags) {
87 const name = normalizeSlug(String(t));
88 if (!name) continue;
89 if (!tagMap.has(name)) tagMap.set(name, { name, count: 0, projects: new Set() });
90 const entry = tagMap.get(name);
91 entry.count += 1;
92 if (proj) entry.projects.add(proj);
93 }
94 } catch (_) {}
95 }
96 return {
97 tags: [...tagMap.values()]
98 .map((x) => ({ name: x.name, count: x.count, projects: [...x.projects].sort() }))
99 .sort((a, b) => b.count - a.count),
100 };
101 }
102
103 /**
104 * @param {import('../../lib/config.mjs').loadConfig extends () => infer R ? R : never} config
105 */
106 export function buildProjectsResource(config) {
107 const paths = listMarkdownFiles(config.vault_path, { ignore: config.ignore });
108 const byProject = new Map();
109 for (const p of paths) {
110 try {
111 const note = readNote(config.vault_path, p);
112 const slug = note.project || '_unscoped';
113 if (!byProject.has(slug)) {
114 byProject.set(slug, { slug, note_count: 0, last_updated: null });
115 }
116 const row = byProject.get(slug);
117 row.note_count += 1;
118 const d = note.date || note.updated || '';
119 if (d && (!row.last_updated || d > row.last_updated)) row.last_updated = d;
120 } catch (_) {}
121 }
122 return {
123 projects: [...byProject.values()].sort((a, b) => a.slug.localeCompare(b.slug)),
124 };
125 }
126
127 export function buildMemoryResource(config, key) {
128 if (!config.memory?.enabled) return { key, value: null, updated_at: null };
129 try {
130 const mm = createMemoryManager(config);
131 const event = mm.getLatest(key);
132 if (!event) {
133 const v = getMemory(config.data_dir, key);
134 if (!v) return { key, value: null, updated_at: null };
135 const { _at, ...rest } = v;
136 return { key, value: rest, updated_at: _at ?? null };
137 }
138 return { key, value: event.data, updated_at: event.ts, id: event.id };
139 } catch (_) {
140 const v = getMemory(config.data_dir, key);
141 if (!v) return { key, value: null, updated_at: null };
142 const { _at, ...rest } = v;
143 return { key, value: rest, updated_at: _at ?? null };
144 }
145 }
146
147 export function buildMemorySummaryResource(config) {
148 if (!config.memory?.enabled) return { enabled: false, provider: null, events: 0 };
149 try {
150 const mm = createMemoryManager(config);
151 const stats = mm.stats();
152 return {
153 enabled: true,
154 provider: config.memory.provider || 'file',
155 total_events: stats.total,
156 counts_by_type: stats.counts_by_type,
157 oldest: stats.oldest,
158 newest: stats.newest,
159 size_bytes: stats.size_bytes,
160 };
161 } catch (_) {
162 return { enabled: true, provider: config.memory.provider || 'file', total_events: 0, error: 'failed to read stats' };
163 }
164 }
165
166 export function buildMemoryEventsResource(config, limit = 50) {
167 if (!config.memory?.enabled) return { enabled: false, events: [] };
168 try {
169 const mm = createMemoryManager(config);
170 const events = mm.list({ limit });
171 return { enabled: true, events, count: events.length };
172 } catch (_) {
173 return { enabled: true, events: [], error: 'failed to read events' };
174 }
175 }
176
177 export function buildMemoryTypeResource(config, type) {
178 if (!config.memory?.enabled) return { enabled: false, type, latest: null, recent: [] };
179 try {
180 const mm = createMemoryManager(config);
181 const latest = mm.getLatest(type);
182 const recent = mm.list({ type, limit: 10 });
183 return { enabled: true, type, latest, recent, count: recent.length };
184 } catch (_) {
185 return { enabled: true, type, latest: null, recent: [], error: 'failed to read' };
186 }
187 }
188
189 /**
190 * Build the lightweight memory pointer index (markdown).
191 * @param {object} config
192 * @param {{ recentLimit?: number }} [opts]
193 * @returns {{ enabled: boolean, index: { markdown: string, generated_at: string, total_events: number, types: string[] } | null }}
194 */
195 export function buildMemoryIndexResource(config, opts = {}) {
196 if (!config.memory?.enabled) return { enabled: false, index: null };
197 try {
198 const mm = createMemoryManager(config);
199 const index = mm.generateIndex({ force: true, recentLimit: opts.recentLimit });
200 return { enabled: true, index };
201 } catch (_) {
202 return { enabled: true, index: null, error: 'failed to generate index' };
203 }
204 }
205
206 /**
207 * Build a topic-scoped memory resource: recent events for a given topic slug.
208 * @param {object} config
209 * @param {string} topicSlug
210 * @param {{ limit?: number }} [opts]
211 * @returns {{ enabled: boolean, topic: string, events: object[], count: number }}
212 */
213 export function buildMemoryTopicResource(config, topicSlug, opts = {}) {
214 if (!config.memory?.enabled) return { enabled: false, topic: topicSlug, events: [], count: 0 };
215 try {
216 const mm = createMemoryManager(config);
217 const limit = opts.limit ?? 50;
218 const events = mm.list({ topic: topicSlug, limit });
219 const topics = mm.listTopics();
220 return { enabled: true, topic: topicSlug, events, count: events.length, all_topics: topics };
221 } catch (_) {
222 return { enabled: true, topic: topicSlug, events: [], count: 0, error: 'failed to read topic events' };
223 }
224 }
225
226 export function buildAirLogResource() {
227 return {
228 entries: [],
229 status: 'no_persisted_log',
230 note: 'AIR attestation ids are not written to a log file yet; future phase may add persistence (see docs/MCP-RESOURCES-PHASE-A.md).',
231 };
232 }
File History 2 commits
sha256:65ccb454656ea5acdea0a10e559b78bcde1eb6ff753ecc2911bc99d1c3d7cadd feat(calendar): enforce agent context tiers in retrieval AP… Human minor 17 hours ago
sha256:9103f98c89257ed2b01c237cea895dabb3e85ea337dccb1161c175e4422355b6 docs: accept Calendar Events v0 spec with Phase 0 security … Human 23 hours ago