harper-knowledge 0.1.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.
Files changed (69) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +276 -0
  3. package/config.yaml +17 -0
  4. package/dist/core/embeddings.d.ts +29 -0
  5. package/dist/core/embeddings.js +199 -0
  6. package/dist/core/entries.d.ts +85 -0
  7. package/dist/core/entries.js +235 -0
  8. package/dist/core/history.d.ts +30 -0
  9. package/dist/core/history.js +119 -0
  10. package/dist/core/search.d.ts +23 -0
  11. package/dist/core/search.js +306 -0
  12. package/dist/core/tags.d.ts +32 -0
  13. package/dist/core/tags.js +76 -0
  14. package/dist/core/triage.d.ts +55 -0
  15. package/dist/core/triage.js +126 -0
  16. package/dist/http-utils.d.ts +37 -0
  17. package/dist/http-utils.js +132 -0
  18. package/dist/index.d.ts +21 -0
  19. package/dist/index.js +76 -0
  20. package/dist/mcp/server.d.ts +24 -0
  21. package/dist/mcp/server.js +124 -0
  22. package/dist/mcp/tools.d.ts +13 -0
  23. package/dist/mcp/tools.js +497 -0
  24. package/dist/oauth/authorize.d.ts +27 -0
  25. package/dist/oauth/authorize.js +438 -0
  26. package/dist/oauth/github.d.ts +28 -0
  27. package/dist/oauth/github.js +62 -0
  28. package/dist/oauth/keys.d.ts +33 -0
  29. package/dist/oauth/keys.js +100 -0
  30. package/dist/oauth/metadata.d.ts +21 -0
  31. package/dist/oauth/metadata.js +55 -0
  32. package/dist/oauth/middleware.d.ts +22 -0
  33. package/dist/oauth/middleware.js +64 -0
  34. package/dist/oauth/register.d.ts +14 -0
  35. package/dist/oauth/register.js +83 -0
  36. package/dist/oauth/token.d.ts +15 -0
  37. package/dist/oauth/token.js +178 -0
  38. package/dist/oauth/validate.d.ts +30 -0
  39. package/dist/oauth/validate.js +52 -0
  40. package/dist/resources/HistoryResource.d.ts +38 -0
  41. package/dist/resources/HistoryResource.js +38 -0
  42. package/dist/resources/KnowledgeEntryResource.d.ts +64 -0
  43. package/dist/resources/KnowledgeEntryResource.js +157 -0
  44. package/dist/resources/QueryLogResource.d.ts +20 -0
  45. package/dist/resources/QueryLogResource.js +57 -0
  46. package/dist/resources/ServiceKeyResource.d.ts +51 -0
  47. package/dist/resources/ServiceKeyResource.js +132 -0
  48. package/dist/resources/TagResource.d.ts +25 -0
  49. package/dist/resources/TagResource.js +32 -0
  50. package/dist/resources/TriageResource.d.ts +51 -0
  51. package/dist/resources/TriageResource.js +107 -0
  52. package/dist/types.d.ts +317 -0
  53. package/dist/types.js +7 -0
  54. package/dist/webhooks/datadog.d.ts +26 -0
  55. package/dist/webhooks/datadog.js +120 -0
  56. package/dist/webhooks/github.d.ts +24 -0
  57. package/dist/webhooks/github.js +167 -0
  58. package/dist/webhooks/middleware.d.ts +14 -0
  59. package/dist/webhooks/middleware.js +161 -0
  60. package/dist/webhooks/types.d.ts +17 -0
  61. package/dist/webhooks/types.js +4 -0
  62. package/package.json +72 -0
  63. package/schema/knowledge.graphql +134 -0
  64. package/web/index.html +735 -0
  65. package/web/js/app.js +461 -0
  66. package/web/js/detail.js +223 -0
  67. package/web/js/editor.js +303 -0
  68. package/web/js/search.js +238 -0
  69. package/web/js/triage.js +305 -0
