harper-knowledge 0.1.0 → 0.1.2

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.
@@ -7,13 +7,14 @@
7
7
  */
8
8
  /**
9
9
  * Initialize the embedding model.
10
- * Downloads the model to ~/hdb/models/ if not present.
10
+ * Downloads the model to the given directory if not present.
11
11
  * Uses file-based locking so only one thread downloads.
12
12
  *
13
- * @param config - Plugin configuration with embeddingModel name
13
+ * @param config - Plugin configuration with embeddingModel name and componentDir
14
14
  */
15
15
  export declare function initEmbeddingModel(config: {
16
16
  embeddingModel: string;
17
+ componentDir: string;
17
18
  }): Promise<void>;
18
19
  /**
19
20
  * Generate an embedding vector for the given text.
@@ -8,7 +8,6 @@
8
8
  import { writeFile, readFile, unlink, mkdir } from "node:fs/promises";
9
9
  import { existsSync } from "node:fs";
10
10
  import path from "node:path";
11
- import os from "node:os";
12
11
  // Module-level state for the loaded model and context
13
12
  let llama = null;
14
13
  let embeddingModel = null;
@@ -24,17 +23,13 @@ const MODEL_CONFIGS = {
24
23
  file: "nomic-embed-text-v2-moe.Q4_K_M.gguf",
25
24
  },
26
25
  };
27
- /**
28
- * Get the directory where models are stored.
29
- */
30
- function getModelsDir() {
31
- return path.join(os.homedir(), "hdb", "models");
32
- }
26
+ // Module-level models directory, set during initEmbeddingModel
27
+ let modelsDir = null;
33
28
  /**
34
29
  * Get the lock file path for download synchronization.
35
30
  */
36
31
  function getLockFilePath(modelName) {
37
- return path.join(getModelsDir(), `${modelName}.lock`);
32
+ return path.join(modelsDir, `${modelName}.lock`);
38
33
  }
39
34
  /**
40
35
  * Get the model URI for node-llama-cpp downloads.
@@ -107,16 +102,16 @@ async function waitForDownload(modelName, modelPath) {
107
102
  */
