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,305 @@
1
+ /**
2
+ * Harper Knowledge Base — Triage Queue
3
+ *
4
+ * Review pending triage items from webhook intake.
5
+ * Accept, dismiss, or link items to existing entries.
6
+ */
7
+
8
+ import {
9
+ api,
10
+ auth,
11
+ navigate,
12
+ escapeHtml,
13
+ showLoginModal,
14
+ formatDate,
15
+ } from "./app.js";
16
+
17
+ /**
18
+ * Render the triage queue page.
19
+ * @param {HTMLElement} container
20
+ */
21
+ export async function render(container) {
22
+ if (!auth.authenticated) {
23
+ container.innerHTML = `
24
+ <div class="empty-state">
25
+ <h2>Authentication Required</h2>
26
+ <p>You must be logged in with a team account to view the triage queue.</p>
27
+ <button id="triage-login" class="primary" style="margin-top: 16px;">Log In</button>
28
+ </div>
29
+ `;
30
+ container
31
+ .querySelector("#triage-login")
32
+ ?.addEventListener("click", () => showLoginModal());
33
+ return;
34
+ }
35
+
36
+ container.innerHTML = '<div class="loading">Loading triage queue...</div>';
37
+
38
+ try {
39
+ const items = await api.get("/Triage/?status=pending");
40
+ const data = Array.isArray(items) ? items : [];
41
+ renderQueue(container, data);
42
+ } catch (err) {
43
+ console.error("Triage load error:", err);
44
+ if (err.status === 401) {
45
+ container.innerHTML = `
46
+ <div class="empty-state">
47
+ <h2>Authentication Required</h2>
48
+ <p>Your session has expired. Please log in again.</p>
49
+ <button id="triage-login" class="primary" style="margin-top: 16px;">Log In</button>
50
+ </div>
51
+ `;
52
+ container
53
+ .querySelector("#triage-login")
54
+ ?.addEventListener("click", () => showLoginModal());
55
+ } else if (err.status === 403) {
56
+ container.innerHTML = `
57
+ <div class="empty-state">
58
+ <h2>Access Denied</h2>
59
+ <p>Triage queue requires team role access.</p>
60
+ </div>
61
+ `;
62
+ } else {
63
+ container.innerHTML = `<div class="status-message error">Failed to load triage queue: ${escapeHtml(err.message)}</div>`;
64
+ }
65
+ }
66
+ }
67
+
68
+ function renderQueue(container, items) {
69
+ if (items.length === 0) {
70
+ container.innerHTML = `
71
+ <h2 style="margin-bottom: 20px;">Triage Queue</h2>
72
+ <div class="empty-state">
73
+ <h2>Queue is empty</h2>
74
+ <p>No pending triage items to review.</p>
75
+ </div>
76
+ `;
77
+ return;
78
+ }
79
+
80
+ container.innerHTML = `
81
+ <div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 20px;">
82
+ <h2>Triage Queue</h2>
83
+ <span style="color: var(--text-dim); font-size: 13px;">${items.length} pending</span>
84
+ </div>
85
+ <div id="triage-list">
86
+ ${items.map(renderTriageCard).join("")}
87
+ </div>
88
+ `;
89
+
90
+ wireTriageEvents(container, items);
91
+ }
92
+
93
+ function renderTriageCard(item) {
94
+ const statusBadge = `<span class="badge badge-${item.status || "pending"}">${escapeHtml(item.status || "pending")}</span>`;
95
+ const sourceBadge = `<span class="badge" style="background: var(--tag-bg); color: var(--tag-text);">${escapeHtml(item.source)}</span>`;
96
+
97
+ return `
98
+ <div class="card" id="triage-${escapeHtml(item.id)}" data-triage-id="${escapeHtml(item.id)}">
99
+ <div style="display: flex; align-items: flex-start; justify-content: space-between; gap: 12px;">
100
+ <div style="flex: 1;">
101
+ <div style="display: flex; align-items: center; gap: 8px; margin-bottom: 8px;">
102
+ ${sourceBadge}
103
+ ${statusBadge}
104
+ ${item.sourceId ? `<span style="font-family: var(--font-mono); font-size: 11px; color: var(--text-dim);">${escapeHtml(item.sourceId)}</span>` : ""}
105
+ </div>
106
+ <div style="font-size: 14px; margin-bottom: 8px;">${escapeHtml(item.summary || "No summary")}</div>
107
+ <div style="font-size: 11px; color: var(--text-dim);">
108
+ Created: ${formatDate(item.createdAt)}
109
+ ${item.matchedEntryId ? ` | Matched: <a href="#entry/${escapeHtml(item.matchedEntryId)}">${escapeHtml(item.matchedEntryId)}</a>` : ""}
110
+ </div>
111
+ </div>
112
+ </div>
113
+
114
+ ${item.rawPayload ? renderPayloadPreview(item.rawPayload) : ""}
115
+
116
+ <div class="triage-actions" data-item-id="${escapeHtml(item.id)}">
117
+ <button class="success triage-accept" data-action="accept">Accept</button>
118
+ <button class="danger triage-dismiss" data-action="dismiss">Dismiss</button>
119
+ <button class="triage-link" data-action="link">Link to Entry</button>
120
+ </div>
121
+
122
+ <div class="triage-link-form" id="link-form-${escapeHtml(item.id)}" style="display: none;">
123
+ <div class="inline-form">
124
+ <input type="text" placeholder="Entry ID to link" class="link-entry-id">
125
+ <button class="primary link-submit">Link</button>
126
+ <button class="link-cancel">Cancel</button>
127
+ </div>
128
+ </div>
129
+ </div>
130
+ `;
131
+ }
132
+
133
+ function renderPayloadPreview(payload) {
134
+ const json =
135
+ typeof payload === "string" ? payload : JSON.stringify(payload, null, 2);
136
+ return `
137
+ <div style="margin-top: 8px;">
138
+ <div class="collapsible-header payload-toggle">
139
+ <span class="arrow">&#9654;</span>
140
+ Raw Payload
141
+ </div>
142
+ <div class="collapsible-body payload-body">
143
+ <div class="json-preview">${escapeHtml(json)}</div>
144
+ </div>
145
+ </div>
146
+ `;
147
+ }
148
+
149
+ function wireTriageEvents(container, items) {
150
+ // Payload toggles
151
+ container.querySelectorAll(".payload-toggle").forEach((toggle) => {
152
+ toggle.addEventListener("click", () => {
153
+ toggle.classList.toggle("open");
154
+ const body = toggle.nextElementSibling;
155
+ body.classList.toggle("open");
156
+ });
157
+ });
158
+
159
+ // Action buttons
160
+ container.querySelectorAll(".triage-actions").forEach((actionsDiv) => {
161
+ const itemId = actionsDiv.dataset.itemId;
162
+
163
+ // Accept
164
+ actionsDiv
165
+ .querySelector(".triage-accept")
166
+ ?.addEventListener("click", () => {
167
+ handleAccept(container, itemId, items);
168
+ });
169
+
170
+ // Dismiss
171
+ actionsDiv
172
+ .querySelector(".triage-dismiss")
173
+ ?.addEventListener("click", () => {
174
+ handleDismiss(container, itemId);
175
+ });
176
+
177
+ // Link — show form
178
+ actionsDiv.querySelector(".triage-link")?.addEventListener("click", () => {
179
+ const form = container.querySelector(`#link-form-${itemId}`);
180
+ if (form) form.style.display = "block";
181
+ });
182
+ });
183
+
184
+ // Link form events
185
+ container.querySelectorAll(".triage-link-form").forEach((form) => {
186
+ const itemId = form.id.replace("link-form-", "");
187
+ const input = form.querySelector(".link-entry-id");
188
+ const submitBtn = form.querySelector(".link-submit");
189
+ const cancelBtn = form.querySelector(".link-cancel");
190
+
191
+ submitBtn?.addEventListener("click", () => {
192
+ const entryId = input?.value?.trim();
193
+ if (entryId) handleLink(container, itemId, entryId);
194
+ });
195
+
196
+ cancelBtn?.addEventListener("click", () => {
197
+ form.style.display = "none";
198
+ });
199
+
200
+ input?.addEventListener("keydown", (e) => {
201
+ if (e.key === "Enter") {
202
+ const entryId = input.value.trim();
203
+ if (entryId) handleLink(container, itemId, entryId);
204
+ }
205
+ });
206
+ });
207
+ }
208
+
209
+ async function handleAccept(container, itemId, items) {
210
+ const item = items.find((i) => i.id === itemId);
211
+ if (!item) return;
212
+
213
+ // Navigate to the add page, pre-populating from the triage summary
214
+ // Store the triage item info in sessionStorage for the editor to pick up
215
+ sessionStorage.setItem(
216
+ "triage-accept",
217
+ JSON.stringify({
218
+ triageId: itemId,
219
+ summary: item.summary,
220
+ source: item.source,
221
+ sourceId: item.sourceId,
222
+ rawPayload: item.rawPayload,
223
+ }),
224
+ );
225
+
226
+ navigate("add");
227
+ }
228
+
229
+ async function handleDismiss(container, itemId) {
230
+ if (!confirm("Dismiss this triage item?")) return;
231
+
232
+ const card = container.querySelector(`#triage-${itemId}`);
233
+ const actionsDiv = card?.querySelector(".triage-actions");
234
+ if (actionsDiv)
235
+ actionsDiv.innerHTML =
236
+ '<span style="color: var(--text-dim);">Processing...</span>';
237
+
238
+ try {
239
+ await api.put(`/Triage/${encodeURIComponent(itemId)}`, {
240
+ action: "dismissed",
241
+ });
242
+
243
+ // Remove the card with a fade
244
+ if (card) {
245
+ card.style.opacity = "0.3";
246
+ card.style.transition = "opacity 0.3s";
247
+ setTimeout(() => card.remove(), 300);
248
+ }
249
+
250
+ // Check if list is now empty
251
+ setTimeout(() => {
252
+ const remaining = container.querySelectorAll(".card");
253
+ if (remaining.length === 0) {
254
+ render(container);
255
+ } else {
256
+ // Update count
257
+ const countEl = container.querySelector(
258
+ 'span[style*="color: var(--text-dim)"]',
259
+ );
260
+ if (countEl) countEl.textContent = `${remaining.length} pending`;
261
+ }
262
+ }, 350);
263
+ } catch (err) {
264
+ console.error("Dismiss error:", err);
265
+ if (actionsDiv) {
266
+ actionsDiv.innerHTML = `<div class="status-message error">Failed: ${escapeHtml(err.message)}</div>`;
267
+ }
268
+ }
269
+ }
270
+
271
+ async function handleLink(container, itemId, entryId) {
272
+ const card = container.querySelector(`#triage-${itemId}`);
273
+ const linkForm = container.querySelector(`#link-form-${itemId}`);
274
+ const actionsDiv = card?.querySelector(".triage-actions");
275
+
276
+ if (linkForm) linkForm.style.display = "none";
277
+ if (actionsDiv)
278
+ actionsDiv.innerHTML =
279
+ '<span style="color: var(--text-dim);">Linking...</span>';
280
+
281
+ try {
282
+ await api.put(`/Triage/${encodeURIComponent(itemId)}`, {
283
+ action: "linked",
284
+ linkedEntryId: entryId,
285
+ });
286
+
287
+ if (card) {
288
+ card.style.opacity = "0.3";
289
+ card.style.transition = "opacity 0.3s";
290
+ setTimeout(() => card.remove(), 300);
291
+ }
292
+
293
+ setTimeout(() => {
294
+ const remaining = container.querySelectorAll(".card");
295
+ if (remaining.length === 0) {
296
+ render(container);
297
+ }
298
+ }, 350);
299
+ } catch (err) {
300
+ console.error("Link error:", err);
301
+ if (actionsDiv) {
302
+ actionsDiv.innerHTML = `<div class="status-message error">Failed: ${escapeHtml(err.message)}</div>`;
303
+ }
304
+ }
305
+ }