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,132 @@
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
+ /** Maximum request body size: 1 MB */
7
+ const MAX_BODY_SIZE = 1_048_576;
8
+ /**
9
+ * Read the request body as a string from Harper's stream-based body.
10
+ *
11
+ * Harper's request.body is a RequestBody wrapper with .on()/.pipe() methods,
12
+ * not a parsed object. We need to consume the stream to get the raw text.
13
+ *
14
+ * Enforces a maximum body size to prevent memory exhaustion from oversized requests.
15
+ */
16
+ export function readBody(request) {
17
+ return new Promise((resolve, reject) => {
18
+ const body = request.body;
19
+ if (!body) {
20
+ resolve("");
21
+ return;
22
+ }
23
+ // If body is already a string (unlikely but handle it)
24
+ if (typeof body === "string") {
25
+ resolve(body);
26
+ return;
27
+ }
28
+ // If body is a stream with .on(), read it
29
+ if (typeof body.on === "function") {
30
+ const chunks = [];
31
+ let totalSize = 0;
32
+ body.on("data", (chunk) => {
33
+ totalSize += chunk.length;
34
+ if (totalSize > MAX_BODY_SIZE) {
35
+ body.destroy?.();
36
+ reject(new Error("Request body too large"));
37
+ return;
38
+ }
39
+ chunks.push(Buffer.from(chunk));
40
+ });
41
+ body.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8")));
42
+ body.on("error", reject);
43
+ return;
44
+ }
45
+ // If body is already an object (parsed), stringify it
46
+ if (typeof body === "object") {
47
+ resolve(JSON.stringify(body));
48
+ return;
49
+ }
50
+ resolve(String(body));
51
+ });
52
+ }
53
+ /**
54
+ * Build Web Standard Headers from Harper's Headers object.
55
+ *
56
+ * Harper's request.headers is a custom Headers class (iterable, with .get()),
57
+ * not a plain Record<string, string>.
58
+ */
59
+ export function buildHeaders(request) {
60
+ const headers = new Headers();
61
+ const src = request.headers;
62
+ if (!src)
63
+ return headers;
64
+ // Harper's Headers class is iterable with [key, value] pairs
65
+ if (typeof src[Symbol.iterator] === "function") {
66
+ for (const [key, value] of src) {
67
+ if (value !== undefined) {
68
+ headers.set(key, Array.isArray(value) ? value.join(", ") : String(value));
69
+ }
70
+ }
71
+ }
72
+ else if (typeof src === "object") {
73
+ // Fallback: plain object
74
+ for (const [key, value] of Object.entries(src)) {
75
+ if (value !== undefined) {
76
+ headers.set(key, Array.isArray(value) ? value.join(", ") : String(value));
77
+ }
78
+ }
79
+ }
80
+ return headers;
81
+ }
82
+ /**
83
+ * Build the base URL (origin) from a Harper request.
84
+ *
85
+ * Uses the request's protocol and host properties. Defaults to
86
+ * http://localhost:9926 if not available.
87
+ */
88
+ export function getBaseUrl(request) {
89
+ const protocol = request.protocol || "http";
90
+ const host = request.host || "localhost:9926";
91
+ return `${protocol}://${host}`;
92
+ }
93
+ /**
94
+ * Parse an application/x-www-form-urlencoded body into a key-value map.
95
+ */
96
+ export function parseFormBody(body) {
97
+ const params = new URLSearchParams(body);
98
+ const result = {};
99
+ for (const [key, value] of params) {
100
+ result[key] = value;
101
+ }
102
+ return result;
103
+ }
104
+ /**
105
+ * Get a header value from Harper's request, case-insensitive.
106
+ */
107
+ export function getHeader(request, name) {
108
+ const headers = request.headers;
109
+ if (!headers)
110
+ return "";
111
+ // Try .get() method (Harper's Headers class)
112
+ if (typeof headers.get === "function") {
113
+ const val = headers.get(name);
114
+ if (val !== undefined && val !== null)
115
+ return String(val);
116
+ }
117
+ // Try direct access (case-sensitive)
118
+ const direct = headers[name] ?? headers[name.toLowerCase()];
119
+ if (direct !== undefined) {
120
+ return Array.isArray(direct) ? direct[0] : String(direct);
121
+ }
122
+ // Fallback: iterate to find case-insensitive match
123
+ const lowerName = name.toLowerCase();
124
+ if (typeof headers[Symbol.iterator] === "function") {
125
+ for (const [key, value] of headers) {
126
+ if (key.toLowerCase() === lowerName && value !== undefined) {
127
+ return Array.isArray(value) ? value[0] : String(value);
128
+ }
129
+ }
130
+ }
131
+ return "";
132
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * harper-knowledge — Harper Knowledge Base Plugin
3
+ *
4
+ * Sub-component plugin that provides a knowledge base with vector search,
5
+ * triage queue, and MCP server integration. Loaded by a parent application
6
+ * via `package:` in the parent's config.yaml.
7
+ *
8
+ * Harper calls handleApplication(scope) on each worker thread.
9
+ */
10
+ import type { Scope } from "./types.ts";
11
+ export { createEntry, getEntry, updateEntry, deprecateEntry, linkSupersedes, linkSiblings, linkRelated, } from "./core/entries.ts";
12
+ export { search, filterByApplicability } from "./core/search.ts";
13
+ export { logEdit, getHistory } from "./core/history.ts";
14
+ export { listTags, syncTags } from "./core/tags.ts";
15
+ export { submitTriage, processTriage, listPending, dismissTriage, findBySourceId, } from "./core/triage.ts";
16
+ export { generateEmbedding, initEmbeddingModel, dispose as disposeEmbeddings, } from "./core/embeddings.ts";
17
+ export type { KnowledgeEntry, KnowledgeEntryInput, KnowledgeEntryUpdate, KnowledgeEntryEdit, TriageItem, KnowledgeTag, QueryLog, ServiceKey, SearchParams, SearchResult, ApplicabilityScope, ApplicabilityContext, TriageAction, TriageProcessOptions, KnowledgePluginConfig, } from "./types.ts";
18
+ /**
19
+ * Plugin entry point — called by Harper on each worker thread.
20
+ */
21
+ export declare function handleApplication(scope: Scope): Promise<void>;
package/dist/index.js ADDED
@@ -0,0 +1,76 @@
1
+ /**
2
+ * harper-knowledge — Harper Knowledge Base Plugin
3
+ *
4
+ * Sub-component plugin that provides a knowledge base with vector search,
5
+ * triage queue, and MCP server integration. Loaded by a parent application
6
+ * via `package:` in the parent's config.yaml.
7
+ *
8
+ * Harper calls handleApplication(scope) on each worker thread.
9
+ */
10
+ import { initEmbeddingModel, dispose as disposeEmbeddings, } from "./core/embeddings.js";
11
+ import { KnowledgeEntryResource } from "./resources/KnowledgeEntryResource.js";
12
+ import { TriageResource } from "./resources/TriageResource.js";
13
+ import { TagResource } from "./resources/TagResource.js";
14
+ import { QueryLogResource } from "./resources/QueryLogResource.js";
15
+ import { ServiceKeyResource } from "./resources/ServiceKeyResource.js";
16
+ import { HistoryResource } from "./resources/HistoryResource.js";
17
+ import { createMcpMiddleware } from "./mcp/server.js";
18
+ import { createWebhookMiddleware } from "./webhooks/middleware.js";
19
+ import { createOAuthMiddleware } from "./oauth/middleware.js";
20
+ import { ensureSigningKey } from "./oauth/keys.js";
21
+ // Re-export core modules for external use
22
+ export { createEntry, getEntry, updateEntry, deprecateEntry, linkSupersedes, linkSiblings, linkRelated, } from "./core/entries.js";
23
+ export { search, filterByApplicability } from "./core/search.js";
24
+ export { logEdit, getHistory } from "./core/history.js";
25
+ export { listTags, syncTags } from "./core/tags.js";
26
+ export { submitTriage, processTriage, listPending, dismissTriage, findBySourceId, } from "./core/triage.js";
27
+ export { generateEmbedding, initEmbeddingModel, dispose as disposeEmbeddings, } from "./core/embeddings.js";
28
+ /**
29
+ * Plugin entry point — called by Harper on each worker thread.
30
+ */
31
+ export async function handleApplication(scope) {
32
+ const scopeLogger = scope.logger;
33
+ scopeLogger?.info?.("Knowledge base plugin initializing...");
34
+ // Read plugin configuration
35
+ const rawOptions = (scope.options.getAll() || {});
36
+ const embeddingModel = rawOptions.embeddingModel || "nomic-embed-text";
37
+ // Initialize the embedding model in the background (downloads on first run).
38
+ // Don't await — the download can take minutes and would exceed Harper's
39
+ // 30-second handleApplication timeout. Semantic search degrades gracefully
40
+ // to keyword-only mode until the model is ready.
41
+ initEmbeddingModel({ embeddingModel }).then(() => scopeLogger?.info?.(`Embedding model "${embeddingModel}" loaded`), (error) => scopeLogger?.error?.("Failed to initialize embedding model — semantic search will be unavailable:", error.message));
42
+ // Initialize OAuth signing keys (generates RSA key pair on first run)
43
+ try {
44
+ await ensureSigningKey();
45
+ }
46
+ catch (error) {
47
+ scopeLogger?.error?.("Failed to initialize OAuth signing key:", error.message);
48
+ }
49
+ // Register REST Resource classes
50
+ scope.resources.set("Knowledge", KnowledgeEntryResource);
51
+ scope.resources.set("Triage", TriageResource);
52
+ scope.resources.set("KnowledgeTag", TagResource);
53
+ scope.resources.set("QueryLog", QueryLogResource);
54
+ scope.resources.set("ServiceKey", ServiceKeyResource);
55
+ scope.resources.set("History", HistoryResource);
56
+ // Register OAuth endpoints (must be first — handles /.well-known/*, /oauth/*)
57
+ scope.server.http?.(createOAuthMiddleware());
58
+ // Register webhook intake middleware (before MCP so /webhooks/* is handled first)
59
+ scope.server.http?.(createWebhookMiddleware(scope));
60
+ // Register MCP endpoint middleware (runFirst so it executes before Harper's auth layer)
61
+ scope.server.http?.(createMcpMiddleware(), { runFirst: true });
62
+ scopeLogger?.info?.("Knowledge base resources, OAuth, and MCP endpoint registered");
63
+ // Watch for configuration changes
64
+ scope.options.on("change", (_key, _value, _config) => {
65
+ scopeLogger?.debug?.("Knowledge base configuration changed");
66
+ // Future: re-configure embedding model or other settings as needed
67
+ });
68
+ // Clean up on scope close
69
+ scope.on("close", () => {
70
+ scopeLogger?.info?.("Knowledge base plugin shutting down");
71
+ disposeEmbeddings().catch((error) => {
72
+ scopeLogger?.error?.("Error disposing embedding model:", error.message);
73
+ });
74
+ });
75
+ scopeLogger?.info?.("Knowledge base plugin initialized");
76
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * MCP Server Middleware
3
+ *
4
+ * Creates an HTTP middleware for Harper's scope.server.http() that handles
5
+ * MCP (Model Context Protocol) requests using Streamable HTTP transport.
6
+ *
7
+ * The middleware intercepts requests to /mcp and dispatches them through
8
+ * the MCP SDK's WebStandardStreamableHTTPServerTransport. Requests to
9
+ * other paths are passed through to the next handler.
10
+ *
11
+ * Auth: Validates JWT Bearer tokens issued by the co-located OAuth 2.1
12
+ * authorization server. Unauthenticated requests get 401 with
13
+ * WWW-Authenticate pointing to the Protected Resource Metadata endpoint.
14
+ */
15
+ import type { HarperRequest } from "../types.ts";
16
+ /**
17
+ * Create an MCP middleware function for Harper's scope.server.http().
18
+ *
19
+ * The middleware:
20
+ * 1. Checks if the request pathname is /mcp or starts with /mcp/
21
+ * 2. If not MCP, passes through to next()
22
+ * 3. If MCP, validates Bearer token, then handles the request
23
+ */
24
+ export declare function createMcpMiddleware(): (request: HarperRequest, next: (req: HarperRequest) => Promise<unknown>) => Promise<unknown>;
@@ -0,0 +1,124 @@
1
+ /**
2
+ * MCP Server Middleware
3
+ *
4
+ * Creates an HTTP middleware for Harper's scope.server.http() that handles
5
+ * MCP (Model Context Protocol) requests using Streamable HTTP transport.
6
+ *
7
+ * The middleware intercepts requests to /mcp and dispatches them through
8
+ * the MCP SDK's WebStandardStreamableHTTPServerTransport. Requests to
9
+ * other paths are passed through to the next handler.
10
+ *
11
+ * Auth: Validates JWT Bearer tokens issued by the co-located OAuth 2.1
12
+ * authorization server. Unauthenticated requests get 401 with
13
+ * WWW-Authenticate pointing to the Protected Resource Metadata endpoint.
14
+ */
15
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
16
+ import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
17
+ import { registerTools } from "./tools.js";
18
+ import { validateMcpAuth } from "../oauth/validate.js";
19
+ import { readBody, buildHeaders, getBaseUrl } from "../http-utils.js";
20
+ /** Anonymous caller for unauthenticated requests — read-only access */
21
+ const ANONYMOUS_CALLER = {
22
+ userId: "anonymous",
23
+ clientId: "anonymous",
24
+ scopes: ["mcp:read"],
25
+ };
26
+ /**
27
+ * Create a new McpServer instance with all tools registered.
28
+ * Called per-request in stateless mode. The caller determines scope access.
29
+ */
30
+ function createServer(caller) {
31
+ const server = new McpServer({
32
+ name: "harper-knowledge",
33
+ version: "0.1.0",
34
+ }, {
35
+ capabilities: {
36
+ tools: {},
37
+ },
38
+ });
39
+ registerTools(server, caller);
40
+ return server;
41
+ }
42
+ /**
43
+ * Create an MCP middleware function for Harper's scope.server.http().
44
+ *
45
+ * The middleware:
46
+ * 1. Checks if the request pathname is /mcp or starts with /mcp/
47
+ * 2. If not MCP, passes through to next()
48
+ * 3. If MCP, validates Bearer token, then handles the request
49
+ */
50
+ export function createMcpMiddleware() {
51
+ return async (request, next) => {
52
+ const pathname = request.pathname || "";
53
+ // Only handle /mcp routes
54
+ if (pathname !== "/mcp" && !pathname.startsWith("/mcp/")) {
55
+ return next(request);
56
+ }
57
+ // Validate JWT Bearer token (if present)
58
+ const { caller, hasToken } = await validateMcpAuth(request);
59
+ // Invalid token → 401 so the client re-authenticates
60
+ if (hasToken && !caller) {
61
+ const baseUrl = getBaseUrl(request);
62
+ return new Response(JSON.stringify({
63
+ jsonrpc: "2.0",
64
+ error: {
65
+ code: -32001,
66
+ message: "Unauthorized",
67
+ },
68
+ id: null,
69
+ }), {
70
+ status: 401,
71
+ headers: {
72
+ "Content-Type": "application/json",
73
+ "WWW-Authenticate": `Bearer resource_metadata="${baseUrl}/.well-known/oauth-protected-resource"`,
74
+ },
75
+ });
76
+ }
77
+ // No token → anonymous read-only; valid token → authenticated caller
78
+ const effectiveCaller = caller ?? ANONYMOUS_CALLER;
79
+ try {
80
+ // Read the body from Harper's stream
81
+ const bodyText = await readBody(request);
82
+ const parsedBody = bodyText ? JSON.parse(bodyText) : undefined;
83
+ // Build an absolute URL (Request constructor requires it)
84
+ const host = request.host || "localhost";
85
+ const protocol = request.protocol || "http";
86
+ const absoluteUrl = `${protocol}://${host}${request.url || pathname}`;
87
+ // Build Web Standard headers
88
+ const headers = buildHeaders(request);
89
+ // Create a minimal Web Standard Request — body is not needed since
90
+ // we pass parsedBody directly to the transport
91
+ const webRequest = new Request(absoluteUrl, {
92
+ method: request.method || "POST",
93
+ headers,
94
+ body: bodyText || undefined,
95
+ });
96
+ // Stateless mode: create a fresh transport and server per request
97
+ const transport = new WebStandardStreamableHTTPServerTransport({
98
+ enableJsonResponse: true,
99
+ });
100
+ const server = createServer(effectiveCaller);
101
+ await server.connect(transport);
102
+ // Let the MCP transport handle the request
103
+ const webResponse = await transport.handleRequest(webRequest, {
104
+ parsedBody,
105
+ });
106
+ return webResponse;
107
+ }
108
+ catch (error) {
109
+ logger?.error?.("MCP request handling failed:", error.message, error.stack);
110
+ // Return a JSON-RPC error response
111
+ return new Response(JSON.stringify({
112
+ jsonrpc: "2.0",
113
+ error: {
114
+ code: -32603,
115
+ message: "Internal server error",
116
+ },
117
+ id: null,
118
+ }), {
119
+ status: 500,
120
+ headers: { "Content-Type": "application/json" },
121
+ });
122
+ }
123
+ };
124
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * MCP Tool Registration
3
+ *
4
+ * Defines and registers all 6 MCP tools with the McpServer instance.
5
+ * Each tool wraps a core function from src/core/ and returns JSON-formatted results.
6
+ */
7
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
8
+ import type { ValidatedCaller } from "../oauth/validate.ts";
9
+ /**
10
+ * Register all knowledge base MCP tools on the given server.
11
+ * The caller determines scope access — write tools require mcp:write.
12
+ */
13
+ export declare function registerTools(server: McpServer, caller: ValidatedCaller): void;