108
103
  async function downloadModelIfNeeded(modelName) {
109
104
  const modelUri = getModelUri(modelName);
110
- const modelsDir = getModelsDir();
105
+ const dir = modelsDir;
111
106
  // Ensure models directory exists
112
- await mkdir(modelsDir, { recursive: true });
107
+ await mkdir(dir, { recursive: true });
113
108
  // Use node-llama-cpp to resolve the actual file path and download if needed.
114
109
  // node-llama-cpp prefixes filenames (e.g., hf_nomic-ai_<file>.gguf),
115
110
  // so we use its entrypointFilePath rather than guessing the name.
116
111
  const { createModelDownloader } = (await import("node-llama-cpp"));
117
112
  const downloader = await createModelDownloader({
118
113
  modelUri,
119
- dirPath: modelsDir,
114
+ dirPath: dir,
120
115
  skipExisting: true,
121
116
  });
122
117
  const modelPath = downloader.entrypointFilePath;
@@ -144,13 +139,14 @@ async function downloadModelIfNeeded(modelName) {
144
139
  }
145
140
  /**
146
141
  * Initialize the embedding model.
147
- * Downloads the model to ~/hdb/models/ if not present.
142
+ * Downloads the model to the given directory if not present.
148
143
  * Uses file-based locking so only one thread downloads.
149
144
  *
150
- * @param config - Plugin configuration with embeddingModel name
145
+ * @param config - Plugin configuration with embeddingModel name and componentDir
151
146
  */
152
147
  export async function initEmbeddingModel(config) {
153
148
  const modelName = config.embeddingModel || "nomic-embed-text";
149
+ modelsDir = path.join(config.componentDir, "models");
154
150
  if (embeddingModel) {
155
151
  logger?.debug?.("Embedding model already initialized");
156
152
  return;
package/dist/index.js CHANGED
@@ -14,6 +14,7 @@ import { TagResource } from "./resources/TagResource.js";
14
14
  import { QueryLogResource } from "./resources/QueryLogResource.js";
15
15
  import { ServiceKeyResource } from "./resources/ServiceKeyResource.js";
16
16
  import { HistoryResource } from "./resources/HistoryResource.js";
17
+ import { MeResource } from "./resources/MeResource.js";
17
18
  import { createMcpMiddleware } from "./mcp/server.js";
18
19
  import { createWebhookMiddleware } from "./webhooks/middleware.js";
19
20
  import { createOAuthMiddleware } from "./oauth/middleware.js";
@@ -38,7 +39,7 @@ export async function handleApplication(scope) {
38
39
  // Don't await — the download can take minutes and would exceed Harper's
39
40
  // 30-second handleApplication timeout. Semantic search degrades gracefully
40
41
  // 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
+ initEmbeddingModel({ embeddingModel, componentDir: scope.directory }).then(() => scopeLogger?.info?.(`Embedding model "${embeddingModel}" loaded`), (error) => scopeLogger?.error?.("Failed to initialize embedding model — semantic search will be unavailable:", error.message));
42
43
  // Initialize OAuth signing keys (generates RSA key pair on first run)
43
44
  try {
44
45
  await ensureSigningKey();
@@ -53,6 +54,7 @@ export async function handleApplication(scope) {
53
54
  scope.resources.set("QueryLog", QueryLogResource);
54
55
  scope.resources.set("ServiceKey", ServiceKeyResource);
55
56
  scope.resources.set("History", HistoryResource);
57
+ scope.resources.set("me", MeResource);
56
58
  // Register OAuth endpoints (must be first — handles /.well-known/*, /oauth/*)
57
59
  scope.server.http?.(createOAuthMiddleware());
58
60
  // Register webhook intake middleware (before MCP so /webhooks/* is handled first)
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * OAuth Authorization Endpoint
3
3
  *
4
- * GET /oauth/authorize — MCP OAuth 2.1 authorization endpoint.
4
+ * GET /mcp-auth/authorize — MCP OAuth 2.1 authorization endpoint.
5
5
  *
6
6
  * Shows a login page with GitHub as the primary auth method and a
7
7
  * subtle link to fall back to Harper credentials. If the user has an
@@ -13,7 +13,7 @@
13
13
  */
14
14
  import type { HarperRequest } from "../types.ts";
15
15
  /**
16
- * Handle GET /oauth/authorize
16
+ * Handle GET /mcp-auth/authorize
17
17
  *
18
18
  * Three modes:
19
19
  * 1. Returning from GitHub login (`pending` param) — complete authorization.
@@ -22,6 +22,6 @@ import type { HarperRequest } from "../types.ts";
22
22
  */
23
23
  export declare function handleAuthorizeGet(request: HarperRequest): Promise<Response>;
24
24
  /**
25
- * Handle POST /oauth/authorize — Harper credential login.
25
+ * Handle POST /mcp-auth/authorize — Harper credential login.
26
26
  */
27
27
  export declare function handleAuthorizePost(request: HarperRequest): Promise<Response>;
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * OAuth Authorization Endpoint
3
3
  *
4
- * GET /oauth/authorize — MCP OAuth 2.1 authorization endpoint.
4
+ * GET /mcp-auth/authorize — MCP OAuth 2.1 authorization endpoint.
5
5
  *
6
6
  * Shows a login page with GitHub as the primary auth method and a
7
7
  * subtle link to fall back to Harper credentials. If the user has an
@@ -15,7 +15,7 @@ import crypto from "node:crypto";
15
15
  import { readBody, parseFormBody } from "../http-utils.js";
16
16
  import { checkOrgMembership } from "./github.js";
17
17
  /**
18
- * Handle GET /oauth/authorize
18
+ * Handle GET /mcp-auth/authorize
19
19
  *
20
20
  * Three modes:
21
21
  * 1. Returning from GitHub login (`pending` param) — complete authorization.
@@ -44,7 +44,7 @@ export async function handleAuthorizeGet(request) {
44
44
  return loginPage(params);
45
45
  }
46
46
  /**
47
- * Handle POST /oauth/authorize — Harper credential login.
47
+ * Handle POST /mcp-auth/authorize — Harper credential login.
48
48
  */
49
49
  export async function handleAuthorizePost(request) {
50
50
  let form;
@@ -266,7 +266,7 @@ async function buildGitHubLoginUrl(params) {
266
266
  redirectUri: params.redirect_uri,
267
267
  type: "pending",
268
268
  });
269
- const returnPath = `/oauth/authorize?pending=${pendingId}`;
269
+ const returnPath = `/mcp-auth/authorize?pending=${pendingId}`;
270
270
  return `/oauth/github/login?redirect=${encodeURIComponent(returnPath)}`;
271
271
  }
272
272
  /**
@@ -361,7 +361,7 @@ async function loginPage(params, errorMsg) {
361
361
  <button type="button" class="cred-toggle" onclick="document.querySelector('.cred-form').classList.toggle('visible');this.style.display='none'">
362
362
  Sign in with Harper credentials
363
363
  </button>
364
- <form method="POST" action="/oauth/authorize" class="cred-form${errorMsg ? " visible" : ""}">
364
+ <form method="POST" action="/mcp-auth/authorize" class="cred-form${errorMsg ? " visible" : ""}">
365
365
  <input type="hidden" name="client_id" value="${escapeAttr(params.client_id)}">
366
366
  <input type="hidden" name="redirect_uri" value="${escapeAttr(params.redirect_uri)}">
367
367
  <input type="hidden" name="response_type" value="${escapeAttr(params.response_type)}">
@@ -32,10 +32,10 @@ export function handleAuthServerMetadata(request) {
32
32
  const baseUrl = getBaseUrl(request);
33
33
  return jsonResponse(200, {
34
34
  issuer: baseUrl,
35
- authorization_endpoint: `${baseUrl}/oauth/authorize`,
36
- token_endpoint: `${baseUrl}/oauth/token`,
37
- registration_endpoint: `${baseUrl}/oauth/register`,
38
- jwks_uri: `${baseUrl}/oauth/jwks`,
35
+ authorization_endpoint: `${baseUrl}/mcp-auth/authorize`,
36
+ token_endpoint: `${baseUrl}/mcp-auth/token`,
37
+ registration_endpoint: `${baseUrl}/mcp-auth/register`,
38
+ jwks_uri: `${baseUrl}/mcp-auth/jwks`,
39
39
  scopes_supported: SCOPES,
40
40
  response_types_supported: ["code"],
41
41
  grant_types_supported: ["authorization_code", "refresh_token"],
@@ -4,14 +4,14 @@
4
4
  * Route dispatcher for all OAuth endpoints. Registered via
5
5
  * scope.server.http() before the MCP and webhook middlewares.
6
6
  *
7
- * Routes:
7
+ * Routes (MCP OAuth 2.1 — separate from @harperfast/oauth's /oauth/* Resource):
8
8
  * GET /.well-known/oauth-protected-resource → metadata
9
9
  * GET /.well-known/oauth-authorization-server → metadata
10
- * POST /oauth/register → DCR
11
- * GET /oauth/authorize → login page
12
- * POST /oauth/authorize → credential validation + redirect
13
- * POST /oauth/token → code exchange / refresh
14
- * GET /oauth/jwks → public key set
10
+ * POST /mcp-auth/register → DCR
11
+ * GET /mcp-auth/authorize → login page
12
+ * POST /mcp-auth/authorize → credential validation + redirect
13
+ * POST /mcp-auth/token → code exchange / refresh
14
+ * GET /mcp-auth/jwks → public key set
15
15
  */
16
16
  import type { HarperRequest } from "../types.ts";
17
17
  type MiddlewareFn = (request: HarperRequest, next: (req: HarperRequest) => Promise<unknown>) => Promise<unknown>;
@@ -4,14 +4,14 @@
4
4
  * Route dispatcher for all OAuth endpoints. Registered via
5
5
  * scope.server.http() before the MCP and webhook middlewares.
6
6
  *
7
- * Routes:
7
+ * Routes (MCP OAuth 2.1 — separate from @harperfast/oauth's /oauth/* Resource):
8
8
  * GET /.well-known/oauth-protected-resource → metadata
9
9
  * GET /.well-known/oauth-authorization-server → metadata
10
- * POST /oauth/register → DCR
11
- * GET /oauth/authorize → login page
12
- * POST /oauth/authorize → credential validation + redirect
13
- * POST /oauth/token → code exchange / refresh
14
- * GET /oauth/jwks → public key set
10
+ * POST /mcp-auth/register → DCR
11
+ * GET /mcp-auth/authorize → login page
12
+ * POST /mcp-auth/authorize → credential validation + redirect
13
+ * POST /mcp-auth/token → code exchange / refresh
14
+ * GET /mcp-auth/jwks → public key set
15
15
  */
16
16
  import { handleProtectedResourceMetadata, handleAuthServerMetadata, } from "./metadata.js";
17
17
  import { handleRegister } from "./register.js";
@@ -34,11 +34,12 @@ export function createOAuthMiddleware() {
34
34
  method === "GET") {
35
35
  return handleAuthServerMetadata(request);
36
36
  }
37
- // OAuth endpoints
38
- if (pathname === "/oauth/register" && method === "POST") {
37
+ // MCP OAuth endpoints (under /mcp-auth/ to avoid conflict with
38
+ // @harperfast/oauth's OAuthResource which owns the /oauth/* path)
39
+ if (pathname === "/mcp-auth/register" && method === "POST") {
39
40
  return handleRegister(request);
40
41
  }
41
- if (pathname === "/oauth/authorize") {
42
+ if (pathname === "/mcp-auth/authorize") {
42
43
  if (method === "GET") {
43
44
  return handleAuthorizeGet(request);
44
45
  }
@@ -46,10 +47,10 @@ export function createOAuthMiddleware() {
46
47
  return handleAuthorizePost(request);
47
48
  }
48
49
  }
49
- if (pathname === "/oauth/token" && method === "POST") {
50
+ if (pathname === "/mcp-auth/token" && method === "POST") {
50
51
  return handleToken(request);
51
52
  }
52
- if (pathname === "/oauth/jwks" && method === "GET") {
53
+ if (pathname === "/mcp-auth/jwks" && method === "GET") {
53
54
  return new Response(JSON.stringify(await getJwks()), {
54
55
  status: 200,
55
56
  headers: {
@@ -58,7 +59,7 @@ export function createOAuthMiddleware() {
58
59
  },
59
60
  });
60
61
  }
61
- // Not an OAuth route — pass through
62
+ // Not a handled route — pass through
62
63
  return next(request);
63
64
  };
64
65
  }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Me Resource
3
+ *
4
+ * Public endpoint that returns the current user's session info.
5
+ * Reads from the Harper session cookie — no extra state tracked.
6
+ *
7
+ * Routes:
8
+ * GET /me — returns current authenticated user or { authenticated: false }
9
+ */
10
+ declare const MeResource_base: any;
11
+ export declare class MeResource extends MeResource_base {
12
+ static loadAsInstance: boolean;
13
+ get(): {
14
+ authenticated: boolean;
15
+ username: any;
16
+ name: any;
17
+ provider: any;
18
+ } | {
19
+ authenticated: boolean;
20
+ username: any;
21
+ provider: string;
22
+ name?: undefined;
23
+ } | {
24
+ authenticated: boolean;
25
+ username?: undefined;
26
+ name?: undefined;
27
+ provider?: undefined;
28
+ };
29
+ }
30
+ export {};
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Me Resource
3
+ *
4
+ * Public endpoint that returns the current user's session info.
5
+ * Reads from the Harper session cookie — no extra state tracked.
6
+ *
7
+ * Routes:
8
+ * GET /me — returns current authenticated user or { authenticated: false }
9
+ */
10
+ function getResourceClass() {
11
+ return globalThis.Resource;
12
+ }
13
+ export class MeResource extends getResourceClass() {
14
+ static loadAsInstance = false;
15
+ get() {
16
+ const context = this.getContext();
17
+ // Check for OAuth session (via @harperfast/oauth)
18
+ const oauthUser = context?.session?.oauthUser;
19
+ if (oauthUser) {
20
+ return {
21
+ authenticated: true,
22
+ username: oauthUser.username,
23
+ name: oauthUser.name,
24
+ provider: oauthUser.provider,
25
+ };
26
+ }
27
+ // Check for Harper user (Basic auth or session-based)
28
+ const user = context?.user;
29
+ if (user) {
30
+ const username = typeof user === "string" ? user : user.username || user.id;
31
+ return {
32
+ authenticated: true,
33
+ username,
34
+ provider: "harper",
35
+ };
36
+ }
37
+ return { authenticated: false };
38
+ }
39
+ }
package/dist/types.d.ts CHANGED
@@ -266,6 +266,7 @@ export interface Table {
266
266
  * Harper Scope passed to handleApplication for sub-component plugins.
267
267
  */
268
268
  export interface Scope {
269
+ directory: string;
269
270
  logger: Logger;
270
271
  resources: {
271
272
  set(name: string, resource: unknown): void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "harper-knowledge",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Knowledge base plugin for Harper with MCP server integration",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
package/web/js/app.js CHANGED
@@ -104,20 +104,20 @@ const auth = {
104
104
  authenticated: false,
105
105
 
106
106
  async check() {
107
- // Check for GitHub session via @harperfast/oauth
107
+ // Check session via /me endpoint (reads Harper session cookie)
108
108
  try {
109
- const res = await fetch("/oauth/github/user");
109
+ const res = await fetch("/me");
110
110
  if (res.ok) {
111
111
  const data = await res.json();
112
- if (data && (data.username || data.login)) {
113
- this.user = data.username || data.login;
112
+ if (data?.authenticated) {
113
+ this.user = data.username;
114
114
  this.authenticated = true;
115
115
  updateAuthUI();
116
116
  return true;
117
117
  }
118
118
  }
119
119
  } catch {
120
- // No GitHub session or endpoint unavailable
120
+ // Endpoint unavailable
121
121
  }
122
122
 
123
123
  // Check for stored Harper credentials (Basic auth)