harper-knowledge 0.1.0 → 0.1.1

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)
@@ -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.1",
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)