harper-kb 0.2.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/LICENSE +21 -0
- package/README.md +288 -0
- package/config.yaml +13 -0
- package/dist/core/embeddings.d.ts +31 -0
- package/dist/core/embeddings.d.ts.map +1 -0
- package/dist/core/embeddings.js +199 -0
- package/dist/core/embeddings.js.map +1 -0
- package/dist/core/entries.d.ts +101 -0
- package/dist/core/entries.d.ts.map +1 -0
- package/dist/core/entries.js +304 -0
- package/dist/core/entries.js.map +1 -0
- package/dist/core/history.d.ts +31 -0
- package/dist/core/history.d.ts.map +1 -0
- package/dist/core/history.js +119 -0
- package/dist/core/history.js.map +1 -0
- package/dist/core/knowledge-base.d.ts +49 -0
- package/dist/core/knowledge-base.d.ts.map +1 -0
- package/dist/core/knowledge-base.js +117 -0
- package/dist/core/knowledge-base.js.map +1 -0
- package/dist/core/search.d.ts +34 -0
- package/dist/core/search.d.ts.map +1 -0
- package/dist/core/search.js +327 -0
- package/dist/core/search.js.map +1 -0
- package/dist/core/tags.d.ts +39 -0
- package/dist/core/tags.d.ts.map +1 -0
- package/dist/core/tags.js +97 -0
- package/dist/core/tags.js.map +1 -0
- package/dist/core/triage.d.ts +61 -0
- package/dist/core/triage.d.ts.map +1 -0
- package/dist/core/triage.js +136 -0
- package/dist/core/triage.js.map +1 -0
- package/dist/core/webhook-endpoints.d.ts +46 -0
- package/dist/core/webhook-endpoints.d.ts.map +1 -0
- package/dist/core/webhook-endpoints.js +85 -0
- package/dist/core/webhook-endpoints.js.map +1 -0
- package/dist/hooks.d.ts +67 -0
- package/dist/hooks.d.ts.map +1 -0
- package/dist/hooks.js +53 -0
- package/dist/hooks.js.map +1 -0
- package/dist/http-utils.d.ts +38 -0
- package/dist/http-utils.d.ts.map +1 -0
- package/dist/http-utils.js +133 -0
- package/dist/http-utils.js.map +1 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +78 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp/protocol.d.ts +25 -0
- package/dist/mcp/protocol.d.ts.map +1 -0
- package/dist/mcp/protocol.js +105 -0
- package/dist/mcp/protocol.js.map +1 -0
- package/dist/mcp/server.d.ts +28 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/mcp/server.js +144 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/mcp/tools.d.ts +26 -0
- package/dist/mcp/tools.d.ts.map +1 -0
- package/dist/mcp/tools.js +706 -0
- package/dist/mcp/tools.js.map +1 -0
- package/dist/oauth/authorize.d.ts +28 -0
- package/dist/oauth/authorize.d.ts.map +1 -0
- package/dist/oauth/authorize.js +421 -0
- package/dist/oauth/authorize.js.map +1 -0
- package/dist/oauth/init.d.ts +18 -0
- package/dist/oauth/init.d.ts.map +1 -0
- package/dist/oauth/init.js +30 -0
- package/dist/oauth/init.js.map +1 -0
- package/dist/oauth/keys.d.ts +34 -0
- package/dist/oauth/keys.d.ts.map +1 -0
- package/dist/oauth/keys.js +101 -0
- package/dist/oauth/keys.js.map +1 -0
- package/dist/oauth/metadata.d.ts +23 -0
- package/dist/oauth/metadata.d.ts.map +1 -0
- package/dist/oauth/metadata.js +57 -0
- package/dist/oauth/metadata.js.map +1 -0
- package/dist/oauth/middleware.d.ts +23 -0
- package/dist/oauth/middleware.d.ts.map +1 -0
- package/dist/oauth/middleware.js +65 -0
- package/dist/oauth/middleware.js.map +1 -0
- package/dist/oauth/register.d.ts +15 -0
- package/dist/oauth/register.d.ts.map +1 -0
- package/dist/oauth/register.js +78 -0
- package/dist/oauth/register.js.map +1 -0
- package/dist/oauth/token.d.ts +16 -0
- package/dist/oauth/token.d.ts.map +1 -0
- package/dist/oauth/token.js +184 -0
- package/dist/oauth/token.js.map +1 -0
- package/dist/oauth/validate.d.ts +40 -0
- package/dist/oauth/validate.d.ts.map +1 -0
- package/dist/oauth/validate.js +61 -0
- package/dist/oauth/validate.js.map +1 -0
- package/dist/resources/HistoryResource.d.ts +41 -0
- package/dist/resources/HistoryResource.d.ts.map +1 -0
- package/dist/resources/HistoryResource.js +61 -0
- package/dist/resources/HistoryResource.js.map +1 -0
- package/dist/resources/KnowledgeBaseResource.d.ts +60 -0
- package/dist/resources/KnowledgeBaseResource.d.ts.map +1 -0
- package/dist/resources/KnowledgeBaseResource.js +118 -0
- package/dist/resources/KnowledgeBaseResource.js.map +1 -0
- package/dist/resources/KnowledgeEntryResource.d.ts +61 -0
- package/dist/resources/KnowledgeEntryResource.d.ts.map +1 -0
- package/dist/resources/KnowledgeEntryResource.js +191 -0
- package/dist/resources/KnowledgeEntryResource.js.map +1 -0
- package/dist/resources/MeResource.d.ts +31 -0
- package/dist/resources/MeResource.d.ts.map +1 -0
- package/dist/resources/MeResource.js +40 -0
- package/dist/resources/MeResource.js.map +1 -0
- package/dist/resources/QueryLogResource.d.ts +22 -0
- package/dist/resources/QueryLogResource.d.ts.map +1 -0
- package/dist/resources/QueryLogResource.js +66 -0
- package/dist/resources/QueryLogResource.js.map +1 -0
- package/dist/resources/ServiceKeyResource.d.ts +52 -0
- package/dist/resources/ServiceKeyResource.d.ts.map +1 -0
- package/dist/resources/ServiceKeyResource.js +151 -0
- package/dist/resources/ServiceKeyResource.js.map +1 -0
- package/dist/resources/TagResource.d.ts +27 -0
- package/dist/resources/TagResource.d.ts.map +1 -0
- package/dist/resources/TagResource.js +41 -0
- package/dist/resources/TagResource.js.map +1 -0
- package/dist/resources/TriageResource.d.ts +53 -0
- package/dist/resources/TriageResource.d.ts.map +1 -0
- package/dist/resources/TriageResource.js +120 -0
- package/dist/resources/TriageResource.js.map +1 -0
- package/dist/resources/WebhookEndpointResource.d.ts +63 -0
- package/dist/resources/WebhookEndpointResource.d.ts.map +1 -0
- package/dist/resources/WebhookEndpointResource.js +115 -0
- package/dist/resources/WebhookEndpointResource.js.map +1 -0
- package/dist/types.d.ts +378 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +8 -0
- package/dist/types.js.map +1 -0
- package/dist/webhooks/github.d.ts +25 -0
- package/dist/webhooks/github.d.ts.map +1 -0
- package/dist/webhooks/github.js +165 -0
- package/dist/webhooks/github.js.map +1 -0
- package/dist/webhooks/middleware.d.ts +19 -0
- package/dist/webhooks/middleware.d.ts.map +1 -0
- package/dist/webhooks/middleware.js +144 -0
- package/dist/webhooks/middleware.js.map +1 -0
- package/dist/webhooks/types.d.ts +18 -0
- package/dist/webhooks/types.d.ts.map +1 -0
- package/dist/webhooks/types.js +5 -0
- package/dist/webhooks/types.js.map +1 -0
- package/package.json +69 -0
- package/schema/knowledge.graphql +136 -0
- package/schema/oauth.graphql +45 -0
|
@@ -0,0 +1,706 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Tool Definitions
|
|
3
|
+
*
|
|
4
|
+
* Static array of tool definitions with JSON Schema input schemas
|
|
5
|
+
* and handler functions. Each handler receives (args, caller) and
|
|
6
|
+
* returns MCP tool content.
|
|
7
|
+
*
|
|
8
|
+
* The kbId is implicit — it comes from the caller (extracted from the URL
|
|
9
|
+
* path /mcp/<kbId>), so tools don't need it as an input parameter.
|
|
10
|
+
*/
|
|
11
|
+
import { search } from "../core/search.js";
|
|
12
|
+
import { createEntry, getEntry, updateEntry, stripEmbedding, reindexEmbeddings, linkRelated, linkSiblings, } from "../core/entries.js";
|
|
13
|
+
import { listTags } from "../core/tags.js";
|
|
14
|
+
import { submitTriage } from "../core/triage.js";
|
|
15
|
+
import { generateEmbedding } from "../core/embeddings.js";
|
|
16
|
+
import { getHistory } from "../core/history.js";
|
|
17
|
+
// ============================================================================
|
|
18
|
+
// Helpers
|
|
19
|
+
// ============================================================================
|
|
20
|
+
function jsonContent(data) {
|
|
21
|
+
return {
|
|
22
|
+
content: [{ type: 'text', text: JSON.stringify(data, null, 2) }],
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
function errorContent(message) {
|
|
26
|
+
return {
|
|
27
|
+
content: [{ type: 'text', text: message }],
|
|
28
|
+
isError: true,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
function requireWrite(caller, action) {
|
|
32
|
+
if (!caller.scopes.includes('mcp:write')) {
|
|
33
|
+
return errorContent(`Write access required to ${action}.`);
|
|
34
|
+
}
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
// ============================================================================
|
|
38
|
+
// Tool Definitions
|
|
39
|
+
// ============================================================================
|
|
40
|
+
export const tools = [
|
|
41
|
+
// =========================================================================
|
|
42
|
+
// 1. knowledge_search — Search the knowledge base
|
|
43
|
+
// =========================================================================
|
|
44
|
+
{
|
|
45
|
+
name: 'knowledge_search',
|
|
46
|
+
description: 'Search the knowledge base using keyword, semantic, or hybrid search. ' +
|
|
47
|
+
'Returns scored results sorted by relevance. Provide optional environment ' +
|
|
48
|
+
'context to boost results matching your setup.',
|
|
49
|
+
inputSchema: {
|
|
50
|
+
type: 'object',
|
|
51
|
+
properties: {
|
|
52
|
+
query: { type: 'string', description: 'Search query string' },
|
|
53
|
+
tags: {
|
|
54
|
+
type: 'array',
|
|
55
|
+
items: { type: 'string' },
|
|
56
|
+
description: 'Filter results by tags',
|
|
57
|
+
},
|
|
58
|
+
limit: {
|
|
59
|
+
type: 'integer',
|
|
60
|
+
minimum: 1,
|
|
61
|
+
maximum: 50,
|
|
62
|
+
description: 'Maximum number of results (default 10)',
|
|
63
|
+
},
|
|
64
|
+
context: {
|
|
65
|
+
type: 'object',
|
|
66
|
+
additionalProperties: { type: 'string' },
|
|
67
|
+
description: "Caller's environment context as key-value pairs for applicability filtering. " +
|
|
68
|
+
"Values can be exact strings or semver ranges (e.g., { product: '>=2.0', tier: 'enterprise' }).",
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
required: ['query'],
|
|
72
|
+
},
|
|
73
|
+
handler: async (args, caller) => {
|
|
74
|
+
try {
|
|
75
|
+
const results = await search({
|
|
76
|
+
kbId: caller.kbId,
|
|
77
|
+
query: args.query,
|
|
78
|
+
tags: args.tags,
|
|
79
|
+
limit: args.limit,
|
|
80
|
+
context: args.context,
|
|
81
|
+
});
|
|
82
|
+
const cleaned = results.map(stripEmbedding);
|
|
83
|
+
return jsonContent({
|
|
84
|
+
resultCount: cleaned.length,
|
|
85
|
+
results: cleaned,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
logger?.error?.('knowledge_search failed:', error.message);
|
|
90
|
+
return errorContent('Search failed. Please try again.');
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
// =========================================================================
|
|
95
|
+
// 2. knowledge_add — Add a new knowledge entry
|
|
96
|
+
// =========================================================================
|
|
97
|
+
{
|
|
98
|
+
name: 'knowledge_add',
|
|
99
|
+
description: 'Add a new entry to the knowledge base. Keep entries focused and concise — ' +
|
|
100
|
+
'one topic per entry. Use the knowledge_link tool to cross-reference related entries ' +
|
|
101
|
+
'rather than duplicating content. Use references for links to external documentation. ' +
|
|
102
|
+
'Entries added via MCP are automatically tagged with confidence "ai-generated".',
|
|
103
|
+
inputSchema: {
|
|
104
|
+
type: 'object',
|
|
105
|
+
properties: {
|
|
106
|
+
title: {
|
|
107
|
+
type: 'string',
|
|
108
|
+
maxLength: 500,
|
|
109
|
+
description: 'Entry title — concise summary of the knowledge',
|
|
110
|
+
},
|
|
111
|
+
content: {
|
|
112
|
+
type: 'string',
|
|
113
|
+
maxLength: 100000,
|
|
114
|
+
description: 'Full content of the knowledge entry (Markdown supported)',
|
|
115
|
+
},
|
|
116
|
+
tags: {
|
|
117
|
+
type: 'array',
|
|
118
|
+
items: { type: 'string', maxLength: 100 },
|
|
119
|
+
maxItems: 50,
|
|
120
|
+
description: 'Tags for categorization (e.g., ["plugins", "config"])',
|
|
121
|
+
},
|
|
122
|
+
source: {
|
|
123
|
+
type: 'string',
|
|
124
|
+
maxLength: 200,
|
|
125
|
+
description: 'Source identifier (e.g., "github-issue", "docs", "slack")',
|
|
126
|
+
},
|
|
127
|
+
sourceUrl: {
|
|
128
|
+
type: 'string',
|
|
129
|
+
maxLength: 2000,
|
|
130
|
+
description: 'URL to the original source',
|
|
131
|
+
},
|
|
132
|
+
references: {
|
|
133
|
+
type: 'array',
|
|
134
|
+
items: {
|
|
135
|
+
type: 'object',
|
|
136
|
+
properties: {
|
|
137
|
+
url: {
|
|
138
|
+
type: 'string',
|
|
139
|
+
maxLength: 2000,
|
|
140
|
+
description: 'URL to the documentation or resource',
|
|
141
|
+
},
|
|
142
|
+
title: {
|
|
143
|
+
type: 'string',
|
|
144
|
+
maxLength: 500,
|
|
145
|
+
description: 'Human-readable title for the link',
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
required: ['url', 'title'],
|
|
149
|
+
},
|
|
150
|
+
maxItems: 20,
|
|
151
|
+
description: 'External documentation links (docs pages, tutorials, API references)',
|
|
152
|
+
},
|
|
153
|
+
appliesTo: {
|
|
154
|
+
type: 'object',
|
|
155
|
+
additionalProperties: { type: 'string' },
|
|
156
|
+
description: 'Applicability scope — what environments this entry applies to. ' +
|
|
157
|
+
"Use semver ranges for version fields (e.g., { product: '>=2.0', platform: 'linux' }).",
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
required: ['title', 'content', 'tags'],
|
|
161
|
+
},
|
|
162
|
+
handler: async (args, caller) => {
|
|
163
|
+
const denied = requireWrite(caller, 'add entries');
|
|
164
|
+
if (denied)
|
|
165
|
+
return denied;
|
|
166
|
+
try {
|
|
167
|
+
const entry = await createEntry({
|
|
168
|
+
kbId: caller.kbId,
|
|
169
|
+
title: args.title,
|
|
170
|
+
content: args.content,
|
|
171
|
+
tags: args.tags,
|
|
172
|
+
source: args.source,
|
|
173
|
+
sourceUrl: args.sourceUrl,
|
|
174
|
+
references: args.references,
|
|
175
|
+
appliesTo: args.appliesTo,
|
|
176
|
+
confidence: 'ai-generated',
|
|
177
|
+
});
|
|
178
|
+
return jsonContent({
|
|
179
|
+
message: 'Knowledge entry created successfully',
|
|
180
|
+
entry: stripEmbedding(entry),
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
catch (error) {
|
|
184
|
+
logger?.error?.('knowledge_add failed:', error.message);
|
|
185
|
+
return errorContent('Failed to create entry. Please try again.');
|
|
186
|
+
}
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
// =========================================================================
|
|
190
|
+
// 3. knowledge_get — Get a knowledge entry by ID
|
|
191
|
+
// =========================================================================
|
|
192
|
+
{
|
|
193
|
+
name: 'knowledge_get',
|
|
194
|
+
description: 'Get a single knowledge entry by ID. If the entry has relationships ' +
|
|
195
|
+
'(supersedes, superseded by, siblings, related), the linked entries ' +
|
|
196
|
+
'are also fetched and included in the response.',
|
|
197
|
+
inputSchema: {
|
|
198
|
+
type: 'object',
|
|
199
|
+
properties: {
|
|
200
|
+
id: { type: 'string', description: 'The knowledge entry ID' },
|
|
201
|
+
},
|
|
202
|
+
required: ['id'],
|
|
203
|
+
},
|
|
204
|
+
handler: async (args, caller) => {
|
|
205
|
+
try {
|
|
206
|
+
const id = args.id;
|
|
207
|
+
const entry = await getEntry(id);
|
|
208
|
+
if (!entry || entry.kbId !== caller.kbId) {
|
|
209
|
+
return errorContent(`Knowledge entry not found: ${id}`);
|
|
210
|
+
}
|
|
211
|
+
const result = {
|
|
212
|
+
entry: stripEmbedding(entry),
|
|
213
|
+
};
|
|
214
|
+
const relationships = {};
|
|
215
|
+
if (entry.supersedesId) {
|
|
216
|
+
const supersedes = await getEntry(entry.supersedesId);
|
|
217
|
+
if (supersedes) {
|
|
218
|
+
relationships.supersedes = stripEmbedding(supersedes);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
if (entry.supersededById) {
|
|
222
|
+
const supersededBy = await getEntry(entry.supersededById);
|
|
223
|
+
if (supersededBy) {
|
|
224
|
+
relationships.supersededBy = stripEmbedding(supersededBy);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
if (entry.siblingIds && entry.siblingIds.length > 0) {
|
|
228
|
+
const siblings = [];
|
|
229
|
+
for (const siblingId of entry.siblingIds) {
|
|
230
|
+
const sibling = await getEntry(siblingId);
|
|
231
|
+
if (sibling) {
|
|
232
|
+
siblings.push(stripEmbedding(sibling));
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
if (siblings.length > 0) {
|
|
236
|
+
relationships.siblings = siblings;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
if (entry.relatedIds && entry.relatedIds.length > 0) {
|
|
240
|
+
const related = [];
|
|
241
|
+
for (const relatedId of entry.relatedIds) {
|
|
242
|
+
const relatedEntry = await getEntry(relatedId);
|
|
243
|
+
if (relatedEntry) {
|
|
244
|
+
related.push(stripEmbedding(relatedEntry));
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
if (related.length > 0) {
|
|
248
|
+
relationships.related = related;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
if (Object.keys(relationships).length > 0) {
|
|
252
|
+
result.relationships = relationships;
|
|
253
|
+
}
|
|
254
|
+
return jsonContent(result);
|
|
255
|
+
}
|
|
256
|
+
catch (error) {
|
|
257
|
+
logger?.error?.('knowledge_get failed:', error.message);
|
|
258
|
+
return errorContent('Failed to get entry. Please try again.');
|
|
259
|
+
}
|
|
260
|
+
},
|
|
261
|
+
},
|
|
262
|
+
// =========================================================================
|
|
263
|
+
// 4. knowledge_related — Find entries related to a given entry
|
|
264
|
+
// =========================================================================
|
|
265
|
+
{
|
|
266
|
+
name: 'knowledge_related',
|
|
267
|
+
description: 'Find knowledge entries related to a given entry. Combines explicit ' +
|
|
268
|
+
'relationships (siblings, related, supersedes chain) with semantic ' +
|
|
269
|
+
"similarity search using the entry's embedding.",
|
|
270
|
+
inputSchema: {
|
|
271
|
+
type: 'object',
|
|
272
|
+
properties: {
|
|
273
|
+
id: {
|
|
274
|
+
type: 'string',
|
|
275
|
+
description: 'The knowledge entry ID to find related entries for',
|
|
276
|
+
},
|
|
277
|
+
limit: {
|
|
278
|
+
type: 'integer',
|
|
279
|
+
minimum: 1,
|
|
280
|
+
maximum: 50,
|
|
281
|
+
description: 'Maximum number of results (default 10)',
|
|
282
|
+
},
|
|
283
|
+
},
|
|
284
|
+
required: ['id'],
|
|
285
|
+
},
|
|
286
|
+
handler: async (args, caller) => {
|
|
287
|
+
const id = args.id;
|
|
288
|
+
const maxResults = args.limit ?? 10;
|
|
289
|
+
try {
|
|
290
|
+
const entry = await getEntry(id);
|
|
291
|
+
if (!entry || entry.kbId !== caller.kbId) {
|
|
292
|
+
return errorContent(`Knowledge entry not found: ${id}`);
|
|
293
|
+
}
|
|
294
|
+
const relatedMap = new Map();
|
|
295
|
+
if (entry.supersedesId) {
|
|
296
|
+
const supersedes = await getEntry(entry.supersedesId);
|
|
297
|
+
if (supersedes) {
|
|
298
|
+
relatedMap.set(supersedes.id, {
|
|
299
|
+
entry: stripEmbedding(supersedes),
|
|
300
|
+
relationship: 'supersedes',
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
if (entry.supersededById) {
|
|
305
|
+
const supersededBy = await getEntry(entry.supersededById);
|
|
306
|
+
if (supersededBy) {
|
|
307
|
+
relatedMap.set(supersededBy.id, {
|
|
308
|
+
entry: stripEmbedding(supersededBy),
|
|
309
|
+
relationship: 'superseded_by',
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
if (entry.siblingIds) {
|
|
314
|
+
for (const siblingId of entry.siblingIds) {
|
|
315
|
+
if (!relatedMap.has(siblingId)) {
|
|
316
|
+
const sibling = await getEntry(siblingId);
|
|
317
|
+
if (sibling) {
|
|
318
|
+
relatedMap.set(sibling.id, {
|
|
319
|
+
entry: stripEmbedding(sibling),
|
|
320
|
+
relationship: 'sibling',
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
if (entry.relatedIds) {
|
|
327
|
+
for (const relatedId of entry.relatedIds) {
|
|
328
|
+
if (!relatedMap.has(relatedId)) {
|
|
329
|
+
const relatedEntry = await getEntry(relatedId);
|
|
330
|
+
if (relatedEntry) {
|
|
331
|
+
relatedMap.set(relatedEntry.id, {
|
|
332
|
+
entry: stripEmbedding(relatedEntry),
|
|
333
|
+
relationship: 'related',
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
// Semantic similarity search using entry content
|
|
340
|
+
let semanticResults = [];
|
|
341
|
+
try {
|
|
342
|
+
const queryText = `${entry.title}\n\n${entry.content}`;
|
|
343
|
+
const embedding = await generateEmbedding(queryText);
|
|
344
|
+
const searchResults = [];
|
|
345
|
+
for await (const item of databases.kb.KnowledgeEntry.search({
|
|
346
|
+
conditions: [{ attribute: 'kbId', comparator: 'equals', value: caller.kbId }],
|
|
347
|
+
sort: { attribute: 'embedding', target: embedding },
|
|
348
|
+
limit: maxResults + 10,
|
|
349
|
+
})) {
|
|
350
|
+
searchResults.push(item);
|
|
351
|
+
}
|
|
352
|
+
semanticResults = searchResults
|
|
353
|
+
.map((r) => r)
|
|
354
|
+
.filter((r) => r.id !== id && !r.deprecated);
|
|
355
|
+
}
|
|
356
|
+
catch {
|
|
357
|
+
// Embedding model may not be available
|
|
358
|
+
}
|
|
359
|
+
for (const result of semanticResults) {
|
|
360
|
+
if (!relatedMap.has(result.id) && relatedMap.size < maxResults) {
|
|
361
|
+
relatedMap.set(result.id, {
|
|
362
|
+
entry: stripEmbedding(result),
|
|
363
|
+
relationship: 'similar',
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
const results = Array.from(relatedMap.values()).slice(0, maxResults);
|
|
368
|
+
return jsonContent({
|
|
369
|
+
entryId: id,
|
|
370
|
+
entryTitle: entry.title,
|
|
371
|
+
relatedCount: results.length,
|
|
372
|
+
related: results,
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
catch (error) {
|
|
376
|
+
logger?.error?.('knowledge_related failed:', error.message);
|
|
377
|
+
return errorContent('Failed to find related entries. Please try again.');
|
|
378
|
+
}
|
|
379
|
+
},
|
|
380
|
+
},
|
|
381
|
+
// =========================================================================
|
|
382
|
+
// 5. knowledge_list_tags — List all knowledge tags
|
|
383
|
+
// =========================================================================
|
|
384
|
+
{
|
|
385
|
+
name: 'knowledge_list_tags',
|
|
386
|
+
description: 'List all tags in the knowledge base with their entry counts. ' +
|
|
387
|
+
'Useful for discovering available categories before searching.',
|
|
388
|
+
inputSchema: {
|
|
389
|
+
type: 'object',
|
|
390
|
+
properties: {},
|
|
391
|
+
},
|
|
392
|
+
handler: async (_args, caller) => {
|
|
393
|
+
try {
|
|
394
|
+
const tags = await listTags(caller.kbId);
|
|
395
|
+
return jsonContent({
|
|
396
|
+
tagCount: tags.length,
|
|
397
|
+
tags,
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
catch (error) {
|
|
401
|
+
logger?.error?.('knowledge_list_tags failed:', error.message);
|
|
402
|
+
return errorContent('Failed to list tags. Please try again.');
|
|
403
|
+
}
|
|
404
|
+
},
|
|
405
|
+
},
|
|
406
|
+
// =========================================================================
|
|
407
|
+
// 6. knowledge_triage — Submit an item to the triage queue
|
|
408
|
+
// =========================================================================
|
|
409
|
+
{
|
|
410
|
+
name: 'knowledge_triage',
|
|
411
|
+
description: 'Submit a new item to the knowledge triage queue for review. ' +
|
|
412
|
+
'Use this when you encounter information that should potentially ' +
|
|
413
|
+
'be added to the knowledge base but needs human review first.',
|
|
414
|
+
inputSchema: {
|
|
415
|
+
type: 'object',
|
|
416
|
+
properties: {
|
|
417
|
+
source: {
|
|
418
|
+
type: 'string',
|
|
419
|
+
description: 'Source identifier (e.g., "claude-code", "github-issue", "slack")',
|
|
420
|
+
},
|
|
421
|
+
summary: {
|
|
422
|
+
type: 'string',
|
|
423
|
+
description: 'Brief summary of the knowledge to triage',
|
|
424
|
+
},
|
|
425
|
+
payload: {
|
|
426
|
+
type: 'object',
|
|
427
|
+
additionalProperties: true,
|
|
428
|
+
description: 'Additional payload data from the source',
|
|
429
|
+
},
|
|
430
|
+
},
|
|
431
|
+
required: ['source', 'summary'],
|
|
432
|
+
},
|
|
433
|
+
handler: async (args, caller) => {
|
|
434
|
+
const denied = requireWrite(caller, 'submit triage items');
|
|
435
|
+
if (denied)
|
|
436
|
+
return denied;
|
|
437
|
+
try {
|
|
438
|
+
const item = await submitTriage(caller.kbId, args.source, args.summary, args.payload);
|
|
439
|
+
return jsonContent({
|
|
440
|
+
message: 'Triage item submitted successfully',
|
|
441
|
+
item,
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
catch (error) {
|
|
445
|
+
logger?.error?.('knowledge_triage failed:', error.message);
|
|
446
|
+
return errorContent('Failed to submit triage item. Please try again.');
|
|
447
|
+
}
|
|
448
|
+
},
|
|
449
|
+
},
|
|
450
|
+
// =========================================================================
|
|
451
|
+
// 7. knowledge_update — Update an existing knowledge entry
|
|
452
|
+
// =========================================================================
|
|
453
|
+
{
|
|
454
|
+
name: 'knowledge_update',
|
|
455
|
+
description: 'Update an existing knowledge entry. Only provide fields you want to change. ' +
|
|
456
|
+
'Prefer keeping entries concise and using references/knowledge_link to connect ' +
|
|
457
|
+
'to other entries and external docs rather than inlining everything. ' +
|
|
458
|
+
'Edits are tracked in the history log with who made the change and why.',
|
|
459
|
+
inputSchema: {
|
|
460
|
+
type: 'object',
|
|
461
|
+
properties: {
|
|
462
|
+
id: {
|
|
463
|
+
type: 'string',
|
|
464
|
+
description: 'The knowledge entry ID to update',
|
|
465
|
+
},
|
|
466
|
+
title: {
|
|
467
|
+
type: 'string',
|
|
468
|
+
maxLength: 500,
|
|
469
|
+
description: 'Updated title',
|
|
470
|
+
},
|
|
471
|
+
content: {
|
|
472
|
+
type: 'string',
|
|
473
|
+
maxLength: 100000,
|
|
474
|
+
description: 'Updated content (Markdown supported)',
|
|
475
|
+
},
|
|
476
|
+
tags: {
|
|
477
|
+
type: 'array',
|
|
478
|
+
items: { type: 'string', maxLength: 100 },
|
|
479
|
+
maxItems: 50,
|
|
480
|
+
description: 'Updated tags',
|
|
481
|
+
},
|
|
482
|
+
source: {
|
|
483
|
+
type: 'string',
|
|
484
|
+
maxLength: 200,
|
|
485
|
+
description: 'Updated source identifier',
|
|
486
|
+
},
|
|
487
|
+
sourceUrl: {
|
|
488
|
+
type: 'string',
|
|
489
|
+
maxLength: 2000,
|
|
490
|
+
description: 'Updated source URL',
|
|
491
|
+
},
|
|
492
|
+
references: {
|
|
493
|
+
type: 'array',
|
|
494
|
+
items: {
|
|
495
|
+
type: 'object',
|
|
496
|
+
properties: {
|
|
497
|
+
url: {
|
|
498
|
+
type: 'string',
|
|
499
|
+
maxLength: 2000,
|
|
500
|
+
description: 'URL to the documentation or resource',
|
|
501
|
+
},
|
|
502
|
+
title: {
|
|
503
|
+
type: 'string',
|
|
504
|
+
maxLength: 500,
|
|
505
|
+
description: 'Human-readable title for the link',
|
|
506
|
+
},
|
|
507
|
+
},
|
|
508
|
+
required: ['url', 'title'],
|
|
509
|
+
},
|
|
510
|
+
maxItems: 20,
|
|
511
|
+
description: 'Updated external documentation links',
|
|
512
|
+
},
|
|
513
|
+
confidence: {
|
|
514
|
+
type: 'string',
|
|
515
|
+
enum: ['ai-generated', 'reviewed', 'verified'],
|
|
516
|
+
description: 'Updated confidence level',
|
|
517
|
+
},
|
|
518
|
+
appliesTo: {
|
|
519
|
+
type: 'object',
|
|
520
|
+
additionalProperties: { type: 'string' },
|
|
521
|
+
description: 'Updated applicability scope. ' +
|
|
522
|
+
"Use semver ranges for version fields (e.g., { product: '>=2.0', platform: 'linux' }).",
|
|
523
|
+
},
|
|
524
|
+
deprecated: {
|
|
525
|
+
type: 'boolean',
|
|
526
|
+
description: 'Mark as deprecated',
|
|
527
|
+
},
|
|
528
|
+
editSummary: {
|
|
529
|
+
type: 'string',
|
|
530
|
+
maxLength: 1000,
|
|
531
|
+
description: 'Brief description of what changed and why (for the edit log)',
|
|
532
|
+
},
|
|
533
|
+
},
|
|
534
|
+
required: ['id'],
|
|
535
|
+
},
|
|
536
|
+
handler: async (args, caller) => {
|
|
537
|
+
const denied = requireWrite(caller, 'update entries');
|
|
538
|
+
if (denied)
|
|
539
|
+
return denied;
|
|
540
|
+
const id = args.id;
|
|
541
|
+
const existing = await getEntry(id);
|
|
542
|
+
if (!existing || existing.kbId !== caller.kbId) {
|
|
543
|
+
return errorContent(`Knowledge entry not found: ${id}`);
|
|
544
|
+
}
|
|
545
|
+
const { id: _id, editSummary, ...updates } = args;
|
|
546
|
+
if (updates.confidence && updates.confidence !== 'ai-generated') {
|
|
547
|
+
delete updates.confidence;
|
|
548
|
+
}
|
|
549
|
+
try {
|
|
550
|
+
const entry = await updateEntry(id, updates, {
|
|
551
|
+
editedBy: caller.userId,
|
|
552
|
+
editSummary: editSummary,
|
|
553
|
+
});
|
|
554
|
+
return jsonContent({
|
|
555
|
+
message: 'Knowledge entry updated successfully',
|
|
556
|
+
entry: stripEmbedding(entry),
|
|
557
|
+
});
|
|
558
|
+
}
|
|
559
|
+
catch (error) {
|
|
560
|
+
logger?.error?.('knowledge_update failed:', error.message);
|
|
561
|
+
return errorContent('Failed to update entry. Please try again.');
|
|
562
|
+
}
|
|
563
|
+
},
|
|
564
|
+
},
|
|
565
|
+
// =========================================================================
|
|
566
|
+
// 8. knowledge_history — Get edit history for an entry
|
|
567
|
+
// =========================================================================
|
|
568
|
+
{
|
|
569
|
+
name: 'knowledge_history',
|
|
570
|
+
description: 'Get the edit history for a knowledge entry. Shows who changed what, ' +
|
|
571
|
+
'when, and why — with snapshots of previous values for each changed field.',
|
|
572
|
+
inputSchema: {
|
|
573
|
+
type: 'object',
|
|
574
|
+
properties: {
|
|
575
|
+
id: {
|
|
576
|
+
type: 'string',
|
|
577
|
+
description: 'The knowledge entry ID to get history for',
|
|
578
|
+
},
|
|
579
|
+
limit: {
|
|
580
|
+
type: 'integer',
|
|
581
|
+
minimum: 1,
|
|
582
|
+
maximum: 100,
|
|
583
|
+
description: 'Maximum number of edits to return (default 50)',
|
|
584
|
+
},
|
|
585
|
+
},
|
|
586
|
+
required: ['id'],
|
|
587
|
+
},
|
|
588
|
+
handler: async (args, caller) => {
|
|
589
|
+
try {
|
|
590
|
+
const id = args.id;
|
|
591
|
+
const entry = await getEntry(id);
|
|
592
|
+
if (!entry || entry.kbId !== caller.kbId) {
|
|
593
|
+
return errorContent(`Knowledge entry not found: ${id}`);
|
|
594
|
+
}
|
|
595
|
+
const edits = await getHistory(id, args.limit);
|
|
596
|
+
return jsonContent({
|
|
597
|
+
entryId: id,
|
|
598
|
+
entryTitle: entry.title,
|
|
599
|
+
editCount: edits.length,
|
|
600
|
+
edits,
|
|
601
|
+
});
|
|
602
|
+
}
|
|
603
|
+
catch (error) {
|
|
604
|
+
logger?.error?.('knowledge_history failed:', error.message);
|
|
605
|
+
return errorContent('Failed to get edit history. Please try again.');
|
|
606
|
+
}
|
|
607
|
+
},
|
|
608
|
+
},
|
|
609
|
+
// =========================================================================
|
|
610
|
+
// 9. knowledge_reindex — Backfill missing embeddings
|
|
611
|
+
// =========================================================================
|
|
612
|
+
{
|
|
613
|
+
name: 'knowledge_reindex',
|
|
614
|
+
description: 'Reindex embeddings for all knowledge entries that are missing them. ' +
|
|
615
|
+
'Use this after the embedding model becomes available on a deployment ' +
|
|
616
|
+
'where entries were initially created without embeddings. ' +
|
|
617
|
+
'Requires write access.',
|
|
618
|
+
inputSchema: {
|
|
619
|
+
type: 'object',
|
|
620
|
+
properties: {},
|
|
621
|
+
},
|
|
622
|
+
handler: async (_args, caller) => {
|
|
623
|
+
const denied = requireWrite(caller, 'reindex embeddings');
|
|
624
|
+
if (denied)
|
|
625
|
+
return denied;
|
|
626
|
+
try {
|
|
627
|
+
const result = await reindexEmbeddings(caller.kbId);
|
|
628
|
+
return jsonContent({
|
|
629
|
+
message: 'Embedding reindex complete',
|
|
630
|
+
...result,
|
|
631
|
+
});
|
|
632
|
+
}
|
|
633
|
+
catch (error) {
|
|
634
|
+
logger?.error?.('knowledge_reindex failed:', error.message);
|
|
635
|
+
return errorContent('Failed to reindex embeddings. Please try again.');
|
|
636
|
+
}
|
|
637
|
+
},
|
|
638
|
+
},
|
|
639
|
+
// =========================================================================
|
|
640
|
+
// 10. knowledge_link — Manage relationships between entries
|
|
641
|
+
// =========================================================================
|
|
642
|
+
{
|
|
643
|
+
name: 'knowledge_link',
|
|
644
|
+
description: 'Create relationships between knowledge entries. Use this to keep the knowledge ' +
|
|
645
|
+
'base well-connected — link related entries instead of duplicating information. ' +
|
|
646
|
+
"Supports 'related' (one-directional) and 'sibling' (multi-way) relationship types.",
|
|
647
|
+
inputSchema: {
|
|
648
|
+
type: 'object',
|
|
649
|
+
properties: {
|
|
650
|
+
type: {
|
|
651
|
+
type: 'string',
|
|
652
|
+
enum: ['related', 'sibling'],
|
|
653
|
+
description: "Relationship type: 'related' adds a one-directional link (A -> B), " +
|
|
654
|
+
"'sibling' creates multi-way links between all provided IDs",
|
|
655
|
+
},
|
|
656
|
+
ids: {
|
|
657
|
+
type: 'array',
|
|
658
|
+
items: { type: 'string' },
|
|
659
|
+
minItems: 2,
|
|
660
|
+
maxItems: 20,
|
|
661
|
+
description: "Entry IDs to link. For 'related', the first ID is the source and " +
|
|
662
|
+
'subsequent IDs are added as its related entries. ' +
|
|
663
|
+
"For 'sibling', all entries are linked to each other.",
|
|
664
|
+
},
|
|
665
|
+
},
|
|
666
|
+
required: ['type', 'ids'],
|
|
667
|
+
},
|
|
668
|
+
handler: async (args, caller) => {
|
|
669
|
+
const denied = requireWrite(caller, 'manage relationships');
|
|
670
|
+
if (denied)
|
|
671
|
+
return denied;
|
|
672
|
+
const ids = args.ids;
|
|
673
|
+
for (const entryId of ids) {
|
|
674
|
+
const entry = await getEntry(entryId);
|
|
675
|
+
if (!entry || entry.kbId !== caller.kbId) {
|
|
676
|
+
return errorContent(`Knowledge entry not found: ${entryId}`);
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
try {
|
|
680
|
+
if (args.type === 'sibling') {
|
|
681
|
+
await linkSiblings(ids);
|
|
682
|
+
return jsonContent({
|
|
683
|
+
message: `Linked ${ids.length} entries as siblings`,
|
|
684
|
+
ids,
|
|
685
|
+
});
|
|
686
|
+
}
|
|
687
|
+
else {
|
|
688
|
+
const [sourceId, ...targetIds] = ids;
|
|
689
|
+
for (const targetId of targetIds) {
|
|
690
|
+
await linkRelated(sourceId, targetId);
|
|
691
|
+
}
|
|
692
|
+
return jsonContent({
|
|
693
|
+
message: `Added ${targetIds.length} related link(s) to entry ${sourceId}`,
|
|
694
|
+
sourceId,
|
|
695
|
+
relatedIds: targetIds,
|
|
696
|
+
});
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
catch (error) {
|
|
700
|
+
logger?.error?.('knowledge_link failed:', error.message);
|
|
701
|
+
return errorContent('Failed to link entries. Please try again.');
|
|
702
|
+
}
|
|
703
|
+
},
|
|
704
|
+
},
|
|
705
|
+
];
|
|
706
|
+
//# sourceMappingURL=tools.js.map
|