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,235 @@
1
+ /**
2
+ * Knowledge Entry Management
3
+ *
4
+ * CRUD operations for knowledge base entries. Handles embedding generation,
5
+ * tag synchronization, and relationship management.
6
+ */
7
+ import crypto from "node:crypto";
8
+ import { generateEmbedding } from "./embeddings.js";
9
+ import { syncTags } from "./tags.js";
10
+ import { logEdit } from "./history.js";
11
+ /**
12
+ * Strip embedding vectors from an entry to keep responses compact.
13
+ * Embeddings are large float arrays not useful in API responses.
14
+ */
15
+ export function stripEmbedding(entry) {
16
+ const { embedding: _embedding, ...rest } = entry;
17
+ return rest;
18
+ }
19
+ /**
20
+ * Create a new knowledge entry.
21
+ *
22
+ * Generates an embedding from title + content, synchronizes tags,
23
+ * and stores the entry. A UUID is generated if no id is provided.
24
+ *
25
+ * @param data - Entry data to create
26
+ * @returns The created knowledge entry
27
+ */
28
+ export async function createEntry(data) {
29
+ const id = data.id || crypto.randomUUID();
30
+ // Generate embedding from title + content
31
+ const embeddingText = `${data.title}\n\n${data.content}`;
32
+ let embedding;
33
+ try {
34
+ embedding = await generateEmbedding(embeddingText);
35
+ }
36
+ catch (error) {
37
+ logger?.warn?.("Failed to generate embedding for new entry:", error.message);
38
+ }
39
+ const entry = {
40
+ id,
41
+ title: data.title,
42
+ content: data.content,
43
+ tags: data.tags || [],
44
+ appliesTo: data.appliesTo,
45
+ source: data.source,
46
+ sourceUrl: data.sourceUrl,
47
+ confidence: data.confidence || "ai-generated",
48
+ addedBy: data.addedBy,
49
+ reviewedBy: data.reviewedBy,
50
+ customerContext: data.customerContext,
51
+ deprecated: data.deprecated ?? false,
52
+ embedding,
53
+ };
54
+ // Sync tag counts (no previous tags for new entries)
55
+ if (entry.tags.length > 0) {
56
+ await syncTags(entry.tags);
57
+ }
58
+ // Store the entry
59
+ await databases.kb.KnowledgeEntry.put(entry);
60
+ return entry;
61
+ }
62
+ /**
63
+ * Get a knowledge entry by ID.
64
+ *
65
+ * @param id - Entry ID
66
+ * @returns The entry, or null if not found
67
+ */
68
+ export async function getEntry(id) {
69
+ const entry = await databases.kb.KnowledgeEntry.get(id);
70
+ return entry;
71
+ }
72
+ /**
73
+ * Update an existing knowledge entry.
74
+ *
75
+ * Merges the update data with the existing entry. If title or content changed,
76
+ * regenerates the embedding. Synchronizes tag counts if tags changed.
77
+ * Optionally logs the edit to the history table.
78
+ *
79
+ * @param id - ID of the entry to update
80
+ * @param data - Fields to update
81
+ * @param options - Optional edit tracking metadata
82
+ * @returns The updated entry
83
+ * @throws Error if the entry does not exist
84
+ */
85
+ export async function updateEntry(id, data, options) {
86
+ const existing = await databases.kb.KnowledgeEntry.get(id);
87
+ if (!existing) {
88
+ throw new Error(`Knowledge entry not found: ${id}`);
89
+ }
90
+ const existingEntry = existing;
91
+ const previousTags = existingEntry.tags || [];
92
+ // Merge updates
93
+ const updated = {
94
+ ...existingEntry,
95
+ ...data,
96
+ id, // Ensure ID is never overwritten
97
+ };
98
+ // Regenerate embedding if title or content changed
99
+ const titleChanged = data.title !== undefined && data.title !== existingEntry.title;
100
+ const contentChanged = data.content !== undefined && data.content !== existingEntry.content;
101
+ if (titleChanged || contentChanged) {
102
+ const embeddingText = `${updated.title}\n\n${updated.content}`;
103
+ try {
104
+ updated.embedding = await generateEmbedding(embeddingText);
105
+ }
106
+ catch (error) {
107
+ logger?.warn?.("Failed to regenerate embedding for entry update:", error.message);
108
+ }
109
+ }
110
+ // Sync tag counts if tags changed
111
+ if (data.tags !== undefined) {
112
+ await syncTags(updated.tags, previousTags);
113
+ }
114
+ // Log the edit before overwriting
115
+ if (options?.editedBy) {
116
+ try {
117
+ await logEdit(id, existingEntry, updated, options.editedBy, options.editSummary);
118
+ }
119
+ catch (error) {
120
+ logger?.warn?.("Failed to log edit history:", error.message);
121
+ }
122
+ }
123
+ // Store the updated entry
124
+ await databases.kb.KnowledgeEntry.put(updated);
125
+ return updated;
126
+ }
127
+ /**
128
+ * Mark an entry as deprecated.
129
+ *
130
+ * @param id - ID of the entry to deprecate
131
+ * @throws Error if the entry does not exist
132
+ */
133
+ export async function deprecateEntry(id) {
134
+ const existing = await databases.kb.KnowledgeEntry.get(id);
135
+ if (!existing) {
136
+ throw new Error(`Knowledge entry not found: ${id}`);
137
+ }
138
+ await databases.kb.KnowledgeEntry.put({
139
+ ...existing,
140
+ id,
141
+ deprecated: true,
142
+ });
143
+ }
144
+ /**
145
+ * Link a new entry as superseding an old entry.
146
+ *
147
+ * Sets newEntry.supersedesId = oldId and oldEntry.supersededById = newId.
148
+ *
149
+ * @param newId - ID of the new (superseding) entry
150
+ * @param oldId - ID of the old (superseded) entry
151
+ * @throws Error if either entry does not exist
152
+ */
153
+ export async function linkSupersedes(newId, oldId) {
154
+ const newEntry = await databases.kb.KnowledgeEntry.get(newId);
155
+ const oldEntry = await databases.kb.KnowledgeEntry.get(oldId);
156
+ if (!newEntry) {
157
+ throw new Error(`New entry not found: ${newId}`);
158
+ }
159
+ if (!oldEntry) {
160
+ throw new Error(`Old entry not found: ${oldId}`);
161
+ }
162
+ await databases.kb.KnowledgeEntry.put({
163
+ ...newEntry,
164
+ id: newId,
165
+ supersedesId: oldId,
166
+ });
167
+ await databases.kb.KnowledgeEntry.put({
168
+ ...oldEntry,
169
+ id: oldId,
170
+ supersededById: newId,
171
+ });
172
+ }
173
+ /**
174
+ * Link multiple entries as siblings.
175
+ *
176
+ * For each entry, adds all other entry IDs to its siblingIds array (deduplicated).
177
+ *
178
+ * @param ids - IDs of entries to link as siblings
179
+ * @throws Error if any entry does not exist
180
+ */
181
+ export async function linkSiblings(ids) {
182
+ if (ids.length < 2) {
183
+ return; // Need at least 2 entries to link
184
+ }
185
+ // Load all entries first to verify they exist
186
+ const entries = [];
187
+ for (const id of ids) {
188
+ const entry = await databases.kb.KnowledgeEntry.get(id);
189
+ if (!entry) {
190
+ throw new Error(`Entry not found: ${id}`);
191
+ }
192
+ entries.push(entry);
193
+ }
194
+ // Update each entry's siblingIds
195
+ for (let i = 0; i < ids.length; i++) {
196
+ const entry = entries[i];
197
+ const entryTyped = entry;
198
+ const existingSiblings = new Set(entryTyped.siblingIds || []);
199
+ // Add all other IDs (not itself)
200
+ for (const otherId of ids) {
201
+ if (otherId !== ids[i]) {
202
+ existingSiblings.add(otherId);
203
+ }
204
+ }
205
+ await databases.kb.KnowledgeEntry.put({
206
+ ...entry,
207
+ id: ids[i],
208
+ siblingIds: Array.from(existingSiblings),
209
+ });
210
+ }
211
+ }
212
+ /**
213
+ * Link two entries as related.
214
+ *
215
+ * Adds relatedId to the entry's relatedIds array (deduplicated).
216
+ * This is a one-directional link; call twice for bidirectional.
217
+ *
218
+ * @param id - ID of the entry to add a related link to
219
+ * @param relatedId - ID of the related entry
220
+ * @throws Error if the entry does not exist
221
+ */
222
+ export async function linkRelated(id, relatedId) {
223
+ const entry = await databases.kb.KnowledgeEntry.get(id);
224
+ if (!entry) {
225
+ throw new Error(`Entry not found: ${id}`);
226
+ }
227
+ const entryTyped = entry;
228
+ const existingRelated = new Set(entryTyped.relatedIds || []);
229
+ existingRelated.add(relatedId);
230
+ await databases.kb.KnowledgeEntry.put({
231
+ ...entry,
232
+ id,
233
+ relatedIds: Array.from(existingRelated),
234
+ });
235
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Edit History Tracking
3
+ *
4
+ * Append-only audit log for knowledge entry edits.
5
+ * Records who changed what, when, and why — with a snapshot
6
+ * of the previous state for diffing.
7
+ */
8
+ import type { KnowledgeEntry, KnowledgeEntryEdit } from "../types.ts";
9
+ /**
10
+ * Log an edit to a knowledge entry.
11
+ *
12
+ * Compares the previous and updated entries to determine which fields changed,
13
+ * then stores an edit record with the previous snapshot.
14
+ *
15
+ * @param entryId - ID of the entry that was edited
16
+ * @param previous - The entry state before the edit
17
+ * @param updated - The entry state after the edit
18
+ * @param editedBy - Username of who made the edit
19
+ * @param editSummary - Optional description of what changed and why
20
+ * @returns The created edit record
21
+ */
22
+ export declare function logEdit(entryId: string, previous: KnowledgeEntry, updated: KnowledgeEntry, editedBy: string, editSummary?: string): Promise<KnowledgeEntryEdit>;
23
+ /**
24
+ * Get the edit history for a knowledge entry, newest first.
25
+ *
26
+ * @param entryId - ID of the entry to get history for
27
+ * @param limit - Maximum number of edits to return (default 50)
28
+ * @returns Array of edit records, newest first
29
+ */
30
+ export declare function getHistory(entryId: string, limit?: number): Promise<KnowledgeEntryEdit[]>;
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Edit History Tracking
3
+ *
4
+ * Append-only audit log for knowledge entry edits.
5
+ * Records who changed what, when, and why — with a snapshot
6
+ * of the previous state for diffing.
7
+ */
8
+ import crypto from "node:crypto";
9
+ /**
10
+ * Log an edit to a knowledge entry.
11
+ *
12
+ * Compares the previous and updated entries to determine which fields changed,
13
+ * then stores an edit record with the previous snapshot.
14
+ *
15
+ * @param entryId - ID of the entry that was edited
16
+ * @param previous - The entry state before the edit
17
+ * @param updated - The entry state after the edit
18
+ * @param editedBy - Username of who made the edit
19
+ * @param editSummary - Optional description of what changed and why
20
+ * @returns The created edit record
21
+ */
22
+ export async function logEdit(entryId, previous, updated, editedBy, editSummary) {
23
+ // Determine which fields actually changed
24
+ const changedFields = detectChangedFields(previous, updated);
25
+ // Build a snapshot of only the previous values for changed fields
26
+ const previousSnapshot = {};
27
+ for (const field of changedFields) {
28
+ previousSnapshot[field] = previous[field];
29
+ }
30
+ const edit = {
31
+ id: crypto.randomUUID(),
32
+ entryId,
33
+ editedBy,
34
+ editSummary,
35
+ previousSnapshot,
36
+ changedFields,
37
+ };
38
+ await databases.kb.KnowledgeEntryEdit.put(edit);
39
+ return edit;
40
+ }
41
+ /**
42
+ * Get the edit history for a knowledge entry, newest first.
43
+ *
44
+ * @param entryId - ID of the entry to get history for
45
+ * @param limit - Maximum number of edits to return (default 50)
46
+ * @returns Array of edit records, newest first
47
+ */
48
+ export async function getHistory(entryId, limit = 50) {
49
+ const edits = [];
50
+ for await (const record of databases.kb.KnowledgeEntryEdit.search({
51
+ conditions: [
52
+ { attribute: "entryId", comparator: "equals", value: entryId },
53
+ ],
54
+ sort: { attribute: "createdAt", descending: true },
55
+ limit,
56
+ })) {
57
+ edits.push(record);
58
+ }
59
+ return edits;
60
+ }
61
+ /**
62
+ * Compare two entry states and return the list of fields that differ.
63
+ * Ignores internal fields like embedding and updatedAt.
64
+ */
65
+ function detectChangedFields(previous, updated) {
66
+ const trackableFields = [
67
+ "title",
68
+ "content",
69
+ "tags",
70
+ "appliesTo",
71
+ "source",
72
+ "sourceUrl",
73
+ "confidence",
74
+ "addedBy",
75
+ "reviewedBy",
76
+ "customerContext",
77
+ "deprecated",
78
+ "supersedesId",
79
+ "supersededById",
80
+ "siblingIds",
81
+ "relatedIds",
82
+ ];
83
+ const changed = [];
84
+ for (const field of trackableFields) {
85
+ const prev = previous[field];
86
+ const next = updated[field];
87
+ if (!deepEqual(prev, next)) {
88
+ changed.push(field);
89
+ }
90
+ }
91
+ return changed;
92
+ }
93
+ /**
94
+ * Simple deep equality check for JSON-serializable values.
95
+ */
96
+ function deepEqual(a, b) {
97
+ if (a === b)
98
+ return true;
99
+ if (a == null || b == null)
100
+ return a === b;
101
+ if (typeof a !== typeof b)
102
+ return false;
103
+ if (Array.isArray(a) && Array.isArray(b)) {
104
+ if (a.length !== b.length)
105
+ return false;
106
+ return a.every((val, i) => deepEqual(val, b[i]));
107
+ }
108
+ if (typeof a === "object" && typeof b === "object") {
109
+ const aObj = a;
110
+ const bObj = b;
111
+ const keys = new Set([...Object.keys(aObj), ...Object.keys(bObj)]);
112
+ for (const key of keys) {
113
+ if (!deepEqual(aObj[key], bObj[key]))
114
+ return false;
115
+ }
116
+ return true;
117
+ }
118
+ return false;
119
+ }
@@ -0,0 +1,23 @@
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 type { SearchParams, SearchResult, ApplicabilityContext } from "../types.ts";
9
+ /**
10
+ * Search the knowledge base.
11
+ *
12
+ * @param params - Search parameters including query, mode, tags, limit, context
13
+ * @returns Scored and sorted search results
14
+ */
15
+ export declare function search(params: SearchParams): Promise<SearchResult[]>;
16
+ /**
17
+ * Filter and re-score results based on applicability context.
18
+ *
19
+ * If the caller provides their environment context (Harper version, storage engine,
20
+ * Node version, platform), results that match get a score boost, while results
21
+ * that specify a different scope get a score penalty (but are NOT hidden).
22
+ */
23
+ export declare function filterByApplicability(results: SearchResult[], context: ApplicabilityContext): SearchResult[];