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.
- package/LICENSE +21 -0
- package/README.md +276 -0
- package/config.yaml +17 -0
- package/dist/core/embeddings.d.ts +29 -0
- package/dist/core/embeddings.js +199 -0
- package/dist/core/entries.d.ts +85 -0
- package/dist/core/entries.js +235 -0
- package/dist/core/history.d.ts +30 -0
- package/dist/core/history.js +119 -0
- package/dist/core/search.d.ts +23 -0
- package/dist/core/search.js +306 -0
- package/dist/core/tags.d.ts +32 -0
- package/dist/core/tags.js +76 -0
- package/dist/core/triage.d.ts +55 -0
- package/dist/core/triage.js +126 -0
- package/dist/http-utils.d.ts +37 -0
- package/dist/http-utils.js +132 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.js +76 -0
- package/dist/mcp/server.d.ts +24 -0
- package/dist/mcp/server.js +124 -0
- package/dist/mcp/tools.d.ts +13 -0
- package/dist/mcp/tools.js +497 -0
- package/dist/oauth/authorize.d.ts +27 -0
- package/dist/oauth/authorize.js +438 -0
- package/dist/oauth/github.d.ts +28 -0
- package/dist/oauth/github.js +62 -0
- package/dist/oauth/keys.d.ts +33 -0
- package/dist/oauth/keys.js +100 -0
- package/dist/oauth/metadata.d.ts +21 -0
- package/dist/oauth/metadata.js +55 -0
- package/dist/oauth/middleware.d.ts +22 -0
- package/dist/oauth/middleware.js +64 -0
- package/dist/oauth/register.d.ts +14 -0
- package/dist/oauth/register.js +83 -0
- package/dist/oauth/token.d.ts +15 -0
- package/dist/oauth/token.js +178 -0
- package/dist/oauth/validate.d.ts +30 -0
- package/dist/oauth/validate.js +52 -0
- package/dist/resources/HistoryResource.d.ts +38 -0
- package/dist/resources/HistoryResource.js +38 -0
- package/dist/resources/KnowledgeEntryResource.d.ts +64 -0
- package/dist/resources/KnowledgeEntryResource.js +157 -0
- package/dist/resources/QueryLogResource.d.ts +20 -0
- package/dist/resources/QueryLogResource.js +57 -0
- package/dist/resources/ServiceKeyResource.d.ts +51 -0
- package/dist/resources/ServiceKeyResource.js +132 -0
- package/dist/resources/TagResource.d.ts +25 -0
- package/dist/resources/TagResource.js +32 -0
- package/dist/resources/TriageResource.d.ts +51 -0
- package/dist/resources/TriageResource.js +107 -0
- package/dist/types.d.ts +317 -0
- package/dist/types.js +7 -0
- package/dist/webhooks/datadog.d.ts +26 -0
- package/dist/webhooks/datadog.js +120 -0
- package/dist/webhooks/github.d.ts +24 -0
- package/dist/webhooks/github.js +167 -0
- package/dist/webhooks/middleware.d.ts +14 -0
- package/dist/webhooks/middleware.js +161 -0
- package/dist/webhooks/types.d.ts +17 -0
- package/dist/webhooks/types.js +4 -0
- package/package.json +72 -0
- package/schema/knowledge.graphql +134 -0
- package/web/index.html +735 -0
- package/web/js/app.js +461 -0
- package/web/js/detail.js +223 -0
- package/web/js/editor.js +303 -0
- package/web/js/search.js +238 -0
- package/web/js/triage.js +305 -0
package/web/js/triage.js
ADDED
|
@@ -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">▶</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
|
+
}
|