@@ -0,0 +1,306 @@
1
+ /**
2
+ * Knowledge Base Search
3
+ *
4
+ * Supports keyword, semantic (vector), and hybrid search modes.
5
+ * Applies applicability context filtering to boost/demote results.
6
+ * Logs all queries to the QueryLog table for analytics.
7
+ */
8
+ import crypto from "node:crypto";
9
+ import { generateEmbedding } from "./embeddings.js";
10
+ /** Default number of results to return */
11
+ const DEFAULT_LIMIT = 10;
12
+ /** Score boost factor for applicability matches */
13
+ const APPLICABILITY_BOOST = 1.2;
14
+ /** Score penalty factor for applicability mismatches */
15
+ const APPLICABILITY_PENALTY = 0.8;
16
+ /**
17
+ * Search the knowledge base.
18
+ *
19
+ * @param params - Search parameters including query, mode, tags, limit, context
20
+ * @returns Scored and sorted search results
21
+ */
22
+ export async function search(params) {
23
+ const { query, tags, limit = DEFAULT_LIMIT, context, mode = "hybrid", } = params;
24
+ let results;
25
+ switch (mode) {
26
+ case "keyword":
27
+ results = await keywordSearch(query, tags, limit);
28
+ break;
29
+ case "semantic":
30
+ results = await semanticSearch(query, limit);
31
+ break;
32
+ case "hybrid":
33
+ default:
34
+ results = await hybridSearch(query, tags, limit);
35
+ break;
36
+ }
37
+ // Apply applicability filtering (boost/demote based on context)
38
+ if (context) {
39
+ results = filterByApplicability(results, context);
40
+ }
41
+ // Re-sort by score after applicability adjustments
42
+ results.sort((a, b) => b.score - a.score);
43
+ // Trim to limit
44
+ results = results.slice(0, limit);
45
+ // Log the search query for analytics
46
+ await logQuery(query, context, results);
47
+ return results;
48
+ }
49
+ /**
50
+ * Perform keyword-based search using Harper's condition-based search.
51
+ */
52
+ async function keywordSearch(query, tags, limit = DEFAULT_LIMIT) {
53
+ const conditions = [];
54
+ // Search by title containing the query
55
+ conditions.push({ attribute: "title", comparator: "contains", value: query });
56
+ // Filter by tags if provided
57
+ if (tags && tags.length > 0) {
58
+ for (const tag of tags) {
59
+ conditions.push({
60
+ attribute: "tags",
61
+ comparator: "contains",
62
+ value: tag,
63
+ });
64
+ }
65
+ }
66
+ // Run the title search
67
+ const titleResults = await collectResults(databases.kb.KnowledgeEntry.search({
68
+ conditions: [
69
+ { attribute: "title", comparator: "contains", value: query },
70
+ { attribute: "deprecated", comparator: "equals", value: false },
71
+ ],
72
+ limit: limit * 2, // Fetch extra for merging
73
+ }));
74
+ // Also search by content
75
+ const contentResults = await collectResults(databases.kb.KnowledgeEntry.search({
76
+ conditions: [
77
+ { attribute: "content", comparator: "contains", value: query },
78
+ { attribute: "deprecated", comparator: "equals", value: false },
79
+ ],
80
+ limit: limit * 2,
81
+ }));
82
+ // Merge and deduplicate
83
+ const seenIds = new Set();
84
+ const results = [];
85
+ // Title matches get higher score
86
+ for (const entry of titleResults) {
87
+ const typed = entry;
88
+ if (typed.id && !seenIds.has(typed.id)) {
89
+ seenIds.add(typed.id);
90
+ results.push({
91
+ ...typed,
92
+ score: 1.0, // Title match = highest keyword score
93
+ matchType: "keyword",
94
+ });
95
+ }
96
+ }
97
+ // Content matches get lower score
98
+ for (const entry of contentResults) {
99
+ const typed = entry;
100
+ if (typed.id && !seenIds.has(typed.id)) {
101
+ seenIds.add(typed.id);
102
+ results.push({
103
+ ...typed,
104
+ score: 0.7, // Content match = lower keyword score
105
+ matchType: "keyword",
106
+ });
107
+ }
108
+ }
109
+ // Filter by tags if needed (post-filter since conditions are ANDed)
110
+ if (tags && tags.length > 0) {
111
+ return results.filter((r) => {
112
+ const entryTags = r.tags || [];
113
+ return tags.some((tag) => entryTags.includes(tag));
114
+ });
115
+ }
116
+ return results;
117
+ }
118
+ /**
119
+ * Perform semantic (vector) search using HNSW index.
120
+ */
121
+ async function semanticSearch(query, limit = DEFAULT_LIMIT) {
122
+ let queryVector;
123
+ try {
124
+ queryVector = await generateEmbedding(query);
125
+ }
126
+ catch (error) {
127
+ logger?.warn?.("Semantic search failed — embedding model not available:", error.message);
128
+ return [];
129
+ }
130
+ const rawResults = await collectResults(databases.kb.KnowledgeEntry.search({
131
+ sort: { attribute: "embedding", target: queryVector },
132
+ limit: limit * 2, // Fetch extra to allow for deprecated filtering
133
+ }));
134
+ const results = [];
135
+ for (const entry of rawResults) {
136
+ const typed = entry;
137
+ if (typed.deprecated)
138
+ continue; // Skip deprecated entries
139
+ // Calculate cosine similarity score (HNSW returns nearest first)
140
+ // Score decreases with position (1.0 for first result, decreasing)
141
+ const positionScore = 1.0 - results.length / (limit * 2);
142
+ results.push({
143
+ ...typed,
144
+ score: Math.max(0.1, positionScore),
145
+ matchType: "semantic",
146
+ });
147
+ if (results.length >= limit)
148
+ break;
149
+ }
150
+ return results;
151
+ }
152
+ /**
153
+ * Perform hybrid search: run both keyword and semantic, merge and deduplicate.
154
+ */
155
+ async function hybridSearch(query, tags, limit = DEFAULT_LIMIT) {
156
+ // Run both searches in parallel
157
+ const [keywordResults, semanticResults] = await Promise.all([
158
+ keywordSearch(query, tags, limit),
159
+ semanticSearch(query, limit),
160
+ ]);
161
+ // Merge and deduplicate
162
+ const resultMap = new Map();
163
+ // Add keyword results
164
+ for (const result of keywordResults) {
165
+ resultMap.set(result.id, result);
166
+ }
167
+ // Merge semantic results (combine scores if entry appears in both)
168
+ for (const result of semanticResults) {
169
+ const existing = resultMap.get(result.id);
170
+ if (existing) {
171
+ // Entry found in both — combine scores and mark as hybrid
172
+ existing.score = ((existing.score + result.score) / 2) * 1.3; // Boost for appearing in both
173
+ existing.matchType = "hybrid";
174
+ }
175
+ else {
176
+ resultMap.set(result.id, result);
177
+ }
178
+ }
179
+ const merged = Array.from(resultMap.values());
180
+ merged.sort((a, b) => b.score - a.score);
181
+ return merged.slice(0, limit);
182
+ }
183
+ /**
184
+ * Filter and re-score results based on applicability context.
185
+ *
186
+ * If the caller provides their environment context (Harper version, storage engine,
187
+ * Node version, platform), results that match get a score boost, while results
188
+ * that specify a different scope get a score penalty (but are NOT hidden).
189
+ */
190
+ export function filterByApplicability(results, context) {
191
+ return results.map((result) => {
192
+ const appliesTo = result.appliesTo;
193
+ if (!appliesTo) {
194
+ // No applicability scope — no adjustment
195
+ return result;
196
+ }
197
+ let matchCount = 0;
198
+ let mismatchCount = 0;
199
+ let totalFields = 0;
200
+ // Check each applicability field
201
+ if (appliesTo.harper && context.harper) {
202
+ totalFields++;
203
+ if (versionMatches(context.harper, appliesTo.harper)) {
204
+ matchCount++;
205
+ }
206
+ else {
207
+ mismatchCount++;
208
+ }
209
+ }
210
+ if (appliesTo.storageEngine && context.storageEngine) {
211
+ totalFields++;
212
+ if (context.storageEngine === appliesTo.storageEngine) {
213
+ matchCount++;
214
+ }
215
+ else {
216
+ mismatchCount++;
217
+ }
218
+ }
219
+ if (appliesTo.node && context.node) {
220
+ totalFields++;
221
+ if (versionMatches(context.node, appliesTo.node)) {
222
+ matchCount++;
223
+ }
224
+ else {
225
+ mismatchCount++;
226
+ }
227
+ }
228
+ if (appliesTo.platform && context.platform) {
229
+ totalFields++;
230
+ if (context.platform === appliesTo.platform) {
231
+ matchCount++;
232
+ }
233
+ else {
234
+ mismatchCount++;
235
+ }
236
+ }
237
+ if (totalFields === 0) {
238
+ return result; // No overlapping fields to compare
239
+ }
240
+ // Adjust score based on matches vs mismatches
241
+ let adjustedScore = result.score;
242
+ if (matchCount > 0) {
243
+ adjustedScore *= APPLICABILITY_BOOST;
244
+ }
245
+ if (mismatchCount > 0) {
246
+ adjustedScore *= APPLICABILITY_PENALTY;
247
+ }
248
+ return { ...result, score: adjustedScore };
249
+ });
250
+ }
251
+ /**
252
+ * Simple version matching.
253
+ * Supports exact match and basic semver range prefixes (>=, <=, ~, ^).
254
+ * For production use, consider a proper semver library.
255
+ */
256
+ function versionMatches(actual, required) {
257
+ // Strip common prefixes for comparison
258
+ const cleanActual = actual.replace(/^[v=]/, "");
259
+ const cleanRequired = required.replace(/^[v=]/, "");
260
+ // Exact match
261
+ if (cleanActual === cleanRequired)
262
+ return true;
263
+ // Range prefix checks (simplified)
264
+ if (required.startsWith(">=")) {
265
+ return cleanActual >= required.slice(2);
266
+ }
267
+ if (required.startsWith("<=")) {
268
+ return cleanActual <= required.slice(2);
269
+ }
270
+ // For ~ and ^ ranges, just check major.minor match
271
+ if (required.startsWith("~") || required.startsWith("^")) {
272
+ const reqParts = required.slice(1).split(".");
273
+ const actParts = cleanActual.split(".");
274
+ return (reqParts[0] === actParts[0] &&
275
+ (reqParts.length < 2 || reqParts[1] === actParts[1]));
276
+ }
277
+ return false;
278
+ }
279
+ /**
280
+ * Log a search query to the QueryLog table for analytics.
281
+ */
282
+ async function logQuery(query, context, results) {
283
+ try {
284
+ await databases.kb.QueryLog.put({
285
+ id: crypto.randomUUID(),
286
+ query,
287
+ context: context || null,
288
+ resultCount: results.length,
289
+ topResultId: results.length > 0 ? results[0].id : null,
290
+ });
291
+ }
292
+ catch (error) {
293
+ // Don't fail the search if logging fails
294
+ logger?.warn?.("Failed to log search query:", error.message);
295
+ }
296
+ }
297
+ /**
298
+ * Collect all results from an async iterable into an array.
299
+ */
300
+ async function collectResults(iterable) {
301
+ const results = [];
302
+ for await (const item of iterable) {
303
+ results.push(item);
304
+ }
305
+ return results;
306
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Tag Management
3
+ *
4
+ * Manages knowledge base tags, including listing and synchronizing
5
+ * tag counts when entries are created, updated, or deleted.
6
+ */
7
+ import type { KnowledgeTag } from "../types.ts";
8
+ /**
9
+ * List knowledge tags.
10
+ *
11
+ * @param limit - Maximum number of tags to return (default 500)
12
+ * @returns Tags from the KnowledgeTag table
13
+ */
14
+ export declare function listTags(limit?: number): Promise<KnowledgeTag[]>;
15
+ /**
16
+ * Get a single tag by name (ID).
17
+ *
18
+ * @param tagName - Tag name (serves as the primary key)
19
+ * @returns The tag record, or null if not found
20
+ */
21
+ export declare function getTag(tagName: string): Promise<KnowledgeTag | null>;
22
+ /**
23
+ * Synchronize tag counts when entries are created, updated, or deleted.
24
+ *
25
+ * For tags added (in newTags but not in previousTags), increment entryCount
26
+ * or create the tag with count 1. For tags removed (in previousTags but not
27
+ * in newTags), decrement entryCount.
28
+ *
29
+ * @param newTags - Tags on the entry after the change
30
+ * @param previousTags - Tags on the entry before the change (empty for new entries)
31
+ */
32
+ export declare function syncTags(newTags: string[], previousTags?: string[]): Promise<void>;
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Tag Management
3
+ *
4
+ * Manages knowledge base tags, including listing and synchronizing
5
+ * tag counts when entries are created, updated, or deleted.
6
+ */
7
+ /**
8
+ * List knowledge tags.
9
+ *
10
+ * @param limit - Maximum number of tags to return (default 500)
11
+ * @returns Tags from the KnowledgeTag table
12
+ */
13
+ export async function listTags(limit = 500) {
14
+ const results = [];
15
+ for await (const tag of databases.kb.KnowledgeTag.search({ limit })) {
16
+ results.push(tag);
17
+ }
18
+ return results;
19
+ }
20
+ /**
21
+ * Get a single tag by name (ID).
22
+ *
23
+ * @param tagName - Tag name (serves as the primary key)
24
+ * @returns The tag record, or null if not found
25
+ */
26
+ export async function getTag(tagName) {
27
+ const tag = await databases.kb.KnowledgeTag.get(tagName);
28
+ return tag;
29
+ }
30
+ /**
31
+ * Synchronize tag counts when entries are created, updated, or deleted.
32
+ *
33
+ * For tags added (in newTags but not in previousTags), increment entryCount
34
+ * or create the tag with count 1. For tags removed (in previousTags but not
35
+ * in newTags), decrement entryCount.
36
+ *
37
+ * @param newTags - Tags on the entry after the change
38
+ * @param previousTags - Tags on the entry before the change (empty for new entries)
39
+ */
40
+ export async function syncTags(newTags, previousTags) {
41
+ const prev = new Set(previousTags || []);
42
+ const next = new Set(newTags);
43
+ // Tags that were added
44
+ const added = newTags.filter((tag) => !prev.has(tag));
45
+ // Tags that were removed
46
+ const removed = (previousTags || []).filter((tag) => !next.has(tag));
47
+ // Increment counts for added tags
48
+ for (const tagName of added) {
49
+ const existing = await databases.kb.KnowledgeTag.get(tagName);
50
+ if (existing) {
51
+ await databases.kb.KnowledgeTag.put({
52
+ ...existing,
53
+ id: tagName,
54
+ entryCount: (existing.entryCount || 0) + 1,
55
+ });
56
+ }
57
+ else {
58
+ await databases.kb.KnowledgeTag.put({
59
+ id: tagName,
60
+ entryCount: 1,
61
+ });
62
+ }
63
+ }
64
+ // Decrement counts for removed tags
65
+ for (const tagName of removed) {
66
+ const existing = await databases.kb.KnowledgeTag.get(tagName);
67
+ if (existing) {
68
+ const currentCount = existing.entryCount || 0;
69
+ await databases.kb.KnowledgeTag.put({
70
+ ...existing,
71
+ id: tagName,
72
+ entryCount: Math.max(0, currentCount - 1),
73
+ });
74
+ }
75
+ }
76
+ }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Triage Queue Management
3
+ *
4
+ * Handles the intake queue for new knowledge submissions from webhooks
5
+ * and other sources. Items go through pending -> processing -> accepted/dismissed.
6
+ */
7
+ import type { TriageItem, TriageAction, TriageProcessOptions } from "../types.ts";
8
+ /**
9
+ * Submit a new item to the triage queue.
10
+ *
11
+ * @param source - Source identifier (e.g., "github-webhook", "slack-bot", "manual")
12
+ * @param summary - Brief summary of the knowledge to triage
13
+ * @param rawPayload - Original raw payload from the source
14
+ * @param sourceId - Optional deduplication key from the source system
15
+ * @returns The created triage item
16
+ */
17
+ export declare function submitTriage(source: string, summary: string, rawPayload?: unknown, sourceId?: string): Promise<TriageItem>;
18
+ /**
19
+ * Find a triage item by its source-specific ID for deduplication.
20
+ *
21
+ * @param sourceId - The source-specific identifier
22
+ * @returns The matching triage item, or null if not found
23
+ */
24
+ export declare function findBySourceId(sourceId: string): Promise<TriageItem | null>;
25
+ /**
26
+ * Process a triage item with the given action.
27
+ *
28
+ * - "accepted": Optionally creates a new knowledge entry from provided data
29
+ * - "dismissed": Marks the item as dismissed
30
+ * - "linked": Links the triage item to an existing knowledge entry
31
+ *
32
+ * @param id - Triage item ID
33
+ * @param action - Action to take
34
+ * @param processedBy - Who processed this item
35
+ * @param options - Additional options (entry data for accept, linked entry ID)
36
+ * @returns The updated triage item
37
+ * @throws Error if the triage item does not exist
38
+ */
39
+ export declare function processTriage(id: string, action: TriageAction, processedBy: string, options?: TriageProcessOptions): Promise<TriageItem>;
40
+ /**
41
+ * List pending triage items.
42
+ *
43
+ * @param limit - Maximum number of items to return (default 200)
44
+ * @returns Array of triage items with status "pending"
45
+ */
46
+ export declare function listPending(limit?: number): Promise<TriageItem[]>;
47
+ /**
48
+ * Dismiss a triage item.
49
+ *
50
+ * Convenience method that calls processTriage with action "dismissed".
51
+ *
52
+ * @param id - Triage item ID
53
+ * @param processedBy - Who dismissed this item
54
+ */
55
+ export declare function dismissTriage(id: string, processedBy: string): Promise<void>;
@@ -0,0 +1,126 @@
1
+ /**
2
+ * Triage Queue Management
3
+ *
4
+ * Handles the intake queue for new knowledge submissions from webhooks
5
+ * and other sources. Items go through pending -> processing -> accepted/dismissed.
6
+ */
7
+ import crypto from "node:crypto";
8
+ import { createEntry } from "./entries.js";
9
+ /**
10
+ * Submit a new item to the triage queue.
11
+ *
12
+ * @param source - Source identifier (e.g., "github-webhook", "slack-bot", "manual")
13
+ * @param summary - Brief summary of the knowledge to triage
14
+ * @param rawPayload - Original raw payload from the source
15
+ * @param sourceId - Optional deduplication key from the source system
16
+ * @returns The created triage item
17
+ */
18
+ export async function submitTriage(source, summary, rawPayload, sourceId) {
19
+ const item = {
20
+ id: crypto.randomUUID(),
21
+ source,
22
+ summary,
23
+ rawPayload: rawPayload || null,
24
+ status: "pending",
25
+ sourceId: sourceId || undefined,
26
+ };
27
+ await databases.kb.TriageItem.put(item);
28
+ logger?.info?.(`Triage item submitted: ${item.id} from ${source}`);
29
+ return item;
30
+ }
31
+ /**
32
+ * Find a triage item by its source-specific ID for deduplication.
33
+ *
34
+ * @param sourceId - The source-specific identifier
35
+ * @returns The matching triage item, or null if not found
36
+ */
37
+ export async function findBySourceId(sourceId) {
38
+ for await (const item of databases.kb.TriageItem.search({
39
+ conditions: [
40
+ { attribute: "sourceId", comparator: "equals", value: sourceId },
41
+ ],
42
+ limit: 1,
43
+ })) {
44
+ return item;
45
+ }
46
+ return null;
47
+ }
48
+ /**
49
+ * Process a triage item with the given action.
50
+ *
51
+ * - "accepted": Optionally creates a new knowledge entry from provided data
52
+ * - "dismissed": Marks the item as dismissed
53
+ * - "linked": Links the triage item to an existing knowledge entry
54
+ *
55
+ * @param id - Triage item ID
56
+ * @param action - Action to take
57
+ * @param processedBy - Who processed this item
58
+ * @param options - Additional options (entry data for accept, linked entry ID)
59
+ * @returns The updated triage item
60
+ * @throws Error if the triage item does not exist
61
+ */
62
+ export async function processTriage(id, action, processedBy, options) {
63
+ const existing = await databases.kb.TriageItem.get(id);
64
+ if (!existing) {
65
+ throw new Error(`Triage item not found: ${id}`);
66
+ }
67
+ const item = existing;
68
+ const now = new Date();
69
+ // Update common fields
70
+ item.status = action;
71
+ item.action = action;
72
+ item.processedBy = processedBy;
73
+ item.processedAt = now;
74
+ // Handle action-specific logic
75
+ if (action === "accepted" && options?.entryData) {
76
+ // Create a new knowledge entry from the triage data
77
+ const entryData = {
78
+ ...options.entryData,
79
+ source: options.entryData.source || item.source,
80
+ addedBy: options.entryData.addedBy || processedBy,
81
+ };
82
+ const entry = await createEntry(entryData);
83
+ item.draftEntryId = entry.id;
84
+ }
85
+ else if (action === "accepted" && options?.linkedEntryId) {
86
+ // Entry was created externally (e.g., web UI created it first) — link it
87
+ item.draftEntryId = options.linkedEntryId;
88
+ }
89
+ else if (action === "linked" && options?.linkedEntryId) {
90
+ // Link to an existing knowledge entry
91
+ item.matchedEntryId = options.linkedEntryId;
92
+ }
93
+ // Store the updated triage item
94
+ await databases.kb.TriageItem.put(item);
95
+ logger?.info?.(`Triage item ${id} processed: ${action} by ${processedBy}`);
96
+ return item;
97
+ }
98
+ /**
99
+ * List pending triage items.
100
+ *
101
+ * @param limit - Maximum number of items to return (default 200)
102
+ * @returns Array of triage items with status "pending"
103
+ */
104
+ export async function listPending(limit = 200) {
105
+ const results = [];
106
+ for await (const item of databases.kb.TriageItem.search({
107
+ conditions: [
108
+ { attribute: "status", comparator: "equals", value: "pending" },
109
+ ],
110
+ limit,
111
+ })) {
112
+ results.push(item);
113
+ }
114
+ return results;
115
+ }
116
+ /**
117
+ * Dismiss a triage item.
118
+ *
119
+ * Convenience method that calls processTriage with action "dismissed".
120
+ *
121
+ * @param id - Triage item ID
122
+ * @param processedBy - Who dismissed this item
123
+ */
124
+ export async function dismissTriage(id, processedBy) {
125
+ await processTriage(id, "dismissed", processedBy);
126
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * HTTP Utilities for Harper's stream-based request/response handling.
3
+ *
4
+ * Shared by MCP middleware, OAuth middleware, and webhook middleware.
5
+ */
6
+ import type { HarperRequest } from "./types.ts";
7
+ /**
8
+ * Read the request body as a string from Harper's stream-based body.
9
+ *
10
+ * Harper's request.body is a RequestBody wrapper with .on()/.pipe() methods,
11
+ * not a parsed object. We need to consume the stream to get the raw text.
12
+ *
13
+ * Enforces a maximum body size to prevent memory exhaustion from oversized requests.
14
+ */
15
+ export declare function readBody(request: HarperRequest): Promise<string>;
16
+ /**
17
+ * Build Web Standard Headers from Harper's Headers object.
18
+ *
19
+ * Harper's request.headers is a custom Headers class (iterable, with .get()),
20
+ * not a plain Record<string, string>.
21
+ */
22
+ export declare function buildHeaders(request: HarperRequest): Headers;
23
+ /**
24
+ * Build the base URL (origin) from a Harper request.
25
+ *
26
+ * Uses the request's protocol and host properties. Defaults to
27
+ * http://localhost:9926 if not available.
28
+ */
29
+ export declare function getBaseUrl(request: HarperRequest): string;
30
+ /**
31
+ * Parse an application/x-www-form-urlencoded body into a key-value map.
32
+ */
33
+ export declare function parseFormBody(body: string): Record<string, string>;
34
+ /**
35
+ * Get a header value from Harper's request, case-insensitive.
36
+ */
37
+ export declare function getHeader(request: HarperRequest, name: string): string;