@199-bio/engram 0.7.2 → 0.7.4

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.
@@ -1 +1 @@
1
- {"version":3,"file":"consolidator.d.ts","sourceRoot":"","sources":["../../src/consolidation/consolidator.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAGH,OAAO,EAAE,cAAc,EAAU,MAAM,EAAW,MAAM,wBAAwB,CAAC;AACjF,OAAO,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAC7D,OAAO,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AA0FtD,UAAU,kBAAkB;IAC1B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,2BAA2B,CAAC,EAAE,MAAM,CAAC;CACtC;AAED,qBAAa,YAAY;IACvB,OAAO,CAAC,MAAM,CAA0B;IACxC,OAAO,CAAC,EAAE,CAAiB;IAC3B,OAAO,CAAC,KAAK,CAA+B;IAC5C,OAAO,CAAC,MAAM,CAA6B;gBAGzC,EAAE,EAAE,cAAc,EAClB,KAAK,CAAC,EAAE,cAAc,EACtB,MAAM,CAAC,EAAE,YAAY;IAYvB,YAAY,IAAI,OAAO;IAIvB;;;OAGG;IACG,WAAW,CAAC,OAAO,GAAE,kBAAuB,GAAG,OAAO,CAAC;QAC3D,cAAc,EAAE,MAAM,CAAC;QACvB,mBAAmB,EAAE,MAAM,CAAC;QAC5B,iBAAiB,EAAE,MAAM,CAAC;KAC3B,CAAC;IAwEF;;OAEG;YACW,gBAAgB;IAoE9B;;OAEG;IACG,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAuHjE;;OAEG;IACH,SAAS,IAAI;QACX,UAAU,EAAE,OAAO,CAAC;QACpB,sBAAsB,EAAE,MAAM,CAAC;QAC/B,sBAAsB,EAAE,MAAM,CAAC;QAC/B,YAAY,EAAE,MAAM,CAAC;QACrB,wBAAwB,EAAE,MAAM,CAAC;KAClC;IAeD;;;OAGG;IACG,mBAAmB,CAAC,OAAO,GAAE;QACjC,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,SAAS,CAAC,EAAE,MAAM,CAAC;KACf,GAAG,OAAO,CAAC;QACf,iBAAiB,EAAE,MAAM,CAAC;QAC1B,eAAe,EAAE,MAAM,CAAC;QACxB,eAAe,EAAE,MAAM,CAAC;KACzB,CAAC;IAwFF;;OAEG;YACW,2BAA2B;IAqDzC;;;OAGG;IACG,aAAa,IAAI,OAAO,CAAC;QAC7B,iBAAiB,EAAE,MAAM,CAAC;QAC1B,eAAe,EAAE,MAAM,CAAC;QACxB,cAAc,EAAE,MAAM,CAAC;QACvB,mBAAmB,EAAE,MAAM,CAAC;KAC7B,CAAC;CAkBH"}
1
+ {"version":3,"file":"consolidator.d.ts","sourceRoot":"","sources":["../../src/consolidation/consolidator.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAGH,OAAO,EAAE,cAAc,EAAU,MAAM,EAAW,MAAM,wBAAwB,CAAC;AAEjF,OAAO,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAC7D,OAAO,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AA0FtD,UAAU,kBAAkB;IAC1B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,2BAA2B,CAAC,EAAE,MAAM,CAAC;CACtC;AAED,qBAAa,YAAY;IACvB,OAAO,CAAC,MAAM,CAA0B;IACxC,OAAO,CAAC,EAAE,CAAiB;IAC3B,OAAO,CAAC,KAAK,CAA+B;IAC5C,OAAO,CAAC,MAAM,CAA6B;gBAGzC,EAAE,EAAE,cAAc,EAClB,KAAK,CAAC,EAAE,cAAc,EACtB,MAAM,CAAC,EAAE,YAAY;IAYvB,YAAY,IAAI,OAAO;IAIvB;;;OAGG;IACG,WAAW,CAAC,OAAO,GAAE,kBAAuB,GAAG,OAAO,CAAC;QAC3D,cAAc,EAAE,MAAM,CAAC;QACvB,mBAAmB,EAAE,MAAM,CAAC;QAC5B,iBAAiB,EAAE,MAAM,CAAC;KAC3B,CAAC;IAwEF;;OAEG;YACW,gBAAgB;IAoE9B;;OAEG;IACG,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAuHjE;;OAEG;IACH,SAAS,IAAI;QACX,UAAU,EAAE,OAAO,CAAC;QACpB,sBAAsB,EAAE,MAAM,CAAC;QAC/B,sBAAsB,EAAE,MAAM,CAAC;QAC/B,YAAY,EAAE,MAAM,CAAC;QACrB,wBAAwB,EAAE,MAAM,CAAC;KAClC;IAeD;;;OAGG;IACG,mBAAmB,CAAC,OAAO,GAAE;QACjC,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,SAAS,CAAC,EAAE,MAAM,CAAC;KACf,GAAG,OAAO,CAAC;QACf,iBAAiB,EAAE,MAAM,CAAC;QAC1B,eAAe,EAAE,MAAM,CAAC;QACxB,eAAe,EAAE,MAAM,CAAC;KACzB,CAAC;IAwFF;;OAEG;YACW,2BAA2B;IAqDzC;;;OAGG;IACG,aAAa,IAAI,OAAO,CAAC;QAC7B,iBAAiB,EAAE,MAAM,CAAC;QAC1B,eAAe,EAAE,MAAM,CAAC;QACxB,cAAc,EAAE,MAAM,CAAC;QACvB,mBAAmB,EAAE,MAAM,CAAC;KAC7B,CAAC;CAkBH"}
@@ -0,0 +1 @@
1
+ {"version":3,"file":"settings.d.ts","sourceRoot":"","sources":["../src/settings.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAMH,MAAM,WAAW,cAAc;IAC7B,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAE5B;AAMD;;GAEG;AACH,wBAAgB,YAAY,IAAI,cAAc,CAU7C;AAED;;GAEG;AACH,wBAAgB,YAAY,CAAC,QAAQ,EAAE,cAAc,GAAG,IAAI,CAW3D;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,IAAI,MAAM,GAAG,SAAS,CAGvD;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAIvD;AAED;;GAEG;AACH,wBAAgB,kBAAkB,IAAI,OAAO,CAE5C"}
@@ -1 +1 @@
1
- {"version":3,"file":"chat-handler.d.ts","sourceRoot":"","sources":["../../src/web/chat-handler.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,EAAE,cAAc,EAAkB,MAAM,wBAAwB,CAAC;AACxE,OAAO,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAC7D,OAAO,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AAmQtD,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,GAAG,YAAY,GAAG,UAAU,GAAG,OAAO,GAAG,MAAM,CAAC;IAC5D,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED,qBAAa,WAAW;IACtB,OAAO,CAAC,MAAM,CAA0B;IACxC,OAAO,CAAC,EAAE,CAAiB;IAC3B,OAAO,CAAC,KAAK,CAAiB;IAC9B,OAAO,CAAC,MAAM,CAAe;IAC7B,OAAO,CAAC,mBAAmB,CAAgC;IAC3D,OAAO,CAAC,YAAY,CAAkB;IACtC,OAAO,CAAC,YAAY,CAAoG;gBAE5G,OAAO,EAAE;QACnB,EAAE,EAAE,cAAc,CAAC;QACnB,KAAK,EAAE,cAAc,CAAC;QACtB,MAAM,EAAE,YAAY,CAAC;KACtB;IAWD,YAAY,IAAI,OAAO;IAIvB,MAAM,IAAI,OAAO;IAIjB,cAAc,IAAI,MAAM;IAIxB,YAAY,IAAI,IAAI;YAKN,YAAY;IAgBpB,IAAI,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAgBzC,UAAU,CAAC,WAAW,EAAE,MAAM,GAAG,cAAc,CAAC,WAAW,CAAC;YAsHrD,cAAc;YAgFd,WAAW;CAoQ1B"}
1
+ {"version":3,"file":"chat-handler.d.ts","sourceRoot":"","sources":["../../src/web/chat-handler.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,EAAE,cAAc,EAAkB,MAAM,wBAAwB,CAAC;AACxE,OAAO,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAC7D,OAAO,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AAoQtD,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,GAAG,YAAY,GAAG,UAAU,GAAG,OAAO,GAAG,MAAM,CAAC;IAC5D,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED,qBAAa,WAAW;IACtB,OAAO,CAAC,MAAM,CAA0B;IACxC,OAAO,CAAC,EAAE,CAAiB;IAC3B,OAAO,CAAC,KAAK,CAAiB;IAC9B,OAAO,CAAC,MAAM,CAAe;IAC7B,OAAO,CAAC,mBAAmB,CAAgC;IAC3D,OAAO,CAAC,YAAY,CAAkB;IACtC,OAAO,CAAC,YAAY,CAAoG;gBAE5G,OAAO,EAAE;QACnB,EAAE,EAAE,cAAc,CAAC;QACnB,KAAK,EAAE,cAAc,CAAC;QACtB,MAAM,EAAE,YAAY,CAAC;KACtB;IASD;;OAEG;IACH,aAAa,IAAI,IAAI;IASrB,YAAY,IAAI,OAAO;IAIvB,MAAM,IAAI,OAAO;IAIjB,cAAc,IAAI,MAAM;IAIxB,YAAY,IAAI,IAAI;YAKN,YAAY;IAgBpB,IAAI,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAgBzC,UAAU,CAAC,WAAW,EAAE,MAAM,GAAG,cAAc,CAAC,WAAW,CAAC;YAsHrD,cAAc;YAgFd,WAAW;CAoQ1B"}
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/web/server.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAOH,OAAO,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AACxD,OAAO,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAC7D,OAAO,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AAwBtD;;;GAGG;AACH,wBAAgB,mBAAmB,IAAI,MAAM,GAAG,IAAI,CAmBnD;AAED,UAAU,gBAAgB;IACxB,EAAE,EAAE,cAAc,CAAC;IACnB,KAAK,EAAE,cAAc,CAAC;IACtB,MAAM,EAAE,YAAY,CAAC;IACrB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,qBAAa,eAAe;IAC1B,OAAO,CAAC,MAAM,CAA4B;IAC1C,OAAO,CAAC,EAAE,CAAiB;IAC3B,OAAO,CAAC,KAAK,CAAiB;IAC9B,OAAO,CAAC,MAAM,CAAe;IAC7B,OAAO,CAAC,IAAI,CAAc;IAC1B,OAAO,CAAC,YAAY,CAAe;IACnC,OAAO,CAAC,IAAI,CAAS;gBAET,OAAO,EAAE,gBAAgB;IAa/B,KAAK,IAAI,OAAO,CAAC,MAAM,CAAC;IAwD9B,OAAO,CAAC,aAAa;IAkBrB,OAAO,CAAC,cAAc;IActB,IAAI,IAAI,IAAI;YAQE,aAAa;YA+Bb,SAAS;YAoWT,WAAW;IAgCzB,OAAO,CAAC,SAAS;CAclB"}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/web/server.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAOH,OAAO,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AACxD,OAAO,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAC7D,OAAO,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AAyBtD;;;GAGG;AACH,wBAAgB,mBAAmB,IAAI,MAAM,GAAG,IAAI,CAmBnD;AAED,UAAU,gBAAgB;IACxB,EAAE,EAAE,cAAc,CAAC;IACnB,KAAK,EAAE,cAAc,CAAC;IACtB,MAAM,EAAE,YAAY,CAAC;IACrB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,qBAAa,eAAe;IAC1B,OAAO,CAAC,MAAM,CAA4B;IAC1C,OAAO,CAAC,EAAE,CAAiB;IAC3B,OAAO,CAAC,KAAK,CAAiB;IAC9B,OAAO,CAAC,MAAM,CAAe;IAC7B,OAAO,CAAC,IAAI,CAAc;IAC1B,OAAO,CAAC,YAAY,CAAe;IACnC,OAAO,CAAC,IAAI,CAAS;gBAET,OAAO,EAAE,gBAAgB;IAa/B,KAAK,IAAI,OAAO,CAAC,MAAM,CAAC;IAwD9B,OAAO,CAAC,aAAa;IAkBrB,OAAO,CAAC,cAAc;IActB,IAAI,IAAI,IAAI;YAQE,aAAa;YA+Bb,SAAS;YAiZT,WAAW;IAgCzB,OAAO,CAAC,SAAS;CAclB"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@199-bio/engram",
3
- "version": "0.7.2",
3
+ "version": "0.7.4",
4
4
  "description": "Give Claude a perfect memory. Local-first MCP server with hybrid search.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -13,6 +13,7 @@
13
13
 
14
14
  import Anthropic from "@anthropic-ai/sdk";
15
15
  import { EngramDatabase, Memory, Digest, Episode } from "../storage/database.js";
16
+ import { getAnthropicApiKey } from "../settings.js";
16
17
  import { KnowledgeGraph } from "../graph/knowledge-graph.js";
17
18
  import { HybridSearch } from "../retrieval/hybrid.js";
18
19
 
@@ -124,7 +125,7 @@ export class Consolidator {
124
125
  this.graph = graph || null;
125
126
  this.search = search || null;
126
127
 
127
- const apiKey = process.env.ANTHROPIC_API_KEY;
128
+ const apiKey = getAnthropicApiKey();
128
129
  if (apiKey) {
129
130
  this.client = new Anthropic({ apiKey });
130
131
  }
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Settings management for Engram
3
+ * Stores configuration in ~/.engram/settings.json
4
+ */
5
+
6
+ import fs from "fs";
7
+ import path from "path";
8
+ import os from "os";
9
+
10
+ export interface EngramSettings {
11
+ anthropic_api_key?: string;
12
+ // Future settings can be added here
13
+ }
14
+
15
+ const SETTINGS_DIR = process.env.ENGRAM_DB_PATH?.replace("~", os.homedir())
16
+ || path.join(os.homedir(), ".engram");
17
+ const SETTINGS_FILE = path.join(SETTINGS_DIR, "settings.json");
18
+
19
+ /**
20
+ * Load settings from file
21
+ */
22
+ export function loadSettings(): EngramSettings {
23
+ try {
24
+ if (fs.existsSync(SETTINGS_FILE)) {
25
+ const data = fs.readFileSync(SETTINGS_FILE, "utf-8");
26
+ return JSON.parse(data);
27
+ }
28
+ } catch (error) {
29
+ console.error("[Engram] Failed to load settings:", error);
30
+ }
31
+ return {};
32
+ }
33
+
34
+ /**
35
+ * Save settings to file
36
+ */
37
+ export function saveSettings(settings: EngramSettings): void {
38
+ try {
39
+ // Ensure directory exists
40
+ if (!fs.existsSync(SETTINGS_DIR)) {
41
+ fs.mkdirSync(SETTINGS_DIR, { recursive: true });
42
+ }
43
+ fs.writeFileSync(SETTINGS_FILE, JSON.stringify(settings, null, 2));
44
+ } catch (error) {
45
+ console.error("[Engram] Failed to save settings:", error);
46
+ throw error;
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Get the Anthropic API key from settings or environment
52
+ * Priority: settings file > environment variable
53
+ */
54
+ export function getAnthropicApiKey(): string | undefined {
55
+ const settings = loadSettings();
56
+ return settings.anthropic_api_key || process.env.ANTHROPIC_API_KEY;
57
+ }
58
+
59
+ /**
60
+ * Set the Anthropic API key in settings
61
+ */
62
+ export function setAnthropicApiKey(apiKey: string): void {
63
+ const settings = loadSettings();
64
+ settings.anthropic_api_key = apiKey;
65
+ saveSettings(settings);
66
+ }
67
+
68
+ /**
69
+ * Check if API key is configured (either in settings or env)
70
+ */
71
+ export function hasAnthropicApiKey(): boolean {
72
+ return !!getAnthropicApiKey();
73
+ }
@@ -7,6 +7,7 @@ import Anthropic from "@anthropic-ai/sdk";
7
7
  import { EngramDatabase, Entity, Memory } from "../storage/database.js";
8
8
  import { KnowledgeGraph } from "../graph/knowledge-graph.js";
9
9
  import { HybridSearch } from "../retrieval/hybrid.js";
10
+ import { getAnthropicApiKey } from "../settings.js";
10
11
 
11
12
  // Tool definitions for Claude
12
13
  const TOOLS: Anthropic.Tool[] = [
@@ -290,9 +291,19 @@ export class ChatHandler {
290
291
  this.graph = options.graph;
291
292
  this.search = options.search;
292
293
 
293
- const apiKey = process.env.ANTHROPIC_API_KEY;
294
+ // Initialize client from settings or env var
295
+ this.refreshClient();
296
+ }
297
+
298
+ /**
299
+ * Refresh the Anthropic client (call after settings change)
300
+ */
301
+ refreshClient(): void {
302
+ const apiKey = getAnthropicApiKey();
294
303
  if (apiKey) {
295
304
  this.client = new Anthropic({ apiKey });
305
+ } else {
306
+ this.client = null;
296
307
  }
297
308
  }
298
309
 
package/src/web/server.ts CHANGED
@@ -13,6 +13,7 @@ import { KnowledgeGraph } from "../graph/knowledge-graph.js";
13
13
  import { HybridSearch } from "../retrieval/hybrid.js";
14
14
  import { ChatHandler } from "./chat-handler.js";
15
15
  import { Consolidator } from "../consolidation/consolidator.js";
16
+ import { loadSettings, saveSettings, hasAnthropicApiKey } from "../settings.js";
16
17
 
17
18
  const __filename = fileURLToPath(import.meta.url);
18
19
  const __dirname = path.dirname(__filename);
@@ -379,13 +380,58 @@ export class EngramWebServer {
379
380
  return;
380
381
  }
381
382
 
383
+ // ============ Settings Endpoints ============
384
+
385
+ // GET /api/settings - get current settings (without exposing full API key)
386
+ if (pathname === "/api/settings" && method === "GET") {
387
+ const settings = loadSettings();
388
+ res.end(JSON.stringify({
389
+ has_api_key: hasAnthropicApiKey(),
390
+ api_key_preview: settings.anthropic_api_key
391
+ ? `${settings.anthropic_api_key.slice(0, 12)}...${settings.anthropic_api_key.slice(-4)}`
392
+ : null,
393
+ api_key_source: settings.anthropic_api_key
394
+ ? "settings"
395
+ : process.env.ANTHROPIC_API_KEY
396
+ ? "environment"
397
+ : null,
398
+ }));
399
+ return;
400
+ }
401
+
402
+ // POST /api/settings - update settings
403
+ if (pathname === "/api/settings" && method === "POST") {
404
+ const { anthropic_api_key } = body as { anthropic_api_key?: string };
405
+
406
+ if (anthropic_api_key !== undefined) {
407
+ const settings = loadSettings();
408
+ if (anthropic_api_key === "") {
409
+ // Clear the API key
410
+ delete settings.anthropic_api_key;
411
+ } else {
412
+ settings.anthropic_api_key = anthropic_api_key;
413
+ }
414
+ saveSettings(settings);
415
+
416
+ // Refresh the chat client
417
+ this.chat.refreshClient();
418
+ this.consolidator = new Consolidator(this.db); // Reinit consolidator
419
+ }
420
+
421
+ res.end(JSON.stringify({
422
+ success: true,
423
+ configured: this.chat.isConfigured(),
424
+ }));
425
+ return;
426
+ }
427
+
382
428
  // GET /api/chat/status - check if chat is configured
383
429
  if (pathname === "/api/chat/status" && method === "GET") {
384
430
  res.end(JSON.stringify({
385
431
  configured: this.chat.isConfigured(),
386
432
  message: this.chat.isConfigured()
387
433
  ? "Chat is ready"
388
- : "Set ANTHROPIC_API_KEY environment variable to enable chat",
434
+ : "Configure API key in Settings",
389
435
  }));
390
436
  return;
391
437
  }
@@ -17,6 +17,7 @@ const views = {
17
17
  entities: document.getElementById('entities-view'),
18
18
  graph: document.getElementById('graph-view'),
19
19
  consolidation: document.getElementById('consolidation-view'),
20
+ settings: document.getElementById('settings-view'),
20
21
  };
21
22
 
22
23
  const statsEl = document.getElementById('stats');
@@ -352,6 +353,7 @@ function switchView(view) {
352
353
  if (view === 'entities') loadEntities(entityTypeFilter.value);
353
354
  if (view === 'graph') loadGraph();
354
355
  if (view === 'consolidation') loadConsolidation();
356
+ if (view === 'settings') loadSettings();
355
357
  }
356
358
 
357
359
  // ============ Consolidation ============
@@ -881,6 +883,104 @@ async function checkApiStatus() {
881
883
  }
882
884
  }
883
885
 
886
+ // ============ Settings ============
887
+
888
+ const apiStatusBadge = document.getElementById('api-status-badge');
889
+ const apiKeyInput = document.getElementById('api-key-input');
890
+ const toggleKeyVisibility = document.getElementById('toggle-key-visibility');
891
+ const saveApiKeyBtn = document.getElementById('save-api-key');
892
+ const clearApiKeyBtn = document.getElementById('clear-api-key');
893
+
894
+ async function loadSettings() {
895
+ try {
896
+ const settings = await api('/api/settings');
897
+ updateSettingsUI(settings);
898
+ } catch (e) {
899
+ console.error('Failed to load settings', e);
900
+ }
901
+ }
902
+
903
+ function updateSettingsUI(settings) {
904
+ if (settings.has_api_key) {
905
+ apiStatusBadge.textContent = `Configured (${settings.api_key_source})`;
906
+ apiStatusBadge.className = 'status-badge configured';
907
+ apiKeyInput.placeholder = settings.api_key_preview || 'sk-ant-api03-...';
908
+ apiKeyInput.value = '';
909
+ } else {
910
+ apiStatusBadge.textContent = 'Not configured';
911
+ apiStatusBadge.className = 'status-badge not-configured';
912
+ apiKeyInput.placeholder = 'sk-ant-api03-...';
913
+ }
914
+ }
915
+
916
+ if (toggleKeyVisibility) {
917
+ toggleKeyVisibility.addEventListener('click', () => {
918
+ const type = apiKeyInput.type === 'password' ? 'text' : 'password';
919
+ apiKeyInput.type = type;
920
+ toggleKeyVisibility.textContent = type === 'password' ? '👁' : '🙈';
921
+ });
922
+ }
923
+
924
+ if (saveApiKeyBtn) {
925
+ saveApiKeyBtn.addEventListener('click', async () => {
926
+ const apiKey = apiKeyInput.value.trim();
927
+ if (!apiKey) {
928
+ alert('Please enter an API key');
929
+ return;
930
+ }
931
+
932
+ if (!apiKey.startsWith('sk-ant-')) {
933
+ alert('Invalid API key format. Should start with sk-ant-');
934
+ return;
935
+ }
936
+
937
+ try {
938
+ saveApiKeyBtn.disabled = true;
939
+ saveApiKeyBtn.textContent = 'Saving...';
940
+
941
+ const result = await api('/api/settings', {
942
+ method: 'POST',
943
+ body: { anthropic_api_key: apiKey },
944
+ });
945
+
946
+ if (result.success) {
947
+ apiKeyInput.value = '';
948
+ await loadSettings();
949
+ await checkApiStatus();
950
+ alert('API key saved successfully!');
951
+ } else {
952
+ alert('Failed to save API key');
953
+ }
954
+ } catch (e) {
955
+ console.error('Failed to save API key', e);
956
+ alert('Error saving API key');
957
+ } finally {
958
+ saveApiKeyBtn.disabled = false;
959
+ saveApiKeyBtn.textContent = 'Save API Key';
960
+ }
961
+ });
962
+ }
963
+
964
+ if (clearApiKeyBtn) {
965
+ clearApiKeyBtn.addEventListener('click', async () => {
966
+ if (!confirm('Are you sure you want to clear the API key?')) return;
967
+
968
+ try {
969
+ clearApiKeyBtn.disabled = true;
970
+ await api('/api/settings', {
971
+ method: 'POST',
972
+ body: { anthropic_api_key: '' },
973
+ });
974
+ await loadSettings();
975
+ await checkApiStatus();
976
+ } catch (e) {
977
+ console.error('Failed to clear API key', e);
978
+ } finally {
979
+ clearApiKeyBtn.disabled = false;
980
+ }
981
+ });
982
+ }
983
+
884
984
  // Initialize
885
985
  checkApiStatus();
886
986
  loadStats();
@@ -14,6 +14,7 @@
14
14
  <button class="nav-btn" data-view="entities">Entities</button>
15
15
  <button class="nav-btn" data-view="graph">Graph</button>
16
16
  <button class="nav-btn" data-view="consolidation">Consolidation</button>
17
+ <button class="nav-btn" data-view="settings">Settings</button>
17
18
  </nav>
18
19
  <div class="stats" id="stats"></div>
19
20
  <div class="api-status" id="api-status" title="Anthropic API Status">
@@ -59,6 +60,40 @@
59
60
  <div id="graph-container"></div>
60
61
  </section>
61
62
 
63
+ <!-- Settings View -->
64
+ <section id="settings-view" class="view">
65
+ <div class="settings-container">
66
+ <h2>Settings</h2>
67
+
68
+ <div class="settings-section">
69
+ <h3>API Configuration</h3>
70
+ <p class="section-desc">Configure your Anthropic API key to enable Chat and Consolidation features.</p>
71
+
72
+ <div class="settings-row" id="api-key-status">
73
+ <label>Status:</label>
74
+ <span class="status-badge" id="api-status-badge">Checking...</span>
75
+ </div>
76
+
77
+ <div class="settings-row">
78
+ <label for="api-key-input">API Key:</label>
79
+ <div class="input-group">
80
+ <input type="password" id="api-key-input" placeholder="sk-ant-api03-..." autocomplete="off">
81
+ <button type="button" id="toggle-key-visibility" title="Show/hide">👁</button>
82
+ </div>
83
+ </div>
84
+
85
+ <div class="settings-row">
86
+ <button id="save-api-key" class="primary-btn">Save API Key</button>
87
+ <button id="clear-api-key" class="danger-btn">Clear</button>
88
+ </div>
89
+
90
+ <p class="help-text">
91
+ Get your API key from <a href="https://console.anthropic.com/settings/keys" target="_blank">console.anthropic.com</a>
92
+ </p>
93
+ </div>
94
+ </div>
95
+ </section>
96
+
62
97
  <!-- Consolidation View -->
63
98
  <section id="consolidation-view" class="view">
64
99
  <div class="consolidation-header">
@@ -11,11 +11,18 @@
11
11
  --text-secondary: #5c5c5c;
12
12
  --text-muted: #8c8c8c;
13
13
  --border: #d4d0c8;
14
+ --border-hover: #b8b4ac;
14
15
  --accent: #d97706;
15
16
  --accent-hover: #b45309;
17
+ --accent-subtle: rgba(217, 119, 6, 0.1);
16
18
  --success: #059669;
19
+ --success-subtle: rgba(5, 150, 105, 0.1);
17
20
  --danger: #dc2626;
21
+ --danger-subtle: rgba(220, 38, 38, 0.1);
18
22
  --shadow: rgba(0, 0, 0, 0.06);
23
+ --shadow-lg: rgba(0, 0, 0, 0.1);
24
+ --radius: 3px;
25
+ --transition: 0.15s ease;
19
26
  }
20
27
 
21
28
  * {
@@ -54,30 +61,34 @@ h2 {
54
61
  header {
55
62
  display: flex;
56
63
  align-items: center;
57
- gap: 2rem;
58
- padding: 1.5rem 2rem;
64
+ gap: 1.5rem;
65
+ padding: 1rem 1.5rem;
59
66
  background: var(--bg-secondary);
60
67
  border-bottom: 1px solid var(--border);
61
68
  }
62
69
 
63
70
  header h1 {
64
71
  color: var(--text-primary);
72
+ font-size: 1.25rem;
73
+ font-weight: 600;
74
+ letter-spacing: -0.01em;
65
75
  }
66
76
 
67
77
  nav {
68
78
  display: flex;
69
- gap: 0.5rem;
79
+ gap: 0.25rem;
70
80
  }
71
81
 
72
82
  .nav-btn {
73
83
  font-family: inherit;
74
- font-size: 0.875rem;
75
- padding: 0.5rem 1rem;
84
+ font-size: 0.8125rem;
85
+ padding: 0.5rem 0.875rem;
76
86
  background: transparent;
77
87
  border: 1px solid transparent;
88
+ border-radius: var(--radius);
78
89
  color: var(--text-secondary);
79
90
  cursor: pointer;
80
- transition: all 0.15s ease;
91
+ transition: all var(--transition);
81
92
  }
82
93
 
83
94
  .nav-btn:hover {
@@ -87,8 +98,9 @@ nav {
87
98
 
88
99
  .nav-btn.active {
89
100
  color: var(--text-primary);
90
- border-color: var(--border);
91
101
  background: var(--bg-primary);
102
+ border-color: var(--border);
103
+ box-shadow: 0 1px 2px var(--shadow);
92
104
  }
93
105
 
94
106
  .stats {
@@ -102,12 +114,14 @@ nav {
102
114
  .api-status {
103
115
  display: flex;
104
116
  align-items: center;
105
- gap: 0.375rem;
117
+ gap: 0.5rem;
106
118
  font-size: 0.75rem;
107
119
  color: var(--text-muted);
108
- padding: 0.25rem 0.625rem;
120
+ padding: 0.375rem 0.75rem;
109
121
  background: var(--bg-tertiary);
110
- border-radius: 2px;
122
+ border-radius: var(--radius);
123
+ border: 1px solid transparent;
124
+ transition: all var(--transition);
111
125
  }
112
126
 
113
127
  .api-dot {
@@ -115,16 +129,36 @@ nav {
115
129
  height: 8px;
116
130
  border-radius: 50%;
117
131
  background: var(--text-muted);
118
- transition: background 0.2s ease;
132
+ transition: all var(--transition);
133
+ flex-shrink: 0;
134
+ }
135
+
136
+ .api-status.connected {
137
+ background: var(--success-subtle);
138
+ border-color: var(--success);
119
139
  }
120
140
 
121
141
  .api-status.connected .api-dot {
122
142
  background: var(--success);
123
- box-shadow: 0 0 4px var(--success);
143
+ box-shadow: 0 0 6px var(--success);
144
+ }
145
+
146
+ .api-status.connected .api-label {
147
+ color: var(--success);
148
+ }
149
+
150
+ .api-status.disconnected {
151
+ background: var(--danger-subtle);
152
+ border-color: var(--danger);
124
153
  }
125
154
 
126
155
  .api-status.disconnected .api-dot {
127
156
  background: var(--danger);
157
+ box-shadow: 0 0 6px var(--danger);
158
+ }
159
+
160
+ .api-status.disconnected .api-label {
161
+ color: var(--danger);
128
162
  }
129
163
 
130
164
  .api-status.checking .api-dot {
@@ -155,23 +189,26 @@ main {
155
189
  /* Search bar */
156
190
  .search-bar {
157
191
  display: flex;
158
- gap: 0.75rem;
192
+ gap: 0.5rem;
159
193
  margin-bottom: 1.5rem;
160
194
  }
161
195
 
162
196
  .search-bar input {
163
197
  flex: 1;
164
198
  font-family: inherit;
165
- font-size: 1rem;
166
- padding: 0.75rem 1rem;
199
+ font-size: 0.9375rem;
200
+ padding: 0.625rem 1rem;
167
201
  border: 1px solid var(--border);
202
+ border-radius: var(--radius);
168
203
  background: var(--bg-secondary);
169
204
  color: var(--text-primary);
205
+ transition: all var(--transition);
170
206
  }
171
207
 
172
208
  .search-bar input:focus {
173
209
  outline: none;
174
210
  border-color: var(--accent);
211
+ box-shadow: 0 0 0 2px var(--accent-subtle);
175
212
  }
176
213
 
177
214
  .search-bar input::placeholder {
@@ -181,22 +218,33 @@ main {
181
218
  /* Buttons */
182
219
  button {
183
220
  font-family: inherit;
184
- font-size: 0.875rem;
185
- padding: 0.625rem 1.25rem;
221
+ font-size: 0.8125rem;
222
+ font-weight: 500;
223
+ padding: 0.5rem 1rem;
186
224
  background: var(--text-primary);
187
225
  color: var(--bg-primary);
188
- border: none;
226
+ border: 1px solid transparent;
227
+ border-radius: var(--radius);
189
228
  cursor: pointer;
190
- transition: background 0.15s ease;
229
+ transition: all var(--transition);
191
230
  }
192
231
 
193
232
  button:hover {
194
233
  background: var(--text-secondary);
234
+ transform: translateY(-1px);
235
+ box-shadow: 0 2px 4px var(--shadow);
236
+ }
237
+
238
+ button:active {
239
+ transform: translateY(0);
240
+ box-shadow: none;
195
241
  }
196
242
 
197
243
  button:disabled {
198
244
  opacity: 0.5;
199
245
  cursor: not-allowed;
246
+ transform: none;
247
+ box-shadow: none;
200
248
  }
201
249
 
202
250
  .toolbar {
@@ -237,18 +285,20 @@ button:disabled {
237
285
  .list {
238
286
  display: flex;
239
287
  flex-direction: column;
240
- gap: 1rem;
288
+ gap: 0.75rem;
241
289
  }
242
290
 
243
291
  .list-item {
244
292
  background: var(--bg-secondary);
245
293
  border: 1px solid var(--border);
246
- padding: 1.25rem 1.5rem;
247
- transition: border-color 0.15s ease;
294
+ border-radius: var(--radius);
295
+ padding: 1rem 1.25rem;
296
+ transition: all var(--transition);
248
297
  }
249
298
 
250
299
  .list-item:hover {
251
- border-color: var(--text-muted);
300
+ border-color: var(--border-hover);
301
+ box-shadow: 0 2px 8px var(--shadow);
252
302
  }
253
303
 
254
304
  .memory-item .content {
@@ -488,10 +538,12 @@ button:disabled {
488
538
 
489
539
  /* Chat toggle button */
490
540
  .chat-toggle {
491
- margin-left: 1rem;
541
+ margin-left: auto;
492
542
  background: var(--accent);
493
- font-size: 0.8125rem;
543
+ font-size: 0.75rem;
544
+ font-weight: 600;
494
545
  padding: 0.5rem 1rem;
546
+ letter-spacing: 0.02em;
495
547
  }
496
548
 
497
549
  .chat-toggle:hover {
@@ -502,21 +554,27 @@ button:disabled {
502
554
  background: var(--text-primary);
503
555
  }
504
556
 
557
+ .chat-toggle.disabled {
558
+ background: var(--text-muted);
559
+ cursor: not-allowed;
560
+ opacity: 0.6;
561
+ }
562
+
505
563
  /* Chat panel */
506
564
  .chat-panel {
507
565
  position: fixed;
508
566
  top: 0;
509
567
  right: 0;
510
- width: 400px;
568
+ width: 380px;
511
569
  height: 100vh;
512
570
  background: var(--bg-primary);
513
571
  border-left: 1px solid var(--border);
514
572
  display: flex;
515
573
  flex-direction: column;
516
- box-shadow: -4px 0 20px var(--shadow);
574
+ box-shadow: -8px 0 32px var(--shadow-lg);
517
575
  z-index: 100;
518
576
  transform: translateX(0);
519
- transition: transform 0.2s ease;
577
+ transition: transform 0.25s cubic-bezier(0.4, 0, 0.2, 1);
520
578
  }
521
579
 
522
580
  .chat-panel.hidden {
@@ -529,37 +587,50 @@ button:disabled {
529
587
  display: flex;
530
588
  align-items: center;
531
589
  justify-content: space-between;
532
- padding: 1rem 1.25rem;
590
+ padding: 0.875rem 1rem;
533
591
  background: var(--bg-secondary);
534
592
  border-bottom: 1px solid var(--border);
535
593
  }
536
594
 
537
595
  .chat-header h3 {
538
- font-size: 1rem;
539
- font-weight: 500;
596
+ font-size: 0.9375rem;
597
+ font-weight: 600;
598
+ letter-spacing: -0.01em;
540
599
  }
541
600
 
542
601
  .chat-actions {
543
602
  display: flex;
544
- gap: 0.5rem;
603
+ align-items: center;
604
+ gap: 0.375rem;
545
605
  }
546
606
 
547
607
  .chat-actions button {
548
- font-size: 0.75rem;
608
+ font-size: 0.6875rem;
609
+ font-weight: 500;
549
610
  padding: 0.375rem 0.625rem;
550
611
  background: var(--bg-tertiary);
551
612
  color: var(--text-secondary);
613
+ border-radius: var(--radius);
552
614
  }
553
615
 
554
616
  .chat-actions button:hover {
555
617
  background: var(--border);
556
618
  color: var(--text-primary);
619
+ transform: none;
620
+ box-shadow: none;
557
621
  }
558
622
 
559
623
  #chat-close {
560
- font-size: 1.25rem;
624
+ font-size: 1.125rem;
561
625
  padding: 0.25rem 0.5rem;
562
626
  line-height: 1;
627
+ background: transparent;
628
+ color: var(--text-muted);
629
+ }
630
+
631
+ #chat-close:hover {
632
+ background: var(--danger-subtle);
633
+ color: var(--danger);
563
634
  }
564
635
 
565
636
  .chat-messages {
@@ -568,21 +639,22 @@ button:disabled {
568
639
  padding: 1rem;
569
640
  display: flex;
570
641
  flex-direction: column;
571
- gap: 1rem;
642
+ gap: 0.75rem;
572
643
  }
573
644
 
574
645
  .chat-message {
575
- padding: 0.875rem 1rem;
576
- font-size: 0.9375rem;
646
+ padding: 0.75rem 1rem;
647
+ font-size: 0.875rem;
577
648
  line-height: 1.6;
578
- max-width: 90%;
649
+ max-width: 88%;
650
+ border-radius: var(--radius);
579
651
  }
580
652
 
581
653
  .chat-message.user {
582
654
  background: var(--accent);
583
655
  color: white;
584
656
  align-self: flex-end;
585
- border-radius: 2px;
657
+ box-shadow: 0 2px 4px rgba(217, 119, 6, 0.2);
586
658
  }
587
659
 
588
660
  .chat-message.assistant {
@@ -685,7 +757,7 @@ button:disabled {
685
757
  .chat-input-form {
686
758
  display: flex;
687
759
  gap: 0.5rem;
688
- padding: 1rem;
760
+ padding: 0.875rem 1rem;
689
761
  background: var(--bg-secondary);
690
762
  border-top: 1px solid var(--border);
691
763
  }
@@ -693,16 +765,19 @@ button:disabled {
693
765
  .chat-input-form input {
694
766
  flex: 1;
695
767
  font-family: inherit;
696
- font-size: 0.9375rem;
697
- padding: 0.625rem 0.875rem;
768
+ font-size: 0.875rem;
769
+ padding: 0.5rem 0.75rem;
698
770
  border: 1px solid var(--border);
771
+ border-radius: var(--radius);
699
772
  background: var(--bg-primary);
700
773
  color: var(--text-primary);
774
+ transition: border-color var(--transition);
701
775
  }
702
776
 
703
777
  .chat-input-form input:focus {
704
778
  outline: none;
705
779
  border-color: var(--accent);
780
+ box-shadow: 0 0 0 2px var(--accent-subtle);
706
781
  }
707
782
 
708
783
  .chat-input-form input:disabled {
@@ -717,6 +792,7 @@ button:disabled {
717
792
 
718
793
  .chat-input-form button {
719
794
  background: var(--accent);
795
+ padding: 0.5rem 0.875rem;
720
796
  }
721
797
 
722
798
  .chat-input-form button:hover {
@@ -725,11 +801,13 @@ button:disabled {
725
801
 
726
802
  .chat-input-form button:disabled {
727
803
  background: var(--text-muted);
804
+ transform: none;
805
+ box-shadow: none;
728
806
  }
729
807
 
730
808
  /* Adjust main content when chat is open */
731
809
  body.chat-open main {
732
- margin-right: 400px;
810
+ margin-right: 380px;
733
811
  }
734
812
 
735
813
  /* Consolidation View */
@@ -978,6 +1056,128 @@ body.chat-open main {
978
1056
  color: var(--text-primary);
979
1057
  }
980
1058
 
1059
+ /* Settings View */
1060
+ .settings-container {
1061
+ max-width: 600px;
1062
+ }
1063
+
1064
+ .settings-section {
1065
+ background: var(--bg-secondary);
1066
+ border: 1px solid var(--border);
1067
+ border-radius: var(--radius);
1068
+ padding: 1.5rem;
1069
+ margin-bottom: 1.5rem;
1070
+ }
1071
+
1072
+ .settings-section h3 {
1073
+ font-size: 1rem;
1074
+ font-weight: 600;
1075
+ margin-bottom: 0.5rem;
1076
+ }
1077
+
1078
+ .settings-row {
1079
+ display: flex;
1080
+ align-items: center;
1081
+ gap: 1rem;
1082
+ margin: 1rem 0;
1083
+ }
1084
+
1085
+ .settings-row label {
1086
+ font-size: 0.875rem;
1087
+ color: var(--text-secondary);
1088
+ min-width: 80px;
1089
+ }
1090
+
1091
+ .input-group {
1092
+ display: flex;
1093
+ flex: 1;
1094
+ gap: 0.5rem;
1095
+ }
1096
+
1097
+ .input-group input {
1098
+ flex: 1;
1099
+ font-family: "SF Mono", Monaco, monospace;
1100
+ font-size: 0.875rem;
1101
+ padding: 0.5rem 0.75rem;
1102
+ border: 1px solid var(--border);
1103
+ border-radius: var(--radius);
1104
+ background: var(--bg-primary);
1105
+ color: var(--text-primary);
1106
+ }
1107
+
1108
+ .input-group input:focus {
1109
+ outline: none;
1110
+ border-color: var(--accent);
1111
+ box-shadow: 0 0 0 2px var(--accent-subtle);
1112
+ }
1113
+
1114
+ .input-group button {
1115
+ padding: 0.5rem 0.75rem;
1116
+ background: var(--bg-tertiary);
1117
+ color: var(--text-secondary);
1118
+ font-size: 0.875rem;
1119
+ }
1120
+
1121
+ .input-group button:hover {
1122
+ background: var(--border);
1123
+ transform: none;
1124
+ box-shadow: none;
1125
+ }
1126
+
1127
+ .primary-btn {
1128
+ background: var(--accent) !important;
1129
+ color: white !important;
1130
+ }
1131
+
1132
+ .primary-btn:hover {
1133
+ background: var(--accent-hover) !important;
1134
+ }
1135
+
1136
+ .danger-btn {
1137
+ background: var(--bg-tertiary) !important;
1138
+ color: var(--text-secondary) !important;
1139
+ }
1140
+
1141
+ .danger-btn:hover {
1142
+ background: var(--danger) !important;
1143
+ color: white !important;
1144
+ }
1145
+
1146
+ .status-badge {
1147
+ display: inline-block;
1148
+ font-size: 0.75rem;
1149
+ font-weight: 500;
1150
+ padding: 0.25rem 0.625rem;
1151
+ border-radius: var(--radius);
1152
+ background: var(--bg-tertiary);
1153
+ color: var(--text-muted);
1154
+ }
1155
+
1156
+ .status-badge.configured {
1157
+ background: var(--success-subtle);
1158
+ color: var(--success);
1159
+ }
1160
+
1161
+ .status-badge.not-configured {
1162
+ background: var(--danger-subtle);
1163
+ color: var(--danger);
1164
+ }
1165
+
1166
+ .help-text {
1167
+ font-size: 0.8125rem;
1168
+ color: var(--text-muted);
1169
+ margin-top: 1rem;
1170
+ }
1171
+
1172
+ .help-text a {
1173
+ color: var(--accent);
1174
+ text-decoration: none;
1175
+ }
1176
+
1177
+ .help-text a:hover {
1178
+ text-decoration: underline;
1179
+ }
1180
+
981
1181
  /* Responsive */
982
1182
  @media (max-width: 640px) {
983
1183
  header {