@hasna/knowledge 0.2.27 → 0.2.28
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/README.md +41 -0
- package/bin/open-knowledge-mcp.js +15 -7
- package/bin/open-knowledge.js +17 -17
- package/dist/agent.d.ts +35 -0
- package/dist/artifact-store.d.ts +63 -0
- package/dist/auth.d.ts +35 -0
- package/dist/embeddings.d.ts +77 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.js +5709 -0
- package/dist/knowledge-db.d.ts +27 -0
- package/dist/manifest-ingest.d.ts +35 -0
- package/dist/outbox-consume.d.ts +25 -0
- package/dist/provenance.d.ts +50 -0
- package/dist/providers.d.ts +89 -0
- package/dist/reindex.d.ts +37 -0
- package/dist/remote-client.d.ts +108 -0
- package/dist/retrieval.d.ts +71 -0
- package/dist/safety.d.ts +70 -0
- package/dist/sdk.d.ts +72 -0
- package/dist/search.d.ts +65 -0
- package/dist/service.d.ts +117 -0
- package/dist/source-ingest.d.ts +18 -0
- package/dist/source-ref.d.ts +30 -0
- package/dist/source-resolver.d.ts +92 -0
- package/dist/storage-contract.d.ts +106 -0
- package/dist/web-search.d.ts +40 -0
- package/dist/wiki-compiler.d.ts +67 -0
- package/dist/wiki-layout.d.ts +23 -0
- package/dist/workspace.d.ts +111 -0
- package/package.json +15 -7
- package/src/agent.ts +0 -367
- package/src/artifact-store.ts +0 -184
- package/src/auth.ts +0 -123
- package/src/cli.ts +0 -1184
- package/src/embeddings.ts +0 -516
- package/src/knowledge-db.ts +0 -354
- package/src/manifest-ingest.ts +0 -515
- package/src/mcp-http.js +0 -110
- package/src/mcp.js +0 -1503
- package/src/outbox-consume.ts +0 -463
- package/src/provenance.ts +0 -93
- package/src/providers.ts +0 -308
- package/src/reindex.ts +0 -260
- package/src/remote-client.ts +0 -268
- package/src/retrieval.ts +0 -326
- package/src/safety.ts +0 -265
- package/src/schema.js +0 -25
- package/src/search.ts +0 -510
- package/src/service.ts +0 -443
- package/src/source-ingest.ts +0 -268
- package/src/source-ref.ts +0 -104
- package/src/source-resolver.ts +0 -436
- package/src/storage-contract.ts +0 -346
- package/src/store.ts +0 -113
- package/src/web-search.ts +0 -330
- package/src/wiki-compiler.ts +0 -711
- package/src/wiki-layout.ts +0 -251
- package/src/workspace.ts +0 -251
package/src/mcp.js
DELETED
|
@@ -1,1503 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bun
|
|
2
|
-
import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
|
-
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
|
-
import { z } from 'zod';
|
|
5
|
-
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
6
|
-
import pkg from '../package.json' with { type: 'json' };
|
|
7
|
-
import { migrateKnowledgeDb, openKnowledgeDb } from './knowledge-db.ts';
|
|
8
|
-
import { defaultStorePath, loadStore, saveStore, makeId, withLock } from './store.ts';
|
|
9
|
-
import { parseSourceRef } from './source-ref.ts';
|
|
10
|
-
import { createKnowledgeService } from './service.ts';
|
|
11
|
-
|
|
12
|
-
const storePathField = z.string().optional().describe('Path to the JSON store file');
|
|
13
|
-
const scopeField = z.enum(['local', 'global', 'project']).optional().describe('Workspace scope');
|
|
14
|
-
|
|
15
|
-
function jsonText(data) {
|
|
16
|
-
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function errorText(message) {
|
|
20
|
-
return { content: [{ type: 'text', text: `Error: ${message}` }], isError: true };
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function shortIdFor(id) {
|
|
24
|
-
return id.replace(/^k_/, '').slice(0, 12);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function resolveStorePath(storePath, scope) {
|
|
28
|
-
if (storePath) return storePath;
|
|
29
|
-
if (scope === 'project' || scope === 'local') {
|
|
30
|
-
return createKnowledgeService({ scope }).jsonStorePath();
|
|
31
|
-
}
|
|
32
|
-
return defaultStorePath();
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function readStoreLocked(storePath, fn) {
|
|
36
|
-
return withLock(storePath, () => fn(loadStore(storePath)));
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function writeStoreLocked(storePath, fn) {
|
|
40
|
-
return withLock(storePath, () => {
|
|
41
|
-
const db = loadStore(storePath);
|
|
42
|
-
const result = fn(db);
|
|
43
|
-
saveStore(storePath, db);
|
|
44
|
-
return result;
|
|
45
|
-
});
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function findItem(db, id) {
|
|
49
|
-
return db.items.find((item) => item.id === id || item.short_id === id);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function sortItems(items, sort = 'created', desc = false) {
|
|
53
|
-
const sorted = [...items].sort((a, b) => {
|
|
54
|
-
if (sort === 'title') return a.title.localeCompare(b.title);
|
|
55
|
-
return a.created_at.localeCompare(b.created_at);
|
|
56
|
-
});
|
|
57
|
-
if (desc) sorted.reverse();
|
|
58
|
-
return sorted;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function activeItems(items, includeArchived) {
|
|
62
|
-
return includeArchived ? items : items.filter((item) => !item.archived);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function limitNumber(value, fallback = 20, max = 100) {
|
|
66
|
-
if (!Number.isFinite(value) || value <= 0) return fallback;
|
|
67
|
-
return Math.min(Math.floor(value), max);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
function parseJsonObject(value) {
|
|
71
|
-
if (!value) return {};
|
|
72
|
-
try {
|
|
73
|
-
const parsed = JSON.parse(value);
|
|
74
|
-
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {};
|
|
75
|
-
} catch {
|
|
76
|
-
return {};
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
function jsonResource(uri, data) {
|
|
81
|
-
return {
|
|
82
|
-
contents: [{
|
|
83
|
-
uri: uri.toString(),
|
|
84
|
-
mimeType: 'application/json',
|
|
85
|
-
text: JSON.stringify(data, null, 2),
|
|
86
|
-
}],
|
|
87
|
-
};
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
function registerTool(server, name, title, description, inputSchema, handler) {
|
|
91
|
-
server.registerTool(name, { title, description, inputSchema }, handler);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
function registerJsonResource(server, name, uri, title, description, read) {
|
|
95
|
-
server.registerResource(name, uri, {
|
|
96
|
-
title,
|
|
97
|
-
description,
|
|
98
|
-
mimeType: 'application/json',
|
|
99
|
-
}, async (resourceUri) => jsonResource(resourceUri, await read(resourceUri)));
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
function registerJsonTemplate(server, name, template, title, description, list, read) {
|
|
103
|
-
server.registerResource(name, new ResourceTemplate(template, { list }), {
|
|
104
|
-
title,
|
|
105
|
-
description,
|
|
106
|
-
mimeType: 'application/json',
|
|
107
|
-
}, async (resourceUri, variables) => jsonResource(resourceUri, await read(resourceUri, variables)));
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
function projectService() {
|
|
111
|
-
return createKnowledgeService({ scope: 'project' });
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
function openProjectDb(service = projectService()) {
|
|
115
|
-
const workspace = service.ensureWorkspace();
|
|
116
|
-
migrateKnowledgeDb(workspace.knowledgeDbPath);
|
|
117
|
-
return openKnowledgeDb(workspace.knowledgeDbPath);
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
function itemResources(storePath = createKnowledgeService({ scope: 'project' }).jsonStorePath()) {
|
|
121
|
-
return readStoreLocked(storePath, (db) => activeItems(db.items, false).slice(0, 100).map((item) => ({
|
|
122
|
-
uri: `knowledge://project/items/${encodeURIComponent(item.id)}`,
|
|
123
|
-
name: item.title,
|
|
124
|
-
description: `Knowledge item ${item.id}`,
|
|
125
|
-
mimeType: 'application/json',
|
|
126
|
-
})));
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
function listRows(db, sql, params = []) {
|
|
130
|
-
return db.query(sql).all(...params);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
function rowWithJson(row, fields = ['metadata_json', 'acl_json']) {
|
|
134
|
-
if (!row) return null;
|
|
135
|
-
const next = { ...row };
|
|
136
|
-
for (const field of fields) {
|
|
137
|
-
if (field in next) {
|
|
138
|
-
const name = field.endsWith('_json') ? field.slice(0, -5) : field;
|
|
139
|
-
next[name] = parseJsonObject(next[field]);
|
|
140
|
-
delete next[field];
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
return next;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
function dbStatsSnapshot(service = projectService()) {
|
|
147
|
-
const stats = service.dbStats();
|
|
148
|
-
const db = openProjectDb(service);
|
|
149
|
-
try {
|
|
150
|
-
return {
|
|
151
|
-
ok: true,
|
|
152
|
-
scope: 'project',
|
|
153
|
-
path: service.workspace.knowledgeDbPath,
|
|
154
|
-
stats,
|
|
155
|
-
schema_versions: listRows(db, 'SELECT version, applied_at FROM schema_versions ORDER BY version ASC'),
|
|
156
|
-
};
|
|
157
|
-
} finally {
|
|
158
|
-
db.close();
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
function storageSnapshot(service = projectService()) {
|
|
163
|
-
const validation = service.validateStorage();
|
|
164
|
-
return {
|
|
165
|
-
ok: validation.ok,
|
|
166
|
-
scope: 'project',
|
|
167
|
-
paths: service.paths(),
|
|
168
|
-
storage: service.storageContract(),
|
|
169
|
-
validation,
|
|
170
|
-
};
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
function configSnapshot(service = projectService()) {
|
|
174
|
-
return {
|
|
175
|
-
ok: true,
|
|
176
|
-
scope: 'project',
|
|
177
|
-
package: {
|
|
178
|
-
name: pkg.name,
|
|
179
|
-
version: pkg.version,
|
|
180
|
-
},
|
|
181
|
-
paths: service.paths(),
|
|
182
|
-
storage: service.storageContract(),
|
|
183
|
-
provider_status: service.providerStatus(),
|
|
184
|
-
model_registry: service.modelRegistry(),
|
|
185
|
-
};
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
function sourceRows(limit = 50, service = projectService()) {
|
|
189
|
-
const db = openProjectDb(service);
|
|
190
|
-
try {
|
|
191
|
-
return listRows(db, `
|
|
192
|
-
SELECT
|
|
193
|
-
s.id,
|
|
194
|
-
s.uri,
|
|
195
|
-
s.kind,
|
|
196
|
-
s.title,
|
|
197
|
-
s.metadata_json,
|
|
198
|
-
s.acl_json,
|
|
199
|
-
s.created_at,
|
|
200
|
-
s.updated_at,
|
|
201
|
-
COUNT(DISTINCT sr.id) AS revisions,
|
|
202
|
-
COUNT(DISTINCT c.id) AS chunks
|
|
203
|
-
FROM sources s
|
|
204
|
-
LEFT JOIN source_revisions sr ON sr.source_id = s.id
|
|
205
|
-
LEFT JOIN chunks c ON c.source_revision_id = sr.id
|
|
206
|
-
GROUP BY s.id
|
|
207
|
-
ORDER BY s.updated_at DESC
|
|
208
|
-
LIMIT ?
|
|
209
|
-
`, [limitNumber(limit, 50, 200)]).map((row) => rowWithJson(row));
|
|
210
|
-
} finally {
|
|
211
|
-
db.close();
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
function sourceSnapshot(id, { limit = 10, service = projectService() } = {}) {
|
|
216
|
-
const db = openProjectDb(service);
|
|
217
|
-
try {
|
|
218
|
-
const source = rowWithJson(db.query(`
|
|
219
|
-
SELECT id, uri, kind, title, metadata_json, acl_json, created_at, updated_at
|
|
220
|
-
FROM sources
|
|
221
|
-
WHERE id = ? OR uri = ?
|
|
222
|
-
`).get(id, id));
|
|
223
|
-
if (!source) return null;
|
|
224
|
-
const revisions = listRows(db, `
|
|
225
|
-
SELECT id, revision, hash, extracted_text_uri, metadata_json, created_at
|
|
226
|
-
FROM source_revisions
|
|
227
|
-
WHERE source_id = ?
|
|
228
|
-
ORDER BY created_at DESC
|
|
229
|
-
LIMIT ?
|
|
230
|
-
`, [source.id, limitNumber(limit, 10, 100)]).map((row) => rowWithJson(row, ['metadata_json']));
|
|
231
|
-
const chunks = listRows(db, `
|
|
232
|
-
SELECT c.id, c.kind, c.ordinal, c.text, c.token_count, c.start_offset, c.end_offset, c.metadata_json, c.created_at,
|
|
233
|
-
sr.revision, sr.hash
|
|
234
|
-
FROM chunks c
|
|
235
|
-
JOIN source_revisions sr ON sr.id = c.source_revision_id
|
|
236
|
-
WHERE sr.source_id = ?
|
|
237
|
-
ORDER BY sr.created_at DESC, c.ordinal ASC
|
|
238
|
-
LIMIT ?
|
|
239
|
-
`, [source.id, limitNumber(limit, 10, 50)]).map((row) => rowWithJson(row, ['metadata_json']));
|
|
240
|
-
return { source, revisions, chunks };
|
|
241
|
-
} finally {
|
|
242
|
-
db.close();
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
function openFilesSnapshot(service = projectService()) {
|
|
247
|
-
const db = openProjectDb(service);
|
|
248
|
-
try {
|
|
249
|
-
const rows = listRows(db, `
|
|
250
|
-
SELECT
|
|
251
|
-
s.id,
|
|
252
|
-
s.uri,
|
|
253
|
-
s.title,
|
|
254
|
-
sr.revision,
|
|
255
|
-
sr.hash,
|
|
256
|
-
c.metadata_json,
|
|
257
|
-
COUNT(c.id) AS chunks
|
|
258
|
-
FROM sources s
|
|
259
|
-
JOIN source_revisions sr ON sr.source_id = s.id
|
|
260
|
-
LEFT JOIN chunks c ON c.source_revision_id = sr.id
|
|
261
|
-
WHERE s.uri LIKE 'open-files://%'
|
|
262
|
-
GROUP BY s.id, sr.id
|
|
263
|
-
ORDER BY s.updated_at DESC
|
|
264
|
-
LIMIT 100
|
|
265
|
-
`);
|
|
266
|
-
return {
|
|
267
|
-
ok: true,
|
|
268
|
-
scope: 'project',
|
|
269
|
-
source_ownership: 'open-files',
|
|
270
|
-
raw_source_bytes_exposed: false,
|
|
271
|
-
refs: rows.map((row) => {
|
|
272
|
-
const metadata = parseJsonObject(row.metadata_json);
|
|
273
|
-
return {
|
|
274
|
-
id: row.id,
|
|
275
|
-
uri: row.uri,
|
|
276
|
-
source_ref: typeof metadata.source_ref === 'string' ? metadata.source_ref : row.uri,
|
|
277
|
-
title: row.title,
|
|
278
|
-
revision: row.revision,
|
|
279
|
-
hash: row.hash,
|
|
280
|
-
chunks: row.chunks,
|
|
281
|
-
};
|
|
282
|
-
}),
|
|
283
|
-
};
|
|
284
|
-
} finally {
|
|
285
|
-
db.close();
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
function wikiRows(limit = 50, service = projectService()) {
|
|
290
|
-
const db = openProjectDb(service);
|
|
291
|
-
try {
|
|
292
|
-
return listRows(db, `
|
|
293
|
-
SELECT id, path, title, artifact_uri, content_hash, status, metadata_json, created_at, updated_at
|
|
294
|
-
FROM wiki_pages
|
|
295
|
-
ORDER BY updated_at DESC
|
|
296
|
-
LIMIT ?
|
|
297
|
-
`, [limitNumber(limit, 50, 200)]).map((row) => rowWithJson(row, ['metadata_json']));
|
|
298
|
-
} finally {
|
|
299
|
-
db.close();
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
async function wikiSnapshot(id, { includeContent = true, service = projectService() } = {}) {
|
|
304
|
-
const db = openProjectDb(service);
|
|
305
|
-
try {
|
|
306
|
-
const page = rowWithJson(db.query(`
|
|
307
|
-
SELECT id, path, title, artifact_uri, content_hash, status, metadata_json, created_at, updated_at
|
|
308
|
-
FROM wiki_pages
|
|
309
|
-
WHERE id = ? OR path = ?
|
|
310
|
-
`).get(id, id), ['metadata_json']);
|
|
311
|
-
if (!page) return null;
|
|
312
|
-
const citations = listRows(db, `
|
|
313
|
-
SELECT id, chunk_id, source_uri, quote, start_offset, end_offset, metadata_json, created_at
|
|
314
|
-
FROM citations
|
|
315
|
-
WHERE wiki_page_id = ?
|
|
316
|
-
ORDER BY created_at ASC
|
|
317
|
-
LIMIT 100
|
|
318
|
-
`, [page.id]).map((row) => rowWithJson(row, ['metadata_json']));
|
|
319
|
-
let content = null;
|
|
320
|
-
if (includeContent) {
|
|
321
|
-
const artifactKey = page.metadata?.artifact_key ?? page.path;
|
|
322
|
-
if (typeof artifactKey === 'string') {
|
|
323
|
-
try {
|
|
324
|
-
content = await service.artifactStore().getText(artifactKey);
|
|
325
|
-
} catch {
|
|
326
|
-
content = null;
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
return { page, citations, content };
|
|
331
|
-
} finally {
|
|
332
|
-
db.close();
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
function indexRows(limit = 50, service = projectService()) {
|
|
337
|
-
const db = openProjectDb(service);
|
|
338
|
-
try {
|
|
339
|
-
return listRows(db, `
|
|
340
|
-
SELECT id, kind, name, artifact_uri, shard_key, metadata_json, created_at, updated_at
|
|
341
|
-
FROM knowledge_indexes
|
|
342
|
-
ORDER BY updated_at DESC
|
|
343
|
-
LIMIT ?
|
|
344
|
-
`, [limitNumber(limit, 50, 200)]).map((row) => rowWithJson(row, ['metadata_json']));
|
|
345
|
-
} finally {
|
|
346
|
-
db.close();
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
function indexSnapshot(id, service = projectService()) {
|
|
351
|
-
const db = openProjectDb(service);
|
|
352
|
-
try {
|
|
353
|
-
const index = rowWithJson(db.query(`
|
|
354
|
-
SELECT id, kind, name, artifact_uri, shard_key, metadata_json, created_at, updated_at
|
|
355
|
-
FROM knowledge_indexes
|
|
356
|
-
WHERE id = ? OR name = ? OR shard_key = ?
|
|
357
|
-
`).get(id, id, id), ['metadata_json']);
|
|
358
|
-
if (!index) return null;
|
|
359
|
-
const vector_counts = listRows(db, `
|
|
360
|
-
SELECT provider, model, dimensions, status, COUNT(*) AS entries
|
|
361
|
-
FROM vector_index_entries
|
|
362
|
-
GROUP BY provider, model, dimensions, status
|
|
363
|
-
ORDER BY entries DESC
|
|
364
|
-
LIMIT 50
|
|
365
|
-
`);
|
|
366
|
-
return { index, vector_counts };
|
|
367
|
-
} finally {
|
|
368
|
-
db.close();
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
function runRows(limit = 50, service = projectService()) {
|
|
373
|
-
const db = openProjectDb(service);
|
|
374
|
-
try {
|
|
375
|
-
return listRows(db, `
|
|
376
|
-
SELECT id, type, prompt, status, provider, model, cost_tokens, cost_usd, metadata_json, created_at, updated_at
|
|
377
|
-
FROM runs
|
|
378
|
-
ORDER BY updated_at DESC
|
|
379
|
-
LIMIT ?
|
|
380
|
-
`, [limitNumber(limit, 50, 200)]).map((row) => rowWithJson(row, ['metadata_json']));
|
|
381
|
-
} finally {
|
|
382
|
-
db.close();
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
function runSnapshot(id, { limit = 50, service = projectService() } = {}) {
|
|
387
|
-
const db = openProjectDb(service);
|
|
388
|
-
try {
|
|
389
|
-
const run = rowWithJson(db.query(`
|
|
390
|
-
SELECT id, type, prompt, status, provider, model, cost_tokens, cost_usd, metadata_json, created_at, updated_at
|
|
391
|
-
FROM runs
|
|
392
|
-
WHERE id = ?
|
|
393
|
-
`).get(id), ['metadata_json']);
|
|
394
|
-
if (!run) return null;
|
|
395
|
-
const events = listRows(db, `
|
|
396
|
-
SELECT id, level, event, metadata_json, created_at
|
|
397
|
-
FROM run_events
|
|
398
|
-
WHERE run_id = ?
|
|
399
|
-
ORDER BY created_at ASC
|
|
400
|
-
LIMIT ?
|
|
401
|
-
`, [id, limitNumber(limit, 50, 200)]).map((row) => rowWithJson(row, ['metadata_json']));
|
|
402
|
-
const usage = listRows(db, `
|
|
403
|
-
SELECT id, provider, model, input_tokens, output_tokens, cost_usd, metadata_json, created_at
|
|
404
|
-
FROM provider_usage
|
|
405
|
-
WHERE run_id = ?
|
|
406
|
-
ORDER BY created_at ASC
|
|
407
|
-
LIMIT 100
|
|
408
|
-
`, [id]).map((row) => rowWithJson(row, ['metadata_json']));
|
|
409
|
-
return { run, events, usage };
|
|
410
|
-
} finally {
|
|
411
|
-
db.close();
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
function decisionsSnapshot(limit = 50, service = projectService()) {
|
|
416
|
-
const db = openProjectDb(service);
|
|
417
|
-
try {
|
|
418
|
-
return {
|
|
419
|
-
ok: true,
|
|
420
|
-
scope: 'project',
|
|
421
|
-
approval_gates: listRows(db, `
|
|
422
|
-
SELECT id, action, target_uri, status, reason, approved_by, metadata_json, created_at, updated_at
|
|
423
|
-
FROM approval_gates
|
|
424
|
-
ORDER BY updated_at DESC
|
|
425
|
-
LIMIT ?
|
|
426
|
-
`, [limitNumber(limit, 50, 200)]).map((row) => rowWithJson(row, ['metadata_json'])),
|
|
427
|
-
audit_events: listRows(db, `
|
|
428
|
-
SELECT id, event_type, action, target_uri, decision, metadata_json, created_at
|
|
429
|
-
FROM audit_events
|
|
430
|
-
ORDER BY created_at DESC
|
|
431
|
-
LIMIT ?
|
|
432
|
-
`, [limitNumber(limit, 50, 200)]).map((row) => rowWithJson(row, ['metadata_json'])),
|
|
433
|
-
};
|
|
434
|
-
} finally {
|
|
435
|
-
db.close();
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
function decisionSnapshot(id, service = projectService()) {
|
|
440
|
-
const db = openProjectDb(service);
|
|
441
|
-
try {
|
|
442
|
-
const approval = rowWithJson(db.query(`
|
|
443
|
-
SELECT id, action, target_uri, status, reason, approved_by, metadata_json, created_at, updated_at
|
|
444
|
-
FROM approval_gates
|
|
445
|
-
WHERE id = ? OR target_uri = ?
|
|
446
|
-
`).get(id, id), ['metadata_json']);
|
|
447
|
-
if (approval) return { kind: 'approval_gate', decision: approval };
|
|
448
|
-
const audit = rowWithJson(db.query(`
|
|
449
|
-
SELECT id, event_type, action, target_uri, decision, metadata_json, created_at
|
|
450
|
-
FROM audit_events
|
|
451
|
-
WHERE id = ? OR target_uri = ?
|
|
452
|
-
`).get(id, id), ['metadata_json']);
|
|
453
|
-
return audit ? { kind: 'audit_event', decision: audit } : null;
|
|
454
|
-
} finally {
|
|
455
|
-
db.close();
|
|
456
|
-
}
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
async function getKnowledgeRecord(kind, id, options = {}) {
|
|
460
|
-
const normalized = kind ?? 'auto';
|
|
461
|
-
const service = createKnowledgeService({ scope: options.scope });
|
|
462
|
-
const attempts = normalized === 'auto'
|
|
463
|
-
? ['item', 'source', 'wiki_page', 'run', 'index', 'decision']
|
|
464
|
-
: [normalized];
|
|
465
|
-
|
|
466
|
-
for (const entry of attempts) {
|
|
467
|
-
if (entry === 'item') {
|
|
468
|
-
const storePath = resolveStorePath(options.store_path, options.scope);
|
|
469
|
-
const item = readStoreLocked(storePath, (db) => findItem(db, id));
|
|
470
|
-
if (item) return { kind: 'item', item, store_path: storePath };
|
|
471
|
-
}
|
|
472
|
-
if (entry === 'source') {
|
|
473
|
-
const source = sourceSnapshot(id, { limit: options.limit, service });
|
|
474
|
-
if (source) return { kind: 'source', ...source };
|
|
475
|
-
}
|
|
476
|
-
if (entry === 'wiki_page') {
|
|
477
|
-
const page = await wikiSnapshot(id, { includeContent: options.include_content !== false, service });
|
|
478
|
-
if (page) return { kind: 'wiki_page', ...page };
|
|
479
|
-
}
|
|
480
|
-
if (entry === 'run') {
|
|
481
|
-
const run = runSnapshot(id, { limit: options.limit, service });
|
|
482
|
-
if (run) return { kind: 'run', ...run };
|
|
483
|
-
}
|
|
484
|
-
if (entry === 'index') {
|
|
485
|
-
const index = indexSnapshot(id, service);
|
|
486
|
-
if (index) return { kind: 'index', ...index };
|
|
487
|
-
}
|
|
488
|
-
if (entry === 'decision') {
|
|
489
|
-
const decision = decisionSnapshot(id, service);
|
|
490
|
-
if (decision) return { kind: 'decision', ...decision };
|
|
491
|
-
}
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
return null;
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
function registerKnowledgeResources(server) {
|
|
498
|
-
registerJsonResource(
|
|
499
|
-
server,
|
|
500
|
-
'knowledge-project-config',
|
|
501
|
-
'knowledge://project/config',
|
|
502
|
-
'Project knowledge config',
|
|
503
|
-
'Resolved project workspace config, provider registry, and storage contract',
|
|
504
|
-
async () => configSnapshot(),
|
|
505
|
-
);
|
|
506
|
-
registerJsonResource(
|
|
507
|
-
server,
|
|
508
|
-
'knowledge-project-storage',
|
|
509
|
-
'knowledge://project/storage',
|
|
510
|
-
'Project knowledge storage',
|
|
511
|
-
'Artifact storage contract and validation for project knowledge',
|
|
512
|
-
async () => storageSnapshot(),
|
|
513
|
-
);
|
|
514
|
-
registerJsonResource(
|
|
515
|
-
server,
|
|
516
|
-
'knowledge-project-schema',
|
|
517
|
-
'knowledge://project/schema',
|
|
518
|
-
'Project knowledge schema',
|
|
519
|
-
'SQLite schema version and table counts for project knowledge',
|
|
520
|
-
async () => dbStatsSnapshot(),
|
|
521
|
-
);
|
|
522
|
-
registerJsonResource(
|
|
523
|
-
server,
|
|
524
|
-
'knowledge-project-sources',
|
|
525
|
-
'knowledge://project/sources',
|
|
526
|
-
'Project knowledge sources',
|
|
527
|
-
'Indexed source refs and revision/chunk counts without raw source bytes',
|
|
528
|
-
async () => ({ ok: true, scope: 'project', sources: sourceRows() }),
|
|
529
|
-
);
|
|
530
|
-
registerJsonResource(
|
|
531
|
-
server,
|
|
532
|
-
'knowledge-project-open-files',
|
|
533
|
-
'knowledge://project/open-files',
|
|
534
|
-
'Project open-files refs',
|
|
535
|
-
'Open-files source refs known to the project knowledge catalog',
|
|
536
|
-
async () => openFilesSnapshot(),
|
|
537
|
-
);
|
|
538
|
-
registerJsonResource(
|
|
539
|
-
server,
|
|
540
|
-
'knowledge-project-wiki-pages',
|
|
541
|
-
'knowledge://project/wiki/pages',
|
|
542
|
-
'Project wiki pages',
|
|
543
|
-
'Generated wiki pages and citation artifact metadata',
|
|
544
|
-
async () => ({ ok: true, scope: 'project', pages: wikiRows() }),
|
|
545
|
-
);
|
|
546
|
-
registerJsonResource(
|
|
547
|
-
server,
|
|
548
|
-
'knowledge-project-indexes',
|
|
549
|
-
'knowledge://project/indexes',
|
|
550
|
-
'Project knowledge indexes',
|
|
551
|
-
'Sharded knowledge indexes and vector-index status',
|
|
552
|
-
async () => ({
|
|
553
|
-
ok: true,
|
|
554
|
-
scope: 'project',
|
|
555
|
-
indexes: indexRows(),
|
|
556
|
-
embeddings: projectService().embeddingStatus(),
|
|
557
|
-
}),
|
|
558
|
-
);
|
|
559
|
-
registerJsonResource(
|
|
560
|
-
server,
|
|
561
|
-
'knowledge-project-runs',
|
|
562
|
-
'knowledge://project/runs',
|
|
563
|
-
'Project knowledge runs',
|
|
564
|
-
'Recent prompt, ingestion, web search, and reindex run ledger entries',
|
|
565
|
-
async () => ({ ok: true, scope: 'project', runs: runRows() }),
|
|
566
|
-
);
|
|
567
|
-
registerJsonResource(
|
|
568
|
-
server,
|
|
569
|
-
'knowledge-project-decisions',
|
|
570
|
-
'knowledge://project/decisions',
|
|
571
|
-
'Project knowledge decisions',
|
|
572
|
-
'Approval gates and audit decisions for generated knowledge operations',
|
|
573
|
-
async () => decisionsSnapshot(),
|
|
574
|
-
);
|
|
575
|
-
|
|
576
|
-
registerJsonTemplate(
|
|
577
|
-
server,
|
|
578
|
-
'knowledge-project-items',
|
|
579
|
-
'knowledge://project/items/{id}',
|
|
580
|
-
'Project knowledge item',
|
|
581
|
-
'Read a compatibility JSON-store item by id',
|
|
582
|
-
async () => ({ resources: itemResources() }),
|
|
583
|
-
async (_uri, variables) => {
|
|
584
|
-
const id = decodeURIComponent(String(variables.id));
|
|
585
|
-
const record = await getKnowledgeRecord('item', id, { scope: 'project' });
|
|
586
|
-
return record ? { ok: true, ...record } : { ok: false, error: `Item not found: ${id}` };
|
|
587
|
-
},
|
|
588
|
-
);
|
|
589
|
-
registerJsonTemplate(
|
|
590
|
-
server,
|
|
591
|
-
'knowledge-project-source',
|
|
592
|
-
'knowledge://project/sources/{id}',
|
|
593
|
-
'Project source',
|
|
594
|
-
'Read indexed source metadata, revisions, and derived chunks',
|
|
595
|
-
async () => ({
|
|
596
|
-
resources: sourceRows().map((source) => ({
|
|
597
|
-
uri: `knowledge://project/sources/${encodeURIComponent(source.id)}`,
|
|
598
|
-
name: source.title ?? source.uri,
|
|
599
|
-
description: `${source.kind} source with ${source.chunks} chunk(s)`,
|
|
600
|
-
mimeType: 'application/json',
|
|
601
|
-
})),
|
|
602
|
-
}),
|
|
603
|
-
async (_uri, variables) => {
|
|
604
|
-
const id = decodeURIComponent(String(variables.id));
|
|
605
|
-
const record = sourceSnapshot(id);
|
|
606
|
-
return record ? { ok: true, kind: 'source', ...record } : { ok: false, error: `Source not found: ${id}` };
|
|
607
|
-
},
|
|
608
|
-
);
|
|
609
|
-
registerJsonTemplate(
|
|
610
|
-
server,
|
|
611
|
-
'knowledge-project-wiki-page',
|
|
612
|
-
'knowledge://project/wiki/pages/{id}',
|
|
613
|
-
'Project wiki page',
|
|
614
|
-
'Read generated wiki page metadata, citations, and artifact text',
|
|
615
|
-
async () => ({
|
|
616
|
-
resources: wikiRows().map((page) => ({
|
|
617
|
-
uri: `knowledge://project/wiki/pages/${encodeURIComponent(page.id)}`,
|
|
618
|
-
name: page.title,
|
|
619
|
-
description: page.path,
|
|
620
|
-
mimeType: 'application/json',
|
|
621
|
-
})),
|
|
622
|
-
}),
|
|
623
|
-
async (_uri, variables) => {
|
|
624
|
-
const id = decodeURIComponent(String(variables.id));
|
|
625
|
-
const record = await wikiSnapshot(id);
|
|
626
|
-
return record ? { ok: true, kind: 'wiki_page', ...record } : { ok: false, error: `Wiki page not found: ${id}` };
|
|
627
|
-
},
|
|
628
|
-
);
|
|
629
|
-
registerJsonTemplate(
|
|
630
|
-
server,
|
|
631
|
-
'knowledge-project-index',
|
|
632
|
-
'knowledge://project/indexes/{id}',
|
|
633
|
-
'Project knowledge index',
|
|
634
|
-
'Read a knowledge index row and vector-count snapshot',
|
|
635
|
-
async () => ({
|
|
636
|
-
resources: indexRows().map((index) => ({
|
|
637
|
-
uri: `knowledge://project/indexes/${encodeURIComponent(index.id)}`,
|
|
638
|
-
name: index.name,
|
|
639
|
-
description: `${index.kind} index${index.shard_key ? ` shard ${index.shard_key}` : ''}`,
|
|
640
|
-
mimeType: 'application/json',
|
|
641
|
-
})),
|
|
642
|
-
}),
|
|
643
|
-
async (_uri, variables) => {
|
|
644
|
-
const id = decodeURIComponent(String(variables.id));
|
|
645
|
-
const record = indexSnapshot(id);
|
|
646
|
-
return record ? { ok: true, kind: 'index', ...record } : { ok: false, error: `Index not found: ${id}` };
|
|
647
|
-
},
|
|
648
|
-
);
|
|
649
|
-
registerJsonTemplate(
|
|
650
|
-
server,
|
|
651
|
-
'knowledge-project-run',
|
|
652
|
-
'knowledge://project/runs/{id}',
|
|
653
|
-
'Project run',
|
|
654
|
-
'Read a knowledge run ledger entry with events and usage',
|
|
655
|
-
async () => ({
|
|
656
|
-
resources: runRows().map((run) => ({
|
|
657
|
-
uri: `knowledge://project/runs/${encodeURIComponent(run.id)}`,
|
|
658
|
-
name: `${run.type}: ${run.status}`,
|
|
659
|
-
description: run.prompt ?? run.id,
|
|
660
|
-
mimeType: 'application/json',
|
|
661
|
-
})),
|
|
662
|
-
}),
|
|
663
|
-
async (_uri, variables) => {
|
|
664
|
-
const id = decodeURIComponent(String(variables.id));
|
|
665
|
-
const record = runSnapshot(id);
|
|
666
|
-
return record ? { ok: true, kind: 'run', ...record } : { ok: false, error: `Run not found: ${id}` };
|
|
667
|
-
},
|
|
668
|
-
);
|
|
669
|
-
registerJsonTemplate(
|
|
670
|
-
server,
|
|
671
|
-
'knowledge-project-decision',
|
|
672
|
-
'knowledge://project/decisions/{id}',
|
|
673
|
-
'Project decision',
|
|
674
|
-
'Read an approval gate or audit decision',
|
|
675
|
-
async () => {
|
|
676
|
-
const decisions = decisionsSnapshot();
|
|
677
|
-
return {
|
|
678
|
-
resources: [
|
|
679
|
-
...decisions.approval_gates.map((entry) => ({
|
|
680
|
-
uri: `knowledge://project/decisions/${encodeURIComponent(entry.id)}`,
|
|
681
|
-
name: `${entry.action}: ${entry.status}`,
|
|
682
|
-
description: entry.target_uri ?? entry.id,
|
|
683
|
-
mimeType: 'application/json',
|
|
684
|
-
})),
|
|
685
|
-
...decisions.audit_events.map((entry) => ({
|
|
686
|
-
uri: `knowledge://project/decisions/${encodeURIComponent(entry.id)}`,
|
|
687
|
-
name: `${entry.action}: ${entry.decision}`,
|
|
688
|
-
description: entry.target_uri ?? entry.id,
|
|
689
|
-
mimeType: 'application/json',
|
|
690
|
-
})),
|
|
691
|
-
],
|
|
692
|
-
};
|
|
693
|
-
},
|
|
694
|
-
async (_uri, variables) => {
|
|
695
|
-
const id = decodeURIComponent(String(variables.id));
|
|
696
|
-
const record = decisionSnapshot(id);
|
|
697
|
-
return record ? { ok: true, ...record } : { ok: false, error: `Decision not found: ${id}` };
|
|
698
|
-
},
|
|
699
|
-
);
|
|
700
|
-
}
|
|
701
|
-
|
|
702
|
-
export function buildServer() {
|
|
703
|
-
const server = new McpServer({
|
|
704
|
-
name: 'open-knowledge',
|
|
705
|
-
version: pkg.version,
|
|
706
|
-
});
|
|
707
|
-
|
|
708
|
-
registerKnowledgeResources(server);
|
|
709
|
-
|
|
710
|
-
registerTool(server, 'ok_paths', 'Knowledge workspace paths', 'Show resolved workspace and store paths', {
|
|
711
|
-
scope: scopeField,
|
|
712
|
-
}, async ({ scope }) => {
|
|
713
|
-
return jsonText(createKnowledgeService({ scope }).paths());
|
|
714
|
-
});
|
|
715
|
-
|
|
716
|
-
registerTool(server, 'ok_storage_status', 'Knowledge storage status', 'Inspect local/S3 artifact storage, source ownership, and scalability contract', {
|
|
717
|
-
scope: scopeField,
|
|
718
|
-
}, async ({ scope }) => {
|
|
719
|
-
const service = createKnowledgeService({ scope });
|
|
720
|
-
const validation = service.validateStorage();
|
|
721
|
-
return jsonText({
|
|
722
|
-
ok: validation.ok,
|
|
723
|
-
...service.storageContract(),
|
|
724
|
-
validation,
|
|
725
|
-
});
|
|
726
|
-
});
|
|
727
|
-
|
|
728
|
-
registerTool(server, 'ok_parse_source_ref', 'Parse source reference', 'Parse and validate an open-files, S3, file, or web source ref', {
|
|
729
|
-
uri: z.string().describe('Source reference URI'),
|
|
730
|
-
}, async ({ uri }) => {
|
|
731
|
-
try {
|
|
732
|
-
return jsonText({ ok: true, source_ref: parseSourceRef(uri) });
|
|
733
|
-
} catch (error) {
|
|
734
|
-
return errorText(error instanceof Error ? error.message : String(error));
|
|
735
|
-
}
|
|
736
|
-
});
|
|
737
|
-
|
|
738
|
-
registerTool(server, 'ok_resolve_source', 'Resolve source content', 'Resolve an indexed source ref through the read-only open-files boundary and return chunk citation evidence', {
|
|
739
|
-
source_ref: z.string().describe('Source reference URI, preferably open-files://...'),
|
|
740
|
-
purpose: z.string().optional().describe('Read-only purpose label, default knowledge_answer'),
|
|
741
|
-
limit: z.number().optional().describe('Maximum chunks to return, default 10'),
|
|
742
|
-
scope: scopeField,
|
|
743
|
-
}, async ({ source_ref, purpose, limit, scope }) => {
|
|
744
|
-
const service = createKnowledgeService({ scope });
|
|
745
|
-
try {
|
|
746
|
-
const result = await service.resolveSource(source_ref, {
|
|
747
|
-
purpose,
|
|
748
|
-
limit,
|
|
749
|
-
});
|
|
750
|
-
return jsonText({ ok: true, ...result });
|
|
751
|
-
} catch (error) {
|
|
752
|
-
return errorText(error instanceof Error ? error.message : String(error));
|
|
753
|
-
}
|
|
754
|
-
});
|
|
755
|
-
|
|
756
|
-
registerTool(server, 'ok_provider_status', 'AI provider status', 'Inspect configured AI SDK providers, model aliases, and BYOK credential availability', {
|
|
757
|
-
scope: scopeField,
|
|
758
|
-
}, async ({ scope }) => {
|
|
759
|
-
const service = createKnowledgeService({ scope });
|
|
760
|
-
return jsonText({ ok: true, ...service.providerStatus() });
|
|
761
|
-
});
|
|
762
|
-
|
|
763
|
-
registerTool(server, 'ok_provider_models', 'AI provider models', 'List AI SDK model aliases and capability metadata', {
|
|
764
|
-
scope: scopeField,
|
|
765
|
-
}, async ({ scope }) => {
|
|
766
|
-
const service = createKnowledgeService({ scope });
|
|
767
|
-
return jsonText({ ok: true, models: service.modelRegistry() });
|
|
768
|
-
});
|
|
769
|
-
|
|
770
|
-
registerTool(server, 'ok_embeddings_status', 'Embedding index status', 'Inspect local embedding/vector index counts by provider and model', {
|
|
771
|
-
scope: scopeField,
|
|
772
|
-
}, async ({ scope }) => {
|
|
773
|
-
const service = createKnowledgeService({ scope });
|
|
774
|
-
return jsonText({ ok: true, ...service.embeddingStatus() });
|
|
775
|
-
});
|
|
776
|
-
|
|
777
|
-
registerTool(server, 'ok_embeddings_index', 'Index embeddings', 'Embed unindexed knowledge chunks into the local vector index', {
|
|
778
|
-
scope: scopeField,
|
|
779
|
-
limit: z.number().optional().describe('Maximum chunks to embed'),
|
|
780
|
-
model: z.string().optional().describe('Embedding model ref, default openai:text-embedding-3-small'),
|
|
781
|
-
dimensions: z.number().optional().describe('Embedding dimensions for deterministic fake mode'),
|
|
782
|
-
fake: z.boolean().optional().describe('Use deterministic fake embeddings for local tests'),
|
|
783
|
-
}, async ({ scope, limit, model, dimensions, fake }) => {
|
|
784
|
-
const service = createKnowledgeService({ scope });
|
|
785
|
-
try {
|
|
786
|
-
return jsonText({ ok: true, ...await service.indexEmbeddings({ limit, modelRef: model, dimensions, fake }) });
|
|
787
|
-
} catch (error) {
|
|
788
|
-
return errorText(error instanceof Error ? error.message : String(error));
|
|
789
|
-
}
|
|
790
|
-
});
|
|
791
|
-
|
|
792
|
-
registerTool(server, 'ok_reindex_status', 'Reindex status', 'Inspect missing embeddings, queued jobs, stale revisions, and vector index health', {
|
|
793
|
-
scope: scopeField,
|
|
794
|
-
model: z.string().optional().describe('Embedding model ref, default openai:text-embedding-3-small'),
|
|
795
|
-
dimensions: z.number().optional().describe('Embedding dimensions for deterministic fake mode'),
|
|
796
|
-
fake: z.boolean().optional().describe('Use deterministic fake embeddings for local tests'),
|
|
797
|
-
}, async ({ scope, model, dimensions, fake }) => {
|
|
798
|
-
const service = createKnowledgeService({ scope });
|
|
799
|
-
try {
|
|
800
|
-
return jsonText({ ok: true, ...service.reindexHealth({ modelRef: model, dimensions, fake }) });
|
|
801
|
-
} catch (error) {
|
|
802
|
-
return errorText(error instanceof Error ? error.message : String(error));
|
|
803
|
-
}
|
|
804
|
-
});
|
|
805
|
-
|
|
806
|
-
registerTool(server, 'ok_reindex_enqueue', 'Enqueue reindex work', 'Queue missing embedding refresh jobs for indexed source chunks', {
|
|
807
|
-
scope: scopeField,
|
|
808
|
-
model: z.string().optional().describe('Embedding model ref, default openai:text-embedding-3-small'),
|
|
809
|
-
dimensions: z.number().optional().describe('Embedding dimensions for deterministic fake mode'),
|
|
810
|
-
fake: z.boolean().optional().describe('Use deterministic fake embeddings for local tests'),
|
|
811
|
-
}, async ({ scope, model, dimensions, fake }) => {
|
|
812
|
-
const service = createKnowledgeService({ scope });
|
|
813
|
-
try {
|
|
814
|
-
return jsonText({ ok: true, ...service.enqueueReindex({ modelRef: model, dimensions, fake }) });
|
|
815
|
-
} catch (error) {
|
|
816
|
-
return errorText(error instanceof Error ? error.message : String(error));
|
|
817
|
-
}
|
|
818
|
-
});
|
|
819
|
-
|
|
820
|
-
registerTool(server, 'ok_reindex_embeddings', 'Refresh embedding index', 'Run incremental or full embedding refresh jobs with run-ledger tracking', {
|
|
821
|
-
scope: scopeField,
|
|
822
|
-
full: z.boolean().optional().describe('Delete and rebuild all embedding/vector rows first'),
|
|
823
|
-
limit: z.number().optional().describe('Maximum chunks to embed'),
|
|
824
|
-
model: z.string().optional().describe('Embedding model ref, default openai:text-embedding-3-small'),
|
|
825
|
-
dimensions: z.number().optional().describe('Embedding dimensions for deterministic fake mode'),
|
|
826
|
-
fake: z.boolean().optional().describe('Use deterministic fake embeddings for local tests'),
|
|
827
|
-
}, async ({ scope, full, limit, model, dimensions, fake }) => {
|
|
828
|
-
const service = createKnowledgeService({ scope });
|
|
829
|
-
try {
|
|
830
|
-
return jsonText({ ok: true, ...await service.refreshEmbeddings({ full, limit, modelRef: model, dimensions, fake }) });
|
|
831
|
-
} catch (error) {
|
|
832
|
-
return errorText(error instanceof Error ? error.message : String(error));
|
|
833
|
-
}
|
|
834
|
-
});
|
|
835
|
-
|
|
836
|
-
registerTool(server, 'ok_semantic_search', 'Semantic search', 'Search the local vector index and return cited chunks with provenance', {
|
|
837
|
-
scope: scopeField,
|
|
838
|
-
query: z.string().describe('Semantic query'),
|
|
839
|
-
limit: z.number().optional().describe('Maximum results'),
|
|
840
|
-
model: z.string().optional().describe('Embedding model ref, default openai:text-embedding-3-small'),
|
|
841
|
-
dimensions: z.number().optional().describe('Embedding dimensions for deterministic fake mode'),
|
|
842
|
-
fake: z.boolean().optional().describe('Use deterministic fake embeddings for local tests'),
|
|
843
|
-
}, async ({ scope, query, limit, model, dimensions, fake }) => {
|
|
844
|
-
const service = createKnowledgeService({ scope });
|
|
845
|
-
try {
|
|
846
|
-
return jsonText({ ok: true, ...await service.semanticSearch({ query, limit, modelRef: model, dimensions, fake }) });
|
|
847
|
-
} catch (error) {
|
|
848
|
-
return errorText(error instanceof Error ? error.message : String(error));
|
|
849
|
-
}
|
|
850
|
-
});
|
|
851
|
-
|
|
852
|
-
registerTool(server, 'ok_search', 'Hybrid knowledge search', 'Search source chunks, generated wiki pages, sharded indexes, and optional semantic vectors', {
|
|
853
|
-
scope: scopeField,
|
|
854
|
-
query: z.string().describe('Search query'),
|
|
855
|
-
limit: z.number().optional().describe('Maximum results'),
|
|
856
|
-
semantic: z.boolean().optional().describe('Include vector semantic results'),
|
|
857
|
-
model: z.string().optional().describe('Embedding model ref, default openai:text-embedding-3-small'),
|
|
858
|
-
dimensions: z.number().optional().describe('Embedding dimensions for deterministic fake mode'),
|
|
859
|
-
fake: z.boolean().optional().describe('Use deterministic fake embeddings for local tests'),
|
|
860
|
-
}, async ({ scope, query, limit, semantic, model, dimensions, fake }) => {
|
|
861
|
-
const service = createKnowledgeService({ scope });
|
|
862
|
-
try {
|
|
863
|
-
return jsonText({ ok: true, ...await service.search({ query, limit, semantic, modelRef: model, dimensions, fake }) });
|
|
864
|
-
} catch (error) {
|
|
865
|
-
return errorText(error instanceof Error ? error.message : String(error));
|
|
866
|
-
}
|
|
867
|
-
});
|
|
868
|
-
|
|
869
|
-
registerTool(server, 'knowledge_search', 'Knowledge context search', 'Return a reranked citation context pack for agent prompts', {
|
|
870
|
-
scope: scopeField,
|
|
871
|
-
query: z.string().describe('Search query or prompt'),
|
|
872
|
-
limit: z.number().optional().describe('Maximum context results'),
|
|
873
|
-
semantic: z.boolean().optional().describe('Include vector semantic results'),
|
|
874
|
-
model: z.string().optional().describe('Embedding model ref, default openai:text-embedding-3-small'),
|
|
875
|
-
dimensions: z.number().optional().describe('Embedding dimensions for deterministic fake mode'),
|
|
876
|
-
fake: z.boolean().optional().describe('Use deterministic fake embeddings for local tests'),
|
|
877
|
-
}, async ({ scope, query, limit, semantic, model, dimensions, fake }) => {
|
|
878
|
-
const service = createKnowledgeService({ scope });
|
|
879
|
-
try {
|
|
880
|
-
return jsonText({ ok: true, ...await service.retrieveContext({ query, limit, semantic, modelRef: model, dimensions, fake }) });
|
|
881
|
-
} catch (error) {
|
|
882
|
-
return errorText(error instanceof Error ? error.message : String(error));
|
|
883
|
-
}
|
|
884
|
-
});
|
|
885
|
-
|
|
886
|
-
registerTool(server, 'knowledge_ask', 'Knowledge prompt answer', 'Answer a prompt using read-only knowledge context and optional AI SDK generation', {
|
|
887
|
-
scope: scopeField,
|
|
888
|
-
prompt: z.string().describe('Prompt to answer with the knowledge base'),
|
|
889
|
-
limit: z.number().optional().describe('Maximum context results'),
|
|
890
|
-
semantic: z.boolean().optional().describe('Include vector semantic results'),
|
|
891
|
-
generate: z.boolean().optional().describe('Call AI SDK text generation; omitted returns a local citation draft'),
|
|
892
|
-
approve_write: z.boolean().optional().describe('Record approval intent for future durable wiki writes'),
|
|
893
|
-
model: z.string().optional().describe('Model alias/ref, default configured provider default'),
|
|
894
|
-
dimensions: z.number().optional().describe('Embedding dimensions for deterministic fake mode'),
|
|
895
|
-
fake: z.boolean().optional().describe('Use deterministic fake embeddings/generation for local tests'),
|
|
896
|
-
}, async ({ scope, prompt, limit, semantic, generate, approve_write, model, dimensions, fake }) => {
|
|
897
|
-
const service = createKnowledgeService({ scope });
|
|
898
|
-
try {
|
|
899
|
-
return jsonText({ ok: true, ...await service.runPrompt({ prompt, limit, semantic, generate, approveWrite: approve_write, modelRef: model, dimensions, fake }) });
|
|
900
|
-
} catch (error) {
|
|
901
|
-
return errorText(error instanceof Error ? error.message : String(error));
|
|
902
|
-
}
|
|
903
|
-
});
|
|
904
|
-
|
|
905
|
-
registerTool(server, 'knowledge_get', 'Get knowledge record', 'Read a knowledge item, indexed source, wiki page, run, index, or decision by id without raw source-byte access', {
|
|
906
|
-
scope: scopeField,
|
|
907
|
-
kind: z.enum(['auto', 'item', 'source', 'wiki_page', 'run', 'index', 'decision']).optional().describe('Record kind; auto tries all supported kinds'),
|
|
908
|
-
id: z.string().describe('Record id, short id, source URI, wiki path, index shard/name, or decision target URI'),
|
|
909
|
-
include_content: z.boolean().optional().describe('Include generated wiki artifact text when reading wiki pages'),
|
|
910
|
-
limit: z.number().optional().describe('Maximum related chunks/events to return'),
|
|
911
|
-
store_path: storePathField,
|
|
912
|
-
}, async ({ scope, kind, id, include_content, limit, store_path }) => {
|
|
913
|
-
try {
|
|
914
|
-
const record = await getKnowledgeRecord(kind ?? 'auto', id, {
|
|
915
|
-
scope,
|
|
916
|
-
include_content,
|
|
917
|
-
limit,
|
|
918
|
-
store_path,
|
|
919
|
-
});
|
|
920
|
-
return record ? jsonText({ ok: true, ...record }) : errorText(`Knowledge record not found: ${id}`);
|
|
921
|
-
} catch (error) {
|
|
922
|
-
return errorText(error instanceof Error ? error.message : String(error));
|
|
923
|
-
}
|
|
924
|
-
});
|
|
925
|
-
|
|
926
|
-
registerTool(server, 'knowledge_ingest', 'Ingest knowledge source', 'Ingest an open-files/S3/file/web source ref or open-files manifest into the derived knowledge catalog', {
|
|
927
|
-
scope: scopeField,
|
|
928
|
-
source_ref: z.string().optional().describe('Source reference URI to ingest, e.g. open-files://file/<id>/revision/<rev>'),
|
|
929
|
-
manifest: z.string().optional().describe('Manifest file path or s3:// URI to ingest'),
|
|
930
|
-
purpose: z.string().optional().describe('Read-only purpose label, default knowledge_answer'),
|
|
931
|
-
}, async ({ scope, source_ref, manifest, purpose }) => {
|
|
932
|
-
if (!source_ref && !manifest) return errorText('Missing input. Provide source_ref or manifest.');
|
|
933
|
-
if (source_ref && manifest) return errorText('Use either source_ref or manifest, not both.');
|
|
934
|
-
const service = createKnowledgeService({ scope });
|
|
935
|
-
try {
|
|
936
|
-
const result = source_ref
|
|
937
|
-
? await service.ingestSource(source_ref, purpose)
|
|
938
|
-
: await service.ingestManifest(manifest);
|
|
939
|
-
return jsonText({ ok: true, mode: source_ref ? 'source' : 'manifest', ...result });
|
|
940
|
-
} catch (error) {
|
|
941
|
-
return errorText(error instanceof Error ? error.message : String(error));
|
|
942
|
-
}
|
|
943
|
-
});
|
|
944
|
-
|
|
945
|
-
registerTool(server, 'knowledge_build', 'Build knowledge answer', 'Run the knowledge prompt flow and optionally file the cited answer into generated wiki artifacts after approval', {
|
|
946
|
-
scope: scopeField,
|
|
947
|
-
prompt: z.string().describe('Prompt to answer and build durable knowledge from'),
|
|
948
|
-
limit: z.number().optional().describe('Maximum context results'),
|
|
949
|
-
semantic: z.boolean().optional().describe('Include vector semantic results'),
|
|
950
|
-
generate: z.boolean().optional().describe('Call AI SDK text generation; omitted returns a local citation draft'),
|
|
951
|
-
approve_write: z.boolean().optional().describe('Approve durable wiki filing for this call'),
|
|
952
|
-
file_answer: z.boolean().optional().describe('Attempt wiki answer filing; writes only with approve_write=true'),
|
|
953
|
-
model: z.string().optional().describe('Model alias/ref, default configured provider default'),
|
|
954
|
-
dimensions: z.number().optional().describe('Embedding dimensions for deterministic fake mode'),
|
|
955
|
-
fake: z.boolean().optional().describe('Use deterministic fake embeddings/generation for local tests'),
|
|
956
|
-
}, async ({ scope, prompt, limit, semantic, generate, approve_write, file_answer, model, dimensions, fake }) => {
|
|
957
|
-
const service = createKnowledgeService({ scope });
|
|
958
|
-
try {
|
|
959
|
-
const result = await service.runPrompt({ prompt, limit, semantic, generate, approveWrite: approve_write, modelRef: model, dimensions, fake });
|
|
960
|
-
let wiki_file = null;
|
|
961
|
-
if (file_answer === true || approve_write === true) {
|
|
962
|
-
wiki_file = await service.fileAnswer({
|
|
963
|
-
prompt,
|
|
964
|
-
answer: result.answer,
|
|
965
|
-
approveWrite: approve_write,
|
|
966
|
-
limit,
|
|
967
|
-
semantic,
|
|
968
|
-
modelRef: model,
|
|
969
|
-
dimensions,
|
|
970
|
-
fake,
|
|
971
|
-
});
|
|
972
|
-
}
|
|
973
|
-
return jsonText({ ok: true, ...result, wiki_file });
|
|
974
|
-
} catch (error) {
|
|
975
|
-
return errorText(error instanceof Error ? error.message : String(error));
|
|
976
|
-
}
|
|
977
|
-
});
|
|
978
|
-
|
|
979
|
-
registerTool(server, 'knowledge_web_search', 'Knowledge web search', 'Run safety-gated provider-native web search and optionally file snippets as web source refs', {
|
|
980
|
-
scope: scopeField,
|
|
981
|
-
query: z.string().describe('Web search query'),
|
|
982
|
-
limit: z.number().optional().describe('Maximum sources'),
|
|
983
|
-
provider: z.enum(['openai', 'anthropic', 'deepseek']).optional().describe('Provider override'),
|
|
984
|
-
model: z.string().optional().describe('Model alias/ref'),
|
|
985
|
-
domains: z.array(z.string()).optional().describe('Allowed domains'),
|
|
986
|
-
fake: z.boolean().optional().describe('Use deterministic fake web results'),
|
|
987
|
-
file_results: z.boolean().optional().describe('File web snippets as web source refs'),
|
|
988
|
-
}, async ({ scope, query, limit, provider, model, domains, fake, file_results }) => {
|
|
989
|
-
const service = createKnowledgeService({ scope });
|
|
990
|
-
try {
|
|
991
|
-
return jsonText({ ok: true, ...await service.webSearch({ query, limit, provider, modelRef: model, domains, fake, fileResults: file_results }) });
|
|
992
|
-
} catch (error) {
|
|
993
|
-
return errorText(error instanceof Error ? error.message : String(error));
|
|
994
|
-
}
|
|
995
|
-
});
|
|
996
|
-
|
|
997
|
-
registerTool(server, 'knowledge_lint', 'Lint knowledge wiki', 'Check generated wiki pages for missing citations, stale citations, duplicates, or source issues', {
|
|
998
|
-
scope: scopeField,
|
|
999
|
-
}, async ({ scope }) => {
|
|
1000
|
-
const service = createKnowledgeService({ scope });
|
|
1001
|
-
try {
|
|
1002
|
-
return jsonText({ ok: true, ...service.lintWiki() });
|
|
1003
|
-
} catch (error) {
|
|
1004
|
-
return errorText(error instanceof Error ? error.message : String(error));
|
|
1005
|
-
}
|
|
1006
|
-
});
|
|
1007
|
-
|
|
1008
|
-
registerTool(server, 'knowledge_run_status', 'Knowledge run status', 'List recent runs or inspect one run ledger with events and provider usage', {
|
|
1009
|
-
scope: scopeField,
|
|
1010
|
-
run_id: z.string().optional().describe('Run id to inspect; omitted lists recent runs'),
|
|
1011
|
-
limit: z.number().optional().describe('Maximum runs or events to return'),
|
|
1012
|
-
}, async ({ scope, run_id, limit }) => {
|
|
1013
|
-
const service = createKnowledgeService({ scope });
|
|
1014
|
-
try {
|
|
1015
|
-
if (run_id) {
|
|
1016
|
-
const run = runSnapshot(run_id, { limit, service });
|
|
1017
|
-
return run ? jsonText({ ok: true, kind: 'run', ...run }) : errorText(`Run not found: ${run_id}`);
|
|
1018
|
-
}
|
|
1019
|
-
return jsonText({ ok: true, runs: runRows(limit, service) });
|
|
1020
|
-
} catch (error) {
|
|
1021
|
-
return errorText(error instanceof Error ? error.message : String(error));
|
|
1022
|
-
}
|
|
1023
|
-
});
|
|
1024
|
-
|
|
1025
|
-
registerTool(server, 'knowledge_storage', 'Knowledge storage contract', 'Inspect local/S3 artifact storage, source ownership, and hosted/SaaS boundary metadata', {
|
|
1026
|
-
scope: scopeField,
|
|
1027
|
-
}, async ({ scope }) => {
|
|
1028
|
-
const service = createKnowledgeService({ scope });
|
|
1029
|
-
try {
|
|
1030
|
-
const validation = service.validateStorage();
|
|
1031
|
-
return jsonText({
|
|
1032
|
-
ok: validation.ok,
|
|
1033
|
-
...service.storageContract(),
|
|
1034
|
-
validation,
|
|
1035
|
-
remote_contract: service.remoteContract(),
|
|
1036
|
-
});
|
|
1037
|
-
} catch (error) {
|
|
1038
|
-
return errorText(error instanceof Error ? error.message : String(error));
|
|
1039
|
-
}
|
|
1040
|
-
});
|
|
1041
|
-
|
|
1042
|
-
registerTool(server, 'knowledge_resolve_source', 'Resolve knowledge source', 'Resolve indexed source chunks through the read-only open-files/source boundary with citation evidence', {
|
|
1043
|
-
source_ref: z.string().describe('Source reference URI, preferably open-files://...'),
|
|
1044
|
-
purpose: z.string().optional().describe('Read-only purpose label, default knowledge_answer'),
|
|
1045
|
-
limit: z.number().optional().describe('Maximum chunks to return, default 10'),
|
|
1046
|
-
scope: scopeField,
|
|
1047
|
-
}, async ({ source_ref, purpose, limit, scope }) => {
|
|
1048
|
-
const service = createKnowledgeService({ scope });
|
|
1049
|
-
try {
|
|
1050
|
-
const result = await service.resolveSource(source_ref, { purpose, limit });
|
|
1051
|
-
return jsonText({ ok: true, ...result });
|
|
1052
|
-
} catch (error) {
|
|
1053
|
-
return errorText(error instanceof Error ? error.message : String(error));
|
|
1054
|
-
}
|
|
1055
|
-
});
|
|
1056
|
-
|
|
1057
|
-
registerTool(server, 'ok_web_search', 'Provider web search', 'Run safety-gated provider-native web search and return citations/sources', {
|
|
1058
|
-
scope: scopeField,
|
|
1059
|
-
query: z.string().describe('Web search query'),
|
|
1060
|
-
limit: z.number().optional().describe('Maximum sources'),
|
|
1061
|
-
provider: z.enum(['openai', 'anthropic', 'deepseek']).optional().describe('Provider override'),
|
|
1062
|
-
model: z.string().optional().describe('Model alias/ref'),
|
|
1063
|
-
domains: z.array(z.string()).optional().describe('Allowed domains'),
|
|
1064
|
-
fake: z.boolean().optional().describe('Use deterministic fake web results'),
|
|
1065
|
-
file_results: z.boolean().optional().describe('File web snippets as web source refs'),
|
|
1066
|
-
}, async ({ scope, query, limit, provider, model, domains, fake, file_results }) => {
|
|
1067
|
-
const service = createKnowledgeService({ scope });
|
|
1068
|
-
try {
|
|
1069
|
-
return jsonText({ ok: true, ...await service.webSearch({ query, limit, provider, modelRef: model, domains, fake, fileResults: file_results }) });
|
|
1070
|
-
} catch (error) {
|
|
1071
|
-
return errorText(error instanceof Error ? error.message : String(error));
|
|
1072
|
-
}
|
|
1073
|
-
});
|
|
1074
|
-
|
|
1075
|
-
registerTool(server, 'ok_add', 'Add a knowledge item', 'Add a new item to the knowledge store', {
|
|
1076
|
-
title: z.string().describe('Item title'),
|
|
1077
|
-
content: z.string().describe('Item content/body'),
|
|
1078
|
-
tags: z.array(z.string()).optional().describe('Tags to attach'),
|
|
1079
|
-
metadata: z.record(z.string(), z.unknown()).optional().describe('Metadata key-value pairs'),
|
|
1080
|
-
url: z.string().optional().describe('Source URL or URI'),
|
|
1081
|
-
store_path: storePathField,
|
|
1082
|
-
scope: scopeField,
|
|
1083
|
-
}, async ({ title, content, tags, metadata, url, store_path, scope }) => {
|
|
1084
|
-
const storePath = resolveStorePath(store_path, scope);
|
|
1085
|
-
const item = writeStoreLocked(storePath, (db) => {
|
|
1086
|
-
const now = new Date().toISOString();
|
|
1087
|
-
const id = makeId();
|
|
1088
|
-
const entry = {
|
|
1089
|
-
id,
|
|
1090
|
-
short_id: shortIdFor(id),
|
|
1091
|
-
title,
|
|
1092
|
-
content,
|
|
1093
|
-
url: url ?? null,
|
|
1094
|
-
tags: tags ?? [],
|
|
1095
|
-
metadata: metadata ?? {},
|
|
1096
|
-
archived: false,
|
|
1097
|
-
created_at: now,
|
|
1098
|
-
updated_at: now,
|
|
1099
|
-
};
|
|
1100
|
-
db.items.push(entry);
|
|
1101
|
-
return entry;
|
|
1102
|
-
});
|
|
1103
|
-
return jsonText({ ok: true, item, message: `Added ${item.id}` });
|
|
1104
|
-
});
|
|
1105
|
-
|
|
1106
|
-
registerTool(server, 'ok_list', 'List knowledge items', 'List items with pagination, search, tag filtering, and sorting', {
|
|
1107
|
-
search: z.string().optional().describe('Search text for title/content'),
|
|
1108
|
-
tag: z.array(z.string()).optional().describe('Filter by tags; item must match all tags'),
|
|
1109
|
-
include_archived: z.boolean().optional().describe('Include archived items'),
|
|
1110
|
-
page: z.number().optional().describe('Page number'),
|
|
1111
|
-
limit: z.number().optional().describe('Items per page'),
|
|
1112
|
-
sort: z.enum(['created', 'title']).optional().describe('Sort field'),
|
|
1113
|
-
desc: z.boolean().optional().describe('Sort descending'),
|
|
1114
|
-
store_path: storePathField,
|
|
1115
|
-
scope: scopeField,
|
|
1116
|
-
}, async ({ search, tag, include_archived, page, limit, sort, desc, store_path, scope }) => {
|
|
1117
|
-
const storePath = resolveStorePath(store_path, scope);
|
|
1118
|
-
return readStoreLocked(storePath, (db) => {
|
|
1119
|
-
const q = search ? search.toLowerCase() : '';
|
|
1120
|
-
const requiredTags = (tag ?? []).map((entry) => entry.toLowerCase());
|
|
1121
|
-
let items = activeItems(db.items, include_archived);
|
|
1122
|
-
if (q) items = items.filter((item) => item.title.toLowerCase().includes(q) || item.content.toLowerCase().includes(q));
|
|
1123
|
-
if (requiredTags.length > 0) {
|
|
1124
|
-
items = items.filter((item) => {
|
|
1125
|
-
const itemTags = (item.tags ?? []).map((entry) => entry.toLowerCase());
|
|
1126
|
-
return requiredTags.every((entry) => itemTags.includes(entry));
|
|
1127
|
-
});
|
|
1128
|
-
}
|
|
1129
|
-
const p = page && page > 0 ? page : 1;
|
|
1130
|
-
const l = limit && limit > 0 ? limit : 20;
|
|
1131
|
-
const sorted = sortItems(items, sort ?? 'created', desc ?? false);
|
|
1132
|
-
const start = (p - 1) * l;
|
|
1133
|
-
const rows = sorted.slice(start, start + l);
|
|
1134
|
-
return jsonText({
|
|
1135
|
-
ok: true,
|
|
1136
|
-
page: p,
|
|
1137
|
-
limit: l,
|
|
1138
|
-
total: sorted.length,
|
|
1139
|
-
total_pages: Math.max(1, Math.ceil(sorted.length / l)),
|
|
1140
|
-
items: rows,
|
|
1141
|
-
});
|
|
1142
|
-
});
|
|
1143
|
-
});
|
|
1144
|
-
|
|
1145
|
-
registerTool(server, 'ok_get', 'Get a knowledge item', 'Retrieve a single item by ID or short ID', {
|
|
1146
|
-
id: z.string().describe('Item ID or short ID'),
|
|
1147
|
-
store_path: storePathField,
|
|
1148
|
-
scope: scopeField,
|
|
1149
|
-
}, async ({ id, store_path, scope }) => {
|
|
1150
|
-
const storePath = resolveStorePath(store_path, scope);
|
|
1151
|
-
return readStoreLocked(storePath, (db) => {
|
|
1152
|
-
const item = findItem(db, id);
|
|
1153
|
-
return item ? jsonText({ ok: true, item }) : errorText(`Item not found: ${id}`);
|
|
1154
|
-
});
|
|
1155
|
-
});
|
|
1156
|
-
|
|
1157
|
-
registerTool(server, 'ok_update', 'Update a knowledge item', 'Update title, content, URL, tags, or metadata', {
|
|
1158
|
-
id: z.string().describe('Item ID or short ID'),
|
|
1159
|
-
title: z.string().optional(),
|
|
1160
|
-
content: z.string().optional(),
|
|
1161
|
-
url: z.string().optional(),
|
|
1162
|
-
tags: z.array(z.string()).optional(),
|
|
1163
|
-
metadata: z.record(z.string(), z.unknown()).optional(),
|
|
1164
|
-
store_path: storePathField,
|
|
1165
|
-
scope: scopeField,
|
|
1166
|
-
}, async ({ id, title, content, url, tags, metadata, store_path, scope }) => {
|
|
1167
|
-
const storePath = resolveStorePath(store_path, scope);
|
|
1168
|
-
const result = writeStoreLocked(storePath, (db) => {
|
|
1169
|
-
const item = findItem(db, id);
|
|
1170
|
-
if (!item) return null;
|
|
1171
|
-
if (title !== undefined) item.title = title;
|
|
1172
|
-
if (content !== undefined) item.content = content;
|
|
1173
|
-
if (url !== undefined) item.url = url;
|
|
1174
|
-
if (tags) item.tags = [...new Set([...(item.tags ?? []), ...tags])];
|
|
1175
|
-
if (metadata) item.metadata = { ...(item.metadata ?? {}), ...metadata };
|
|
1176
|
-
item.updated_at = new Date().toISOString();
|
|
1177
|
-
return item;
|
|
1178
|
-
});
|
|
1179
|
-
return result ? jsonText({ ok: true, item: result }) : errorText(`Item not found: ${id}`);
|
|
1180
|
-
});
|
|
1181
|
-
|
|
1182
|
-
registerTool(server, 'ok_delete', 'Delete a knowledge item', 'Permanently delete an item by ID. Requires confirm=true.', {
|
|
1183
|
-
id: z.string().describe('Item ID or short ID'),
|
|
1184
|
-
confirm: z.boolean().describe('Must be true to confirm deletion'),
|
|
1185
|
-
store_path: storePathField,
|
|
1186
|
-
scope: scopeField,
|
|
1187
|
-
}, async ({ id, confirm, store_path, scope }) => {
|
|
1188
|
-
if (!confirm) return errorText('Refusing delete without confirm=true.');
|
|
1189
|
-
const storePath = resolveStorePath(store_path, scope);
|
|
1190
|
-
const deleted = writeStoreLocked(storePath, (db) => {
|
|
1191
|
-
const before = db.items.length;
|
|
1192
|
-
db.items = db.items.filter((item) => item.id !== id && item.short_id !== id);
|
|
1193
|
-
return before !== db.items.length;
|
|
1194
|
-
});
|
|
1195
|
-
return deleted ? jsonText({ ok: true, deleted_id: id }) : errorText(`Item not found: ${id}`);
|
|
1196
|
-
});
|
|
1197
|
-
|
|
1198
|
-
registerTool(server, 'ok_archive', 'Archive a knowledge item', 'Soft-delete an item by setting archived=true', {
|
|
1199
|
-
id: z.string(),
|
|
1200
|
-
store_path: storePathField,
|
|
1201
|
-
scope: scopeField,
|
|
1202
|
-
}, async ({ id, store_path, scope }) => {
|
|
1203
|
-
const storePath = resolveStorePath(store_path, scope);
|
|
1204
|
-
const item = writeStoreLocked(storePath, (db) => {
|
|
1205
|
-
const entry = findItem(db, id);
|
|
1206
|
-
if (!entry) return null;
|
|
1207
|
-
entry.archived = true;
|
|
1208
|
-
entry.updated_at = new Date().toISOString();
|
|
1209
|
-
return entry;
|
|
1210
|
-
});
|
|
1211
|
-
return item ? jsonText({ ok: true, item }) : errorText(`Item not found: ${id}`);
|
|
1212
|
-
});
|
|
1213
|
-
|
|
1214
|
-
registerTool(server, 'ok_restore', 'Restore a knowledge item', 'Restore an archived item', {
|
|
1215
|
-
id: z.string(),
|
|
1216
|
-
store_path: storePathField,
|
|
1217
|
-
scope: scopeField,
|
|
1218
|
-
}, async ({ id, store_path, scope }) => {
|
|
1219
|
-
const storePath = resolveStorePath(store_path, scope);
|
|
1220
|
-
const item = writeStoreLocked(storePath, (db) => {
|
|
1221
|
-
const entry = findItem(db, id);
|
|
1222
|
-
if (!entry) return null;
|
|
1223
|
-
entry.archived = false;
|
|
1224
|
-
entry.updated_at = new Date().toISOString();
|
|
1225
|
-
return entry;
|
|
1226
|
-
});
|
|
1227
|
-
return item ? jsonText({ ok: true, item }) : errorText(`Item not found: ${id}`);
|
|
1228
|
-
});
|
|
1229
|
-
|
|
1230
|
-
registerTool(server, 'ok_upsert', 'Upsert a knowledge item', 'Create or update an item by ID', {
|
|
1231
|
-
id: z.string(),
|
|
1232
|
-
title: z.string().optional(),
|
|
1233
|
-
content: z.string().optional(),
|
|
1234
|
-
tags: z.array(z.string()).optional(),
|
|
1235
|
-
metadata: z.record(z.string(), z.unknown()).optional(),
|
|
1236
|
-
store_path: storePathField,
|
|
1237
|
-
scope: scopeField,
|
|
1238
|
-
}, async ({ id, title, content, tags, metadata, store_path, scope }) => {
|
|
1239
|
-
const storePath = resolveStorePath(store_path, scope);
|
|
1240
|
-
const item = writeStoreLocked(storePath, (db) => {
|
|
1241
|
-
let entry = findItem(db, id);
|
|
1242
|
-
const now = new Date().toISOString();
|
|
1243
|
-
if (!entry) {
|
|
1244
|
-
if (!title || !content) return null;
|
|
1245
|
-
entry = {
|
|
1246
|
-
id,
|
|
1247
|
-
short_id: shortIdFor(id),
|
|
1248
|
-
title,
|
|
1249
|
-
content,
|
|
1250
|
-
tags: tags ?? [],
|
|
1251
|
-
metadata: metadata ?? {},
|
|
1252
|
-
archived: false,
|
|
1253
|
-
created_at: now,
|
|
1254
|
-
updated_at: now,
|
|
1255
|
-
};
|
|
1256
|
-
db.items.push(entry);
|
|
1257
|
-
return entry;
|
|
1258
|
-
}
|
|
1259
|
-
if (title !== undefined) entry.title = title;
|
|
1260
|
-
if (content !== undefined) entry.content = content;
|
|
1261
|
-
if (tags) entry.tags = [...new Set([...(entry.tags ?? []), ...tags])];
|
|
1262
|
-
if (metadata) entry.metadata = { ...(entry.metadata ?? {}), ...metadata };
|
|
1263
|
-
entry.updated_at = now;
|
|
1264
|
-
return entry;
|
|
1265
|
-
});
|
|
1266
|
-
return item ? jsonText({ ok: true, item }) : errorText('New item requires both title and content.');
|
|
1267
|
-
});
|
|
1268
|
-
|
|
1269
|
-
registerTool(server, 'ok_untag', 'Remove tags from a knowledge item', 'Remove specific tags from an item', {
|
|
1270
|
-
id: z.string(),
|
|
1271
|
-
tags: z.array(z.string()),
|
|
1272
|
-
store_path: storePathField,
|
|
1273
|
-
scope: scopeField,
|
|
1274
|
-
}, async ({ id, tags, store_path, scope }) => {
|
|
1275
|
-
const storePath = resolveStorePath(store_path, scope);
|
|
1276
|
-
const result = writeStoreLocked(storePath, (db) => {
|
|
1277
|
-
const item = findItem(db, id);
|
|
1278
|
-
if (!item) return null;
|
|
1279
|
-
const remove = new Set(tags.map((tag) => tag.toLowerCase()));
|
|
1280
|
-
const before = (item.tags ?? []).length;
|
|
1281
|
-
item.tags = (item.tags ?? []).filter((tag) => !remove.has(tag.toLowerCase()));
|
|
1282
|
-
item.updated_at = new Date().toISOString();
|
|
1283
|
-
return { item, removed: before - item.tags.length };
|
|
1284
|
-
});
|
|
1285
|
-
return result ? jsonText({ ok: true, ...result }) : errorText(`Item not found: ${id}`);
|
|
1286
|
-
});
|
|
1287
|
-
|
|
1288
|
-
registerTool(server, 'ok_bulk_delete', 'Bulk delete knowledge items', 'Delete multiple items by tag or search. Requires confirm=true.', {
|
|
1289
|
-
tag: z.array(z.string()).optional(),
|
|
1290
|
-
search: z.string().optional(),
|
|
1291
|
-
confirm: z.boolean(),
|
|
1292
|
-
store_path: storePathField,
|
|
1293
|
-
scope: scopeField,
|
|
1294
|
-
}, async ({ tag, search, confirm, store_path, scope }) => {
|
|
1295
|
-
if (!confirm) return errorText('Refusing bulk delete without confirm=true.');
|
|
1296
|
-
if (!tag && !search) return errorText('Missing filter. Use tag or search.');
|
|
1297
|
-
const storePath = resolveStorePath(store_path, scope);
|
|
1298
|
-
const deleted = writeStoreLocked(storePath, (db) => {
|
|
1299
|
-
const q = search ? search.toLowerCase() : '';
|
|
1300
|
-
const tags = (tag ?? []).map((entry) => entry.toLowerCase());
|
|
1301
|
-
const deleteIds = new Set(db.items.filter((item) => {
|
|
1302
|
-
const matchesSearch = q ? item.title.toLowerCase().includes(q) || item.content.toLowerCase().includes(q) : false;
|
|
1303
|
-
const itemTags = (item.tags ?? []).map((entry) => entry.toLowerCase());
|
|
1304
|
-
const matchesTag = tags.length > 0 ? tags.some((entry) => itemTags.includes(entry)) : false;
|
|
1305
|
-
return matchesSearch || matchesTag;
|
|
1306
|
-
}).map((item) => item.id));
|
|
1307
|
-
db.items = db.items.filter((item) => !deleteIds.has(item.id));
|
|
1308
|
-
return deleteIds.size;
|
|
1309
|
-
});
|
|
1310
|
-
return jsonText({ ok: true, deleted });
|
|
1311
|
-
});
|
|
1312
|
-
|
|
1313
|
-
registerTool(server, 'ok_prune', 'Prune knowledge items', 'Remove old and/or empty knowledge items. Requires confirm=true.', {
|
|
1314
|
-
older_than_days: z.number().optional().describe('Remove items older than N days'),
|
|
1315
|
-
empty: z.boolean().optional().describe('Remove items with empty content'),
|
|
1316
|
-
confirm: z.boolean(),
|
|
1317
|
-
store_path: storePathField,
|
|
1318
|
-
scope: scopeField,
|
|
1319
|
-
}, async ({ older_than_days, empty, confirm, store_path, scope }) => {
|
|
1320
|
-
if (!confirm) return errorText('Refusing prune without confirm=true.');
|
|
1321
|
-
const storePath = resolveStorePath(store_path, scope);
|
|
1322
|
-
const pruned = writeStoreLocked(storePath, (db) => {
|
|
1323
|
-
const before = db.items.length;
|
|
1324
|
-
let cutoff = null;
|
|
1325
|
-
if (older_than_days !== undefined) {
|
|
1326
|
-
cutoff = new Date();
|
|
1327
|
-
cutoff.setDate(cutoff.getDate() - older_than_days);
|
|
1328
|
-
}
|
|
1329
|
-
db.items = db.items.filter((item) => {
|
|
1330
|
-
if (cutoff && new Date(item.created_at) < cutoff) return false;
|
|
1331
|
-
if (empty && item.content.trim().length === 0) return false;
|
|
1332
|
-
return true;
|
|
1333
|
-
});
|
|
1334
|
-
return before - db.items.length;
|
|
1335
|
-
});
|
|
1336
|
-
return jsonText({ ok: true, pruned });
|
|
1337
|
-
});
|
|
1338
|
-
|
|
1339
|
-
registerTool(server, 'ok_dedupe', 'Dedupe knowledge items', 'Remove duplicate items by title and content. Requires confirm=true.', {
|
|
1340
|
-
confirm: z.boolean(),
|
|
1341
|
-
store_path: storePathField,
|
|
1342
|
-
scope: scopeField,
|
|
1343
|
-
}, async ({ confirm, store_path, scope }) => {
|
|
1344
|
-
if (!confirm) return errorText('Refusing dedupe without confirm=true.');
|
|
1345
|
-
const storePath = resolveStorePath(store_path, scope);
|
|
1346
|
-
const removed = writeStoreLocked(storePath, (db) => {
|
|
1347
|
-
const seen = new Set();
|
|
1348
|
-
const before = db.items.length;
|
|
1349
|
-
db.items = db.items.filter((item) => {
|
|
1350
|
-
const key = `${item.title}\u0000${item.content}`;
|
|
1351
|
-
if (seen.has(key)) return false;
|
|
1352
|
-
seen.add(key);
|
|
1353
|
-
return true;
|
|
1354
|
-
});
|
|
1355
|
-
return before - db.items.length;
|
|
1356
|
-
});
|
|
1357
|
-
return jsonText({ ok: true, removed });
|
|
1358
|
-
});
|
|
1359
|
-
|
|
1360
|
-
registerTool(server, 'ok_stats', 'Knowledge store statistics', 'Get aggregate stats about the knowledge store', {
|
|
1361
|
-
store_path: storePathField,
|
|
1362
|
-
scope: scopeField,
|
|
1363
|
-
}, async ({ store_path, scope }) => {
|
|
1364
|
-
const storePath = resolveStorePath(store_path, scope);
|
|
1365
|
-
return readStoreLocked(storePath, (db) => {
|
|
1366
|
-
const items = activeItems(db.items, false);
|
|
1367
|
-
const tagCounts = {};
|
|
1368
|
-
for (const item of items) {
|
|
1369
|
-
for (const tag of item.tags ?? []) tagCounts[tag] = (tagCounts[tag] ?? 0) + 1;
|
|
1370
|
-
}
|
|
1371
|
-
return jsonText({
|
|
1372
|
-
ok: true,
|
|
1373
|
-
total: items.length,
|
|
1374
|
-
archived: db.items.length - items.length,
|
|
1375
|
-
tags: Object.fromEntries(Object.entries(tagCounts).sort((a, b) => b[1] - a[1])),
|
|
1376
|
-
});
|
|
1377
|
-
});
|
|
1378
|
-
});
|
|
1379
|
-
|
|
1380
|
-
registerTool(server, 'ok_export', 'Export knowledge items', 'Export all items to a JSON file', {
|
|
1381
|
-
file: z.string().optional().describe('Output file path'),
|
|
1382
|
-
store_path: storePathField,
|
|
1383
|
-
scope: scopeField,
|
|
1384
|
-
}, async ({ file, store_path, scope }) => {
|
|
1385
|
-
const storePath = resolveStorePath(store_path, scope);
|
|
1386
|
-
return readStoreLocked(storePath, (db) => {
|
|
1387
|
-
const filePath = file || './knowledge-export.json';
|
|
1388
|
-
writeFileSync(filePath, JSON.stringify(db, null, 2));
|
|
1389
|
-
return jsonText({ ok: true, file: filePath, count: db.items.length });
|
|
1390
|
-
});
|
|
1391
|
-
});
|
|
1392
|
-
|
|
1393
|
-
registerTool(server, 'ok_import', 'Import knowledge items', 'Import items from an exported JSON file, skipping duplicate IDs', {
|
|
1394
|
-
file: z.string().describe('Path to exported JSON file'),
|
|
1395
|
-
store_path: storePathField,
|
|
1396
|
-
scope: scopeField,
|
|
1397
|
-
}, async ({ file, store_path, scope }) => {
|
|
1398
|
-
if (!existsSync(file)) return errorText(`File not found: ${file}`);
|
|
1399
|
-
const imported = JSON.parse(readFileSync(file, 'utf8'));
|
|
1400
|
-
if (!imported || !Array.isArray(imported.items)) return errorText('Invalid import file: expected {"items": [...]}');
|
|
1401
|
-
const storePath = resolveStorePath(store_path, scope);
|
|
1402
|
-
const result = writeStoreLocked(storePath, (db) => {
|
|
1403
|
-
const existingIds = new Set(db.items.map((item) => item.id));
|
|
1404
|
-
let added = 0;
|
|
1405
|
-
for (const item of imported.items) {
|
|
1406
|
-
if (!existingIds.has(item.id)) {
|
|
1407
|
-
db.items.push(item);
|
|
1408
|
-
existingIds.add(item.id);
|
|
1409
|
-
added += 1;
|
|
1410
|
-
}
|
|
1411
|
-
}
|
|
1412
|
-
return { added, skipped: imported.items.length - added };
|
|
1413
|
-
});
|
|
1414
|
-
return jsonText({ ok: true, ...result });
|
|
1415
|
-
});
|
|
1416
|
-
|
|
1417
|
-
registerTool(server, 'ok_batch', 'Batch add knowledge items', 'Add multiple items at once', {
|
|
1418
|
-
items: z.array(z.object({
|
|
1419
|
-
id: z.string().optional(),
|
|
1420
|
-
title: z.string(),
|
|
1421
|
-
content: z.string(),
|
|
1422
|
-
tags: z.array(z.string()).optional(),
|
|
1423
|
-
metadata: z.record(z.string(), z.unknown()).optional(),
|
|
1424
|
-
created_at: z.string().optional(),
|
|
1425
|
-
updated_at: z.string().optional(),
|
|
1426
|
-
})),
|
|
1427
|
-
store_path: storePathField,
|
|
1428
|
-
scope: scopeField,
|
|
1429
|
-
}, async ({ items, store_path, scope }) => {
|
|
1430
|
-
const storePath = resolveStorePath(store_path, scope);
|
|
1431
|
-
const result = writeStoreLocked(storePath, (db) => {
|
|
1432
|
-
const existingIds = new Set(db.items.map((item) => item.id));
|
|
1433
|
-
let added = 0;
|
|
1434
|
-
let skipped = 0;
|
|
1435
|
-
const now = new Date().toISOString();
|
|
1436
|
-
for (const entry of items) {
|
|
1437
|
-
if (entry.id && existingIds.has(entry.id)) {
|
|
1438
|
-
skipped += 1;
|
|
1439
|
-
continue;
|
|
1440
|
-
}
|
|
1441
|
-
const id = entry.id ?? makeId();
|
|
1442
|
-
db.items.push({
|
|
1443
|
-
id,
|
|
1444
|
-
short_id: shortIdFor(id),
|
|
1445
|
-
title: entry.title,
|
|
1446
|
-
content: entry.content,
|
|
1447
|
-
tags: entry.tags ?? [],
|
|
1448
|
-
metadata: entry.metadata ?? {},
|
|
1449
|
-
archived: false,
|
|
1450
|
-
created_at: entry.created_at ?? now,
|
|
1451
|
-
updated_at: entry.updated_at ?? now,
|
|
1452
|
-
});
|
|
1453
|
-
existingIds.add(id);
|
|
1454
|
-
added += 1;
|
|
1455
|
-
}
|
|
1456
|
-
return { added, skipped };
|
|
1457
|
-
});
|
|
1458
|
-
return jsonText({ ok: true, ...result });
|
|
1459
|
-
});
|
|
1460
|
-
|
|
1461
|
-
return server;
|
|
1462
|
-
}
|
|
1463
|
-
|
|
1464
|
-
function printHelp() {
|
|
1465
|
-
console.error(`Usage: open-knowledge-mcp [options]
|
|
1466
|
-
|
|
1467
|
-
Runs the @hasna/knowledge MCP server (stdio by default).
|
|
1468
|
-
|
|
1469
|
-
Options:
|
|
1470
|
-
--http Serve MCP over Streamable HTTP (127.0.0.1)
|
|
1471
|
-
--port <number> HTTP port (default: 8819, env: MCP_HTTP_PORT)
|
|
1472
|
-
-h, --help Show this help text`);
|
|
1473
|
-
}
|
|
1474
|
-
|
|
1475
|
-
export async function main() {
|
|
1476
|
-
if (process.argv.includes('-h') || process.argv.includes('--help')) {
|
|
1477
|
-
printHelp();
|
|
1478
|
-
return;
|
|
1479
|
-
}
|
|
1480
|
-
|
|
1481
|
-
const { isHttpMode, resolveMcpHttpPort, startMcpHttpServer } = await import('./mcp-http.js');
|
|
1482
|
-
|
|
1483
|
-
if (isHttpMode()) {
|
|
1484
|
-
const handle = await startMcpHttpServer(buildServer, {
|
|
1485
|
-
port: resolveMcpHttpPort(),
|
|
1486
|
-
});
|
|
1487
|
-
process.on('SIGINT', () => void handle.close().finally(() => process.exit(0)));
|
|
1488
|
-
process.on('SIGTERM', () => void handle.close().finally(() => process.exit(0)));
|
|
1489
|
-
return;
|
|
1490
|
-
}
|
|
1491
|
-
|
|
1492
|
-
const server = buildServer();
|
|
1493
|
-
const transport = new StdioServerTransport();
|
|
1494
|
-
await server.connect(transport);
|
|
1495
|
-
console.error('open-knowledge MCP server running on stdio');
|
|
1496
|
-
}
|
|
1497
|
-
|
|
1498
|
-
if (import.meta.main) {
|
|
1499
|
-
main().catch((err) => {
|
|
1500
|
-
console.error('MCP server error:', err);
|
|
1501
|
-
process.exit(1);
|
|
1502
|
-
});
|
|
1503
|
-
}
|