@199-bio/engram 0.7.0 → 0.7.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.
package/dist/index.js CHANGED
@@ -14,7 +14,7 @@ import { EngramDatabase } from "./storage/database.js";
14
14
  import { KnowledgeGraph } from "./graph/knowledge-graph.js";
15
15
  import { createRetriever } from "./retrieval/colbert.js";
16
16
  import { HybridSearch } from "./retrieval/hybrid.js";
17
- import { EngramWebServer } from "./web/server.js";
17
+ import { EngramWebServer, getRunningServerUrl } from "./web/server.js";
18
18
  import { Consolidator } from "./consolidation/consolidator.js";
19
19
  // ============ Configuration ============
20
20
  const DB_PATH = process.env.ENGRAM_DB_PATH
@@ -44,11 +44,21 @@ async function initialize() {
44
44
  if (consolidator.isConfigured()) {
45
45
  console.error(`[Engram] Consolidation enabled (ANTHROPIC_API_KEY found)`);
46
46
  }
47
+ // Start web server automatically (unless another instance is already running)
48
+ const existingUrl = getRunningServerUrl();
49
+ if (!existingUrl) {
50
+ webServer = new EngramWebServer({ db, graph, search });
51
+ const url = await webServer.start();
52
+ console.error(`[Engram] Web interface: ${url}`);
53
+ }
54
+ else {
55
+ console.error(`[Engram] Web interface already running: ${existingUrl}`);
56
+ }
47
57
  }
48
58
  // ============ MCP Server ============
49
59
  const server = new Server({
50
60
  name: "engram",
51
- version: "0.6.0",
61
+ version: "0.7.2",
52
62
  }, {
53
63
  capabilities: {
54
64
  tools: {},
@@ -410,6 +420,23 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
410
420
  }
411
421
  case "engram_web": {
412
422
  const { port = 3847 } = args;
423
+ // Check if a server is already running (from any MCP instance)
424
+ const existingUrl = getRunningServerUrl();
425
+ if (existingUrl) {
426
+ return {
427
+ content: [
428
+ {
429
+ type: "text",
430
+ text: JSON.stringify({
431
+ success: true,
432
+ url: existingUrl,
433
+ message: `Web interface already running at ${existingUrl}`,
434
+ reused: true,
435
+ }, null, 2),
436
+ },
437
+ ],
438
+ };
439
+ }
413
440
  // Create or reuse web server
414
441
  if (!webServer) {
415
442
  webServer = new EngramWebServer({ db, graph, search, port });
@@ -1 +1 @@
1
- {"version":3,"file":"database.d.ts","sourceRoot":"","sources":["../../src/storage/database.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"database.d.ts","sourceRoot":"","sources":["../../src/storage/database.ts"],"names":[],"mappings":"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"}
@@ -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;AAiLtD,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;gBAE/C,OAAO,EAAE;QACnB,EAAE,EAAE,cAAc,CAAC;QACnB,KAAK,EAAE,cAAc,CAAC;QACtB,MAAM,EAAE,YAAY,CAAC;KACtB;IAWD,YAAY,IAAI,OAAO;IAIvB,YAAY,IAAI,IAAI;IAId,IAAI,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;YAgFlC,WAAW;CAgL1B"}
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 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/web/server.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAMH,OAAO,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AACxD,OAAO,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAC7D,OAAO,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AAkBtD,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;IAkB9B,IAAI,IAAI,IAAI;YAOE,aAAa;YA+Bb,SAAS;YA8ST,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;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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@199-bio/engram",
3
- "version": "0.7.0",
3
+ "version": "0.7.2",
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",
package/src/index.ts CHANGED
@@ -19,7 +19,7 @@ import { EngramDatabase } from "./storage/database.js";
19
19
  import { KnowledgeGraph } from "./graph/knowledge-graph.js";
20
20
  import { createRetriever } from "./retrieval/colbert.js";
21
21
  import { HybridSearch } from "./retrieval/hybrid.js";
22
- import { EngramWebServer } from "./web/server.js";
22
+ import { EngramWebServer, getRunningServerUrl } from "./web/server.js";
23
23
  import { Consolidator } from "./consolidation/consolidator.js";
24
24
 
25
25
  // ============ Configuration ============
@@ -59,6 +59,16 @@ async function initialize(): Promise<void> {
59
59
  if (consolidator.isConfigured()) {
60
60
  console.error(`[Engram] Consolidation enabled (ANTHROPIC_API_KEY found)`);
61
61
  }
62
+
63
+ // Start web server automatically (unless another instance is already running)
64
+ const existingUrl = getRunningServerUrl();
65
+ if (!existingUrl) {
66
+ webServer = new EngramWebServer({ db, graph, search });
67
+ const url = await webServer.start();
68
+ console.error(`[Engram] Web interface: ${url}`);
69
+ } else {
70
+ console.error(`[Engram] Web interface already running: ${existingUrl}`);
71
+ }
62
72
  }
63
73
 
64
74
  // ============ MCP Server ============
@@ -66,7 +76,7 @@ async function initialize(): Promise<void> {
66
76
  const server = new Server(
67
77
  {
68
78
  name: "engram",
69
- version: "0.6.0",
79
+ version: "0.7.2",
70
80
  },
71
81
  {
72
82
  capabilities: {
@@ -480,6 +490,24 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
480
490
  case "engram_web": {
481
491
  const { port = 3847 } = args as { port?: number };
482
492
 
493
+ // Check if a server is already running (from any MCP instance)
494
+ const existingUrl = getRunningServerUrl();
495
+ if (existingUrl) {
496
+ return {
497
+ content: [
498
+ {
499
+ type: "text" as const,
500
+ text: JSON.stringify({
501
+ success: true,
502
+ url: existingUrl,
503
+ message: `Web interface already running at ${existingUrl}`,
504
+ reused: true,
505
+ }, null, 2),
506
+ },
507
+ ],
508
+ };
509
+ }
510
+
483
511
  // Create or reuse web server
484
512
  if (!webServer) {
485
513
  webServer = new EngramWebServer({ db, graph, search, port });
@@ -372,11 +372,11 @@ export class EngramDatabase {
372
372
  `).run(id);
373
373
  }
374
374
 
375
- getAllMemories(limit: number = 1000, includeDisabled: boolean = false): Memory[] {
375
+ getAllMemories(limit: number = 1000, includeDisabled: boolean = false, offset: number = 0): Memory[] {
376
376
  const sql = includeDisabled
377
- ? "SELECT * FROM memories ORDER BY timestamp DESC LIMIT ?"
378
- : "SELECT * FROM memories WHERE disabled = 0 ORDER BY timestamp DESC LIMIT ?";
379
- const rows = this.stmt(sql).all(limit) as Record<string, unknown>[];
377
+ ? "SELECT * FROM memories ORDER BY timestamp DESC LIMIT ? OFFSET ?"
378
+ : "SELECT * FROM memories WHERE disabled = 0 ORDER BY timestamp DESC LIMIT ? OFFSET ?";
379
+ const rows = this.stmt(sql).all(limit, offset) as Record<string, unknown>[];
380
380
  return rows.map((row) => this.rowToMemory(row));
381
381
  }
382
382
 
@@ -135,7 +135,7 @@ const TOOLS: Anthropic.Tool[] = [
135
135
  },
136
136
  {
137
137
  name: "delete_memory",
138
- description: "Delete a memory by its ID.",
138
+ description: "Delete a memory by its ID (soft-delete, can be recovered).",
139
139
  input_schema: {
140
140
  type: "object" as const,
141
141
  properties: {
@@ -147,6 +147,87 @@ const TOOLS: Anthropic.Tool[] = [
147
147
  required: ["id"],
148
148
  },
149
149
  },
150
+ {
151
+ name: "edit_memory",
152
+ description: "Edit an existing memory's content or importance.",
153
+ input_schema: {
154
+ type: "object" as const,
155
+ properties: {
156
+ id: {
157
+ type: "string",
158
+ description: "The memory ID to edit",
159
+ },
160
+ content: {
161
+ type: "string",
162
+ description: "New content (replaces existing)",
163
+ },
164
+ importance: {
165
+ type: "number",
166
+ description: "New importance (0-1): 0.9=core identity, 0.8=major, 0.5=normal, 0.3=minor",
167
+ },
168
+ },
169
+ required: ["id"],
170
+ },
171
+ },
172
+ {
173
+ name: "create_memory",
174
+ description: "Create a new memory. Use for storing user information, preferences, or facts.",
175
+ input_schema: {
176
+ type: "object" as const,
177
+ properties: {
178
+ content: {
179
+ type: "string",
180
+ description: "The information to store",
181
+ },
182
+ importance: {
183
+ type: "number",
184
+ description: "0-1 score: 0.9=core identity, 0.8=major, 0.5=normal (default), 0.3=minor",
185
+ },
186
+ },
187
+ required: ["content"],
188
+ },
189
+ },
190
+ {
191
+ name: "create_entity",
192
+ description: "Create a new entity (person, organization, or place).",
193
+ input_schema: {
194
+ type: "object" as const,
195
+ properties: {
196
+ name: {
197
+ type: "string",
198
+ description: "The entity name",
199
+ },
200
+ type: {
201
+ type: "string",
202
+ enum: ["person", "organization", "place"],
203
+ description: "Entity type",
204
+ },
205
+ },
206
+ required: ["name", "type"],
207
+ },
208
+ },
209
+ {
210
+ name: "create_relationship",
211
+ description: "Create a relationship between two entities.",
212
+ input_schema: {
213
+ type: "object" as const,
214
+ properties: {
215
+ from: {
216
+ type: "string",
217
+ description: "Source entity name",
218
+ },
219
+ to: {
220
+ type: "string",
221
+ description: "Target entity name",
222
+ },
223
+ type: {
224
+ type: "string",
225
+ description: "Relationship type (e.g., 'works_at', 'lives_in', 'knows', 'sibling_of')",
226
+ },
227
+ },
228
+ required: ["from", "to", "type"],
229
+ },
230
+ },
150
231
  {
151
232
  name: "find_duplicates",
152
233
  description: "Find potential duplicate entities that could be merged.",
@@ -183,12 +264,22 @@ interface ChatMessage {
183
264
  content: string;
184
265
  }
185
266
 
267
+ // Stream event types for SSE
268
+ export interface StreamEvent {
269
+ type: "text" | "tool_start" | "tool_end" | "error" | "done";
270
+ content?: string;
271
+ tool?: string;
272
+ result?: unknown;
273
+ }
274
+
186
275
  export class ChatHandler {
187
276
  private client: Anthropic | null = null;
188
277
  private db: EngramDatabase;
189
278
  private graph: KnowledgeGraph;
190
279
  private search: HybridSearch;
191
280
  private conversationHistory: Anthropic.MessageParam[] = [];
281
+ private isProcessing: boolean = false;
282
+ private messageQueue: Array<{ message: string; resolve: (value: string) => void; reject: (error: Error) => void }> = [];
192
283
 
193
284
  constructor(options: {
194
285
  db: EngramDatabase;
@@ -209,15 +300,174 @@ export class ChatHandler {
209
300
  return this.client !== null;
210
301
  }
211
302
 
303
+ isBusy(): boolean {
304
+ return this.isProcessing;
305
+ }
306
+
307
+ getQueueLength(): number {
308
+ return this.messageQueue.length;
309
+ }
310
+
212
311
  clearHistory(): void {
213
312
  this.conversationHistory = [];
214
313
  }
215
314
 
315
+ // Process message queue
316
+ private async processQueue(): Promise<void> {
317
+ if (this.isProcessing || this.messageQueue.length === 0) return;
318
+
319
+ const { message, resolve, reject } = this.messageQueue.shift()!;
320
+ try {
321
+ const result = await this.processMessage(message);
322
+ resolve(result);
323
+ } catch (error) {
324
+ reject(error instanceof Error ? error : new Error(String(error)));
325
+ }
326
+
327
+ // Process next in queue
328
+ this.processQueue();
329
+ }
330
+
331
+ // Queue-aware chat method
216
332
  async chat(userMessage: string): Promise<string> {
217
333
  if (!this.client) {
218
334
  return "Chat is not configured. Set ANTHROPIC_API_KEY environment variable.";
219
335
  }
220
336
 
337
+ // If busy, queue the message
338
+ if (this.isProcessing) {
339
+ return new Promise((resolve, reject) => {
340
+ this.messageQueue.push({ message: userMessage, resolve, reject });
341
+ });
342
+ }
343
+
344
+ return this.processMessage(userMessage);
345
+ }
346
+
347
+ // Streaming chat with callbacks for real-time updates
348
+ async *chatStream(userMessage: string): AsyncGenerator<StreamEvent> {
349
+ if (!this.client) {
350
+ yield { type: "error", content: "Chat is not configured. Set ANTHROPIC_API_KEY environment variable." };
351
+ return;
352
+ }
353
+
354
+ this.isProcessing = true;
355
+
356
+ // Add user message to history
357
+ this.conversationHistory.push({
358
+ role: "user",
359
+ content: userMessage,
360
+ });
361
+
362
+ try {
363
+ // Keep conversation history manageable
364
+ if (this.conversationHistory.length > 20) {
365
+ this.conversationHistory = this.conversationHistory.slice(-20);
366
+ }
367
+
368
+ let continueLoop = true;
369
+ let fullResponse = "";
370
+
371
+ while (continueLoop) {
372
+ const stream = this.client.messages.stream({
373
+ model: "claude-haiku-4-5-20241022",
374
+ max_tokens: 1024,
375
+ system: SYSTEM_PROMPT,
376
+ tools: TOOLS,
377
+ messages: this.conversationHistory,
378
+ });
379
+
380
+ let currentToolUse: { id: string; name: string; input: string } | null = null;
381
+
382
+ for await (const event of stream) {
383
+ if (event.type === "content_block_start") {
384
+ if (event.content_block.type === "tool_use") {
385
+ currentToolUse = {
386
+ id: event.content_block.id,
387
+ name: event.content_block.name,
388
+ input: "",
389
+ };
390
+ yield { type: "tool_start", tool: event.content_block.name };
391
+ }
392
+ } else if (event.type === "content_block_delta") {
393
+ if (event.delta.type === "text_delta") {
394
+ fullResponse += event.delta.text;
395
+ yield { type: "text", content: event.delta.text };
396
+ } else if (event.delta.type === "input_json_delta" && currentToolUse) {
397
+ currentToolUse.input += event.delta.partial_json;
398
+ }
399
+ } else if (event.type === "content_block_stop") {
400
+ if (currentToolUse) {
401
+ // Execute the tool
402
+ let toolInput: Record<string, unknown> = {};
403
+ try {
404
+ toolInput = JSON.parse(currentToolUse.input || "{}");
405
+ } catch {
406
+ toolInput = {};
407
+ }
408
+
409
+ const result = await this.executeTool(currentToolUse.name, toolInput);
410
+ yield { type: "tool_end", tool: currentToolUse.name, result };
411
+ currentToolUse = null;
412
+ }
413
+ }
414
+ }
415
+
416
+ // Get final message to check stop reason
417
+ const finalMessage = await stream.finalMessage();
418
+
419
+ if (finalMessage.stop_reason === "tool_use") {
420
+ // Process tool results and continue
421
+ const toolUseBlocks = finalMessage.content.filter(
422
+ (block): block is Anthropic.ToolUseBlock => block.type === "tool_use"
423
+ );
424
+
425
+ const toolResults: Anthropic.ToolResultBlockParam[] = [];
426
+ for (const toolUse of toolUseBlocks) {
427
+ const result = await this.executeTool(toolUse.name, toolUse.input as Record<string, unknown>);
428
+ toolResults.push({
429
+ type: "tool_result",
430
+ tool_use_id: toolUse.id,
431
+ content: JSON.stringify(result),
432
+ });
433
+ }
434
+
435
+ // Add to history
436
+ this.conversationHistory.push({
437
+ role: "assistant",
438
+ content: finalMessage.content,
439
+ });
440
+ this.conversationHistory.push({
441
+ role: "user",
442
+ content: toolResults,
443
+ });
444
+ } else {
445
+ // Done - add final response to history
446
+ this.conversationHistory.push({
447
+ role: "assistant",
448
+ content: finalMessage.content,
449
+ });
450
+ continueLoop = false;
451
+ }
452
+ }
453
+
454
+ yield { type: "done" };
455
+ } catch (error) {
456
+ const message = error instanceof Error ? error.message : String(error);
457
+ yield { type: "error", content: message };
458
+ } finally {
459
+ this.isProcessing = false;
460
+ // Process any queued messages
461
+ this.processQueue();
462
+ }
463
+ }
464
+
465
+ // Original non-streaming method for backwards compatibility
466
+ private async processMessage(userMessage: string): Promise<string> {
467
+ if (!this.client) {
468
+ return "Chat is not configured. Set ANTHROPIC_API_KEY environment variable.";
469
+ }
470
+
221
471
  // Add user message to history
222
472
  this.conversationHistory.push({
223
473
  role: "user",
@@ -427,9 +677,93 @@ export class ChatHandler {
427
677
  return { error: `Memory not found: ${id}` };
428
678
  }
429
679
 
680
+ // Soft-delete: remove from index and disable
430
681
  await this.search.removeFromIndex(id);
431
- this.db.deleteMemory(id);
432
- return { success: true, deleted_id: id };
682
+ this.db.updateMemory(id, { disabled: true });
683
+ return { success: true, disabled_id: id, message: "Memory disabled (soft-deleted)" };
684
+ }
685
+
686
+ case "edit_memory": {
687
+ const id = input.id as string;
688
+ const content = input.content as string | undefined;
689
+ const importance = input.importance as number | undefined;
690
+
691
+ const memory = this.db.getMemory(id);
692
+ if (!memory) {
693
+ return { error: `Memory not found: ${id}` };
694
+ }
695
+
696
+ const updates: { content?: string; importance?: number } = {};
697
+ if (content !== undefined) updates.content = content;
698
+ if (importance !== undefined) updates.importance = importance;
699
+
700
+ if (Object.keys(updates).length === 0) {
701
+ return { error: "No updates provided" };
702
+ }
703
+
704
+ const updated = this.db.updateMemory(id, updates);
705
+
706
+ // Re-index if content changed
707
+ if (content !== undefined && updated) {
708
+ await this.search.removeFromIndex(id);
709
+ await this.search.indexMemory(updated);
710
+ }
711
+
712
+ return {
713
+ success: true,
714
+ memory_id: id,
715
+ updated_fields: Object.keys(updates),
716
+ };
717
+ }
718
+
719
+ case "create_memory": {
720
+ const content = input.content as string;
721
+ const importance = (input.importance as number) || 0.5;
722
+
723
+ const memory = this.db.createMemory(content, "chat", importance);
724
+ await this.search.indexMemory(memory);
725
+
726
+ return {
727
+ success: true,
728
+ memory_id: memory.id,
729
+ content: memory.content.substring(0, 100) + (memory.content.length > 100 ? "..." : ""),
730
+ };
731
+ }
732
+
733
+ case "create_entity": {
734
+ const name = input.name as string;
735
+ const type = input.type as "person" | "organization" | "place";
736
+
737
+ // Check if entity already exists
738
+ const existing = this.db.findEntityByName(name);
739
+ if (existing) {
740
+ return { error: `Entity already exists: ${name}`, existing_id: existing.id };
741
+ }
742
+
743
+ const entity = this.graph.getOrCreateEntity(name, type);
744
+ return {
745
+ success: true,
746
+ entity_id: entity.id,
747
+ name: entity.name,
748
+ type: entity.type,
749
+ };
750
+ }
751
+
752
+ case "create_relationship": {
753
+ const fromName = input.from as string;
754
+ const toName = input.to as string;
755
+ const type = input.type as string;
756
+
757
+ // Get or create both entities (default to person type if not existing)
758
+ const fromEntity = this.graph.getOrCreateEntity(fromName, "person");
759
+ const toEntity = this.graph.getOrCreateEntity(toName, "person");
760
+
761
+ this.graph.relate(fromEntity.name, toEntity.name, type);
762
+
763
+ return {
764
+ success: true,
765
+ relationship: `${fromName} -[${type}]-> ${toName}`,
766
+ };
433
767
  }
434
768
 
435
769
  case "find_duplicates": {
package/src/web/server.ts CHANGED
@@ -6,6 +6,7 @@
6
6
  import http from "http";
7
7
  import fs from "fs";
8
8
  import path from "path";
9
+ import os from "os";
9
10
  import { fileURLToPath } from "url";
10
11
  import { EngramDatabase } from "../storage/database.js";
11
12
  import { KnowledgeGraph } from "../graph/knowledge-graph.js";
@@ -18,6 +19,12 @@ const __dirname = path.dirname(__filename);
18
19
 
19
20
  const STATIC_DIR = path.join(__dirname, "..", "..", "src", "web", "static");
20
21
 
22
+ // Port file for discovery - allows finding the running web server
23
+ const PORT_FILE = path.join(
24
+ process.env.ENGRAM_DB_PATH?.replace("~", os.homedir()) || path.join(os.homedir(), ".engram"),
25
+ "web-server.json"
26
+ );
27
+
21
28
  const MIME_TYPES: Record<string, string> = {
22
29
  ".html": "text/html",
23
30
  ".css": "text/css",
@@ -27,6 +34,31 @@ const MIME_TYPES: Record<string, string> = {
27
34
  ".svg": "image/svg+xml",
28
35
  };
29
36
 
37
+ /**
38
+ * Get the URL of a currently running Engram web server
39
+ * Returns null if no server is running
40
+ */
41
+ export function getRunningServerUrl(): string | null {
42
+ try {
43
+ if (fs.existsSync(PORT_FILE)) {
44
+ const data = JSON.parse(fs.readFileSync(PORT_FILE, "utf-8"));
45
+ const { port, pid } = data;
46
+
47
+ // Check if process is still running
48
+ try {
49
+ process.kill(pid, 0); // Signal 0 = check if process exists
50
+ return `http://localhost:${port}`;
51
+ } catch {
52
+ // Process not running, clean up stale file
53
+ fs.unlinkSync(PORT_FILE);
54
+ }
55
+ }
56
+ } catch {
57
+ // File doesn't exist or can't be read
58
+ }
59
+ return null;
60
+ }
61
+
30
62
  interface WebServerOptions {
31
63
  db: EngramDatabase;
32
64
  graph: KnowledgeGraph;
@@ -61,21 +93,92 @@ export class EngramWebServer {
61
93
  return `http://localhost:${this.port}`;
62
94
  }
63
95
 
96
+ // Check if another server is already running
97
+ const existingUrl = getRunningServerUrl();
98
+ if (existingUrl) {
99
+ console.error(`[Engram] Web interface already running at ${existingUrl}`);
100
+ return existingUrl;
101
+ }
102
+
64
103
  this.server = http.createServer((req, res) => this.handleRequest(req, res));
65
104
 
66
- return new Promise((resolve, reject) => {
67
- this.server!.listen(this.port, () => {
68
- const url = `http://localhost:${this.port}`;
69
- console.error(`[Engram] Web interface running at ${url}`);
70
- resolve(url);
71
- });
105
+ // Try to start on preferred port, auto-increment if taken
106
+ const maxAttempts = 10;
107
+ let currentPort = this.port;
72
108
 
73
- this.server!.on("error", reject);
74
- });
109
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
110
+ try {
111
+ await new Promise<void>((resolve, reject) => {
112
+ this.server!.once("error", (err: NodeJS.ErrnoException) => {
113
+ if (err.code === "EADDRINUSE") {
114
+ currentPort++;
115
+ resolve(); // Try next port
116
+ } else {
117
+ reject(err);
118
+ }
119
+ });
120
+
121
+ this.server!.listen(currentPort, () => {
122
+ resolve();
123
+ });
124
+ });
125
+
126
+ // If we get here without error, server is listening
127
+ if (this.server!.listening) {
128
+ this.port = currentPort;
129
+ const url = `http://localhost:${this.port}`;
130
+
131
+ // Write port file for discovery
132
+ this.writePortFile();
133
+
134
+ console.error(`[Engram] Web interface running at ${url}`);
135
+ return url;
136
+ }
137
+ } catch (err) {
138
+ if (attempt === maxAttempts - 1) {
139
+ throw err;
140
+ }
141
+ }
142
+ }
143
+
144
+ throw new Error(`Could not find available port after ${maxAttempts} attempts`);
145
+ }
146
+
147
+ private writePortFile(): void {
148
+ try {
149
+ // Ensure directory exists
150
+ const dir = path.dirname(PORT_FILE);
151
+ if (!fs.existsSync(dir)) {
152
+ fs.mkdirSync(dir, { recursive: true });
153
+ }
154
+
155
+ fs.writeFileSync(PORT_FILE, JSON.stringify({
156
+ port: this.port,
157
+ pid: process.pid,
158
+ started: new Date().toISOString(),
159
+ }));
160
+ } catch (err) {
161
+ console.error("[Engram] Failed to write port file:", err);
162
+ }
163
+ }
164
+
165
+ private removePortFile(): void {
166
+ try {
167
+ if (fs.existsSync(PORT_FILE)) {
168
+ const data = JSON.parse(fs.readFileSync(PORT_FILE, "utf-8"));
169
+ // Only remove if we wrote it
170
+ if (data.pid === process.pid) {
171
+ fs.unlinkSync(PORT_FILE);
172
+ }
173
+ }
174
+ } catch {
175
+ // Ignore cleanup errors
176
+ }
75
177
  }
76
178
 
77
179
  stop(): void {
78
180
  if (this.server) {
181
+ this.removePortFile();
79
182
  this.server.close();
80
183
  this.server = null;
81
184
  }
@@ -134,6 +237,7 @@ export class EngramWebServer {
134
237
  if (pathname === "/api/memories" && method === "GET") {
135
238
  const query = url.searchParams.get("q");
136
239
  const limit = parseInt(url.searchParams.get("limit") || "50");
240
+ const offset = parseInt(url.searchParams.get("offset") || "0");
137
241
 
138
242
  if (query) {
139
243
  const results = await this.search.search(query, { limit });
@@ -145,7 +249,7 @@ export class EngramWebServer {
145
249
  })),
146
250
  }));
147
251
  } else {
148
- const memories = this.db.getAllMemories(limit);
252
+ const memories = this.db.getAllMemories(limit, false, offset);
149
253
  res.end(JSON.stringify({ memories }));
150
254
  }
151
255
  return;
@@ -307,6 +411,59 @@ export class EngramWebServer {
307
411
  return;
308
412
  }
309
413
 
414
+ // POST /api/chat/stream - streaming chat with SSE
415
+ if (pathname === "/api/chat/stream" && method === "POST") {
416
+ const { message } = body as { message: string };
417
+ if (!message) {
418
+ res.writeHead(400, { "Content-Type": "application/json" });
419
+ res.end(JSON.stringify({ error: "Message is required" }));
420
+ return;
421
+ }
422
+
423
+ // Check if chat is busy
424
+ if (this.chat.isBusy()) {
425
+ res.writeHead(429, { "Content-Type": "application/json" });
426
+ res.end(JSON.stringify({
427
+ error: "Chat is busy",
428
+ queue_length: this.chat.getQueueLength(),
429
+ }));
430
+ return;
431
+ }
432
+
433
+ // Set up SSE headers
434
+ res.writeHead(200, {
435
+ "Content-Type": "text/event-stream",
436
+ "Cache-Control": "no-cache",
437
+ "Connection": "keep-alive",
438
+ });
439
+
440
+ // Stream events to client
441
+ try {
442
+ for await (const event of this.chat.chatStream(message)) {
443
+ const data = JSON.stringify(event);
444
+ res.write(`data: ${data}\n\n`);
445
+ }
446
+ } catch (error) {
447
+ const errorEvent = {
448
+ type: "error",
449
+ content: error instanceof Error ? error.message : String(error),
450
+ };
451
+ res.write(`data: ${JSON.stringify(errorEvent)}\n\n`);
452
+ }
453
+
454
+ res.end();
455
+ return;
456
+ }
457
+
458
+ // GET /api/chat/queue - check queue status
459
+ if (pathname === "/api/chat/queue" && method === "GET") {
460
+ res.end(JSON.stringify({
461
+ busy: this.chat.isBusy(),
462
+ queue_length: this.chat.getQueueLength(),
463
+ }));
464
+ return;
465
+ }
466
+
310
467
  // ============ Consolidation Endpoints ============
311
468
 
312
469
  // GET /api/consolidation/status - get consolidation status
@@ -8,6 +8,8 @@ const API_BASE = '';
8
8
  // State
9
9
  let currentView = 'memories';
10
10
  let editingMemoryId = null;
11
+ let memoriesOffset = 0;
12
+ const MEMORIES_PAGE_SIZE = 25;
11
13
 
12
14
  // DOM Elements
13
15
  const views = {
@@ -72,17 +74,28 @@ async function loadStats() {
72
74
  statsEl.textContent = text;
73
75
  }
74
76
 
75
- // Load memories
76
- async function loadMemories(query = '') {
77
- const path = query ? `/api/memories?q=${encodeURIComponent(query)}` : '/api/memories';
77
+ // Load memories with pagination
78
+ async function loadMemories(query = '', append = false) {
79
+ if (!append) {
80
+ memoriesOffset = 0;
81
+ }
82
+
83
+ const limit = MEMORIES_PAGE_SIZE;
84
+ let path;
85
+ if (query) {
86
+ path = `/api/memories?q=${encodeURIComponent(query)}&limit=${limit}`;
87
+ } else {
88
+ path = `/api/memories?limit=${limit}&offset=${memoriesOffset}`;
89
+ }
90
+
78
91
  const data = await api(path);
79
92
 
80
- if (data.memories.length === 0) {
93
+ if (data.memories.length === 0 && !append) {
81
94
  memoriesList.innerHTML = '<div class="empty-state">No memories found</div>';
82
95
  return;
83
96
  }
84
97
 
85
- memoriesList.innerHTML = data.memories.map(m => `
98
+ const memoriesHtml = data.memories.map(m => `
86
99
  <div class="list-item memory-item" data-id="${m.id}">
87
100
  <div class="content">${escapeHtml(m.content)}</div>
88
101
  <div class="meta">
@@ -98,7 +111,29 @@ async function loadMemories(query = '') {
98
111
  </div>
99
112
  `).join('');
100
113
 
101
- // Attach event listeners
114
+ if (append) {
115
+ // Remove old "Load More" button if exists
116
+ const oldLoadMore = memoriesList.querySelector('.load-more-container');
117
+ if (oldLoadMore) oldLoadMore.remove();
118
+ memoriesList.insertAdjacentHTML('beforeend', memoriesHtml);
119
+ } else {
120
+ memoriesList.innerHTML = memoriesHtml;
121
+ }
122
+
123
+ // Add "Load More" button if we got a full page and not searching
124
+ if (!query && data.memories.length === MEMORIES_PAGE_SIZE) {
125
+ memoriesList.insertAdjacentHTML('beforeend', `
126
+ <div class="load-more-container">
127
+ <button class="load-more-btn">Load More</button>
128
+ </div>
129
+ `);
130
+ memoriesList.querySelector('.load-more-btn').addEventListener('click', () => {
131
+ memoriesOffset += MEMORIES_PAGE_SIZE;
132
+ loadMemories('', true);
133
+ });
134
+ }
135
+
136
+ // Attach event listeners to new items
102
137
  memoriesList.querySelectorAll('.edit-btn').forEach(btn => {
103
138
  btn.addEventListener('click', (e) => {
104
139
  e.stopPropagation();
@@ -402,7 +437,7 @@ async function loadContradictions() {
402
437
  }
403
438
  }
404
439
 
405
- // Load digests
440
+ // Load digests with hierarchy visualization
406
441
  async function loadDigests() {
407
442
  try {
408
443
  const data = await api('/api/digests');
@@ -412,17 +447,59 @@ async function loadDigests() {
412
447
  return;
413
448
  }
414
449
 
415
- digestsList.innerHTML = data.digests.map(d => `
416
- <div class="list-item digest-item">
417
- <div class="content">${escapeHtml(d.content)}</div>
418
- <div class="meta">
419
- ${d.topic ? `<span class="topic">${escapeHtml(d.topic)}</span>` : ''}
420
- <span>Level ${d.level}</span>
421
- <span>${d.source_count} memories</span>
422
- <span>${formatDate(d.created_at)}</span>
423
- </div>
424
- </div>
425
- `).join('');
450
+ // Group digests by level
451
+ const byLevel = { 1: [], 2: [], 3: [] };
452
+ data.digests.forEach(d => {
453
+ const level = d.level || 1;
454
+ if (!byLevel[level]) byLevel[level] = [];
455
+ byLevel[level].push(d);
456
+ });
457
+
458
+ const levelLabels = {
459
+ 1: 'Session Summaries',
460
+ 2: 'Topic Digests',
461
+ 3: 'Entity Profiles'
462
+ };
463
+
464
+ const levelDescs = {
465
+ 1: 'Summaries of individual conversations',
466
+ 2: 'Consolidated topic-based knowledge',
467
+ 3: 'High-level entity profiles and patterns'
468
+ };
469
+
470
+ let html = '';
471
+ for (const level of [3, 2, 1]) { // Show highest level first
472
+ const digests = byLevel[level];
473
+ if (digests.length === 0) continue;
474
+
475
+ html += `
476
+ <div class="digest-level">
477
+ <h3 class="level-header">
478
+ <span class="level-badge">L${level}</span>
479
+ ${levelLabels[level]}
480
+ <span class="level-count">(${digests.length})</span>
481
+ </h3>
482
+ <p class="level-desc">${levelDescs[level]}</p>
483
+ <div class="digest-list">
484
+ `;
485
+
486
+ digests.forEach(d => {
487
+ html += `
488
+ <div class="list-item digest-item" data-level="${level}">
489
+ <div class="content">${escapeHtml(d.content)}</div>
490
+ <div class="meta">
491
+ ${d.topic ? `<span class="topic">${escapeHtml(d.topic)}</span>` : ''}
492
+ <span>${d.source_count} sources</span>
493
+ <span>${formatDate(d.created_at)}</span>
494
+ </div>
495
+ </div>
496
+ `;
497
+ });
498
+
499
+ html += '</div></div>';
500
+ }
501
+
502
+ digestsList.innerHTML = html;
426
503
  } catch (e) {
427
504
  console.error('Failed to load digests', e);
428
505
  digestsList.innerHTML = '<div class="empty-state">Failed to load digests</div>';
@@ -637,7 +714,7 @@ function addChatMessage(content, role) {
637
714
  chatMessages.scrollTop = chatMessages.scrollHeight;
638
715
  }
639
716
 
640
- // Send chat message
717
+ // Send chat message with streaming
641
718
  async function sendChatMessage(message) {
642
719
  if (!message.trim()) return;
643
720
 
@@ -646,24 +723,80 @@ async function sendChatMessage(message) {
646
723
  chatInput.value = '';
647
724
  chatInput.disabled = true;
648
725
 
649
- // Show thinking indicator
650
- const thinkingDiv = document.createElement('div');
651
- thinkingDiv.className = 'chat-message thinking';
652
- thinkingDiv.innerHTML = '<p>Thinking...</p>';
653
- chatMessages.appendChild(thinkingDiv);
726
+ // Create assistant message div for streaming
727
+ const responseDiv = document.createElement('div');
728
+ responseDiv.className = 'chat-message assistant streaming';
729
+ responseDiv.innerHTML = '<p></p>';
730
+ chatMessages.appendChild(responseDiv);
654
731
  chatMessages.scrollTop = chatMessages.scrollHeight;
655
732
 
733
+ const contentEl = responseDiv.querySelector('p');
734
+ let currentContent = '';
735
+
656
736
  try {
657
- const data = await api('/api/chat', {
737
+ const response = await fetch('/api/chat/stream', {
658
738
  method: 'POST',
659
- body: { message },
739
+ headers: { 'Content-Type': 'application/json' },
740
+ body: JSON.stringify({ message }),
660
741
  });
661
742
 
662
- // Remove thinking indicator
663
- thinkingDiv.remove();
743
+ if (!response.ok) {
744
+ const errorData = await response.json();
745
+ throw new Error(errorData.error || 'Stream request failed');
746
+ }
664
747
 
665
- // Add assistant response
666
- addChatMessage(data.response, 'assistant');
748
+ const reader = response.body.getReader();
749
+ const decoder = new TextDecoder();
750
+
751
+ while (true) {
752
+ const { done, value } = await reader.read();
753
+ if (done) break;
754
+
755
+ const chunk = decoder.decode(value, { stream: true });
756
+ const lines = chunk.split('\n');
757
+
758
+ for (const line of lines) {
759
+ if (line.startsWith('data: ')) {
760
+ try {
761
+ const event = JSON.parse(line.slice(6));
762
+
763
+ switch (event.type) {
764
+ case 'text':
765
+ currentContent += event.content;
766
+ contentEl.innerHTML = formatChatContent(currentContent);
767
+ chatMessages.scrollTop = chatMessages.scrollHeight;
768
+ break;
769
+
770
+ case 'tool_start':
771
+ // Show tool execution indicator
772
+ const toolIndicator = document.createElement('span');
773
+ toolIndicator.className = 'tool-indicator';
774
+ toolIndicator.textContent = `Using ${event.tool}...`;
775
+ contentEl.appendChild(toolIndicator);
776
+ chatMessages.scrollTop = chatMessages.scrollHeight;
777
+ break;
778
+
779
+ case 'tool_end':
780
+ // Remove tool indicator
781
+ const indicators = contentEl.querySelectorAll('.tool-indicator');
782
+ indicators.forEach(ind => ind.remove());
783
+ break;
784
+
785
+ case 'error':
786
+ currentContent += `\n\nError: ${event.content}`;
787
+ contentEl.innerHTML = formatChatContent(currentContent);
788
+ break;
789
+
790
+ case 'done':
791
+ responseDiv.classList.remove('streaming');
792
+ break;
793
+ }
794
+ } catch (e) {
795
+ // Ignore JSON parse errors for incomplete chunks
796
+ }
797
+ }
798
+ }
799
+ }
667
800
 
668
801
  // Refresh data in case something changed
669
802
  loadStats();
@@ -672,14 +805,23 @@ async function sendChatMessage(message) {
672
805
  if (currentView === 'memories') loadMemories(searchInput.value);
673
806
 
674
807
  } catch (e) {
675
- thinkingDiv.remove();
676
- addChatMessage('Error: Failed to get response. Please try again.', 'assistant');
808
+ responseDiv.classList.remove('streaming');
809
+ contentEl.innerHTML = formatChatContent(`Error: ${e.message || 'Failed to get response'}`);
677
810
  }
678
811
 
679
812
  chatInput.disabled = false;
680
813
  chatInput.focus();
681
814
  }
682
815
 
816
+ // Format chat content (markdown-like)
817
+ function formatChatContent(content) {
818
+ return content
819
+ .replace(/\n\n/g, '</p><p>')
820
+ .replace(/\n/g, '<br>')
821
+ .replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
822
+ .replace(/`(.+?)`/g, '<code>$1</code>');
823
+ }
824
+
683
825
  // Clear chat history
684
826
  async function clearChatHistory() {
685
827
  try {
@@ -713,6 +855,33 @@ chatForm.addEventListener('submit', (e) => {
713
855
  sendChatMessage(chatInput.value);
714
856
  });
715
857
 
858
+ // ============ API Status Indicator ============
859
+
860
+ const apiStatusEl = document.getElementById('api-status');
861
+
862
+ async function checkApiStatus() {
863
+ apiStatusEl.classList.remove('connected', 'disconnected');
864
+ apiStatusEl.classList.add('checking');
865
+ apiStatusEl.title = 'Checking API status...';
866
+
867
+ try {
868
+ const data = await api('/api/chat/status');
869
+ apiStatusEl.classList.remove('checking');
870
+ if (data.configured) {
871
+ apiStatusEl.classList.add('connected');
872
+ apiStatusEl.title = 'Anthropic API connected';
873
+ } else {
874
+ apiStatusEl.classList.add('disconnected');
875
+ apiStatusEl.title = 'API key not configured - set ANTHROPIC_API_KEY';
876
+ }
877
+ } catch (e) {
878
+ apiStatusEl.classList.remove('checking');
879
+ apiStatusEl.classList.add('disconnected');
880
+ apiStatusEl.title = 'Failed to check API status';
881
+ }
882
+ }
883
+
716
884
  // Initialize
885
+ checkApiStatus();
717
886
  loadStats();
718
887
  loadMemories();
@@ -16,6 +16,10 @@
16
16
  <button class="nav-btn" data-view="consolidation">Consolidation</button>
17
17
  </nav>
18
18
  <div class="stats" id="stats"></div>
19
+ <div class="api-status" id="api-status" title="Anthropic API Status">
20
+ <span class="api-dot"></span>
21
+ <span class="api-label">API</span>
22
+ </div>
19
23
  <button id="chat-toggle" class="chat-toggle" title="Open Chat Assistant">Chat</button>
20
24
  </header>
21
25
 
@@ -98,6 +98,45 @@ nav {
98
98
  font-variant-numeric: tabular-nums;
99
99
  }
100
100
 
101
+ /* API Status indicator */
102
+ .api-status {
103
+ display: flex;
104
+ align-items: center;
105
+ gap: 0.375rem;
106
+ font-size: 0.75rem;
107
+ color: var(--text-muted);
108
+ padding: 0.25rem 0.625rem;
109
+ background: var(--bg-tertiary);
110
+ border-radius: 2px;
111
+ }
112
+
113
+ .api-dot {
114
+ width: 8px;
115
+ height: 8px;
116
+ border-radius: 50%;
117
+ background: var(--text-muted);
118
+ transition: background 0.2s ease;
119
+ }
120
+
121
+ .api-status.connected .api-dot {
122
+ background: var(--success);
123
+ box-shadow: 0 0 4px var(--success);
124
+ }
125
+
126
+ .api-status.disconnected .api-dot {
127
+ background: var(--danger);
128
+ }
129
+
130
+ .api-status.checking .api-dot {
131
+ background: var(--accent);
132
+ animation: pulse 1s infinite;
133
+ }
134
+
135
+ @keyframes pulse {
136
+ 0%, 100% { opacity: 1; }
137
+ 50% { opacity: 0.4; }
138
+ }
139
+
101
140
  /* Main content */
102
141
  main {
103
142
  max-width: 960px;
@@ -418,6 +457,25 @@ button:disabled {
418
457
  font-style: italic;
419
458
  }
420
459
 
460
+ /* Load more button */
461
+ .load-more-container {
462
+ display: flex;
463
+ justify-content: center;
464
+ padding: 1.5rem 0;
465
+ }
466
+
467
+ .load-more-btn {
468
+ background: var(--bg-tertiary);
469
+ color: var(--text-secondary);
470
+ padding: 0.75rem 2rem;
471
+ font-size: 0.875rem;
472
+ }
473
+
474
+ .load-more-btn:hover {
475
+ background: var(--border);
476
+ color: var(--text-primary);
477
+ }
478
+
421
479
  /* Score badge */
422
480
  .score {
423
481
  display: inline-block;
@@ -572,6 +630,42 @@ button:disabled {
572
630
  font-style: italic;
573
631
  }
574
632
 
633
+ /* Streaming message indicator */
634
+ .chat-message.streaming {
635
+ position: relative;
636
+ }
637
+
638
+ .chat-message.streaming::after {
639
+ content: '';
640
+ display: inline-block;
641
+ width: 0.5rem;
642
+ height: 1rem;
643
+ background: var(--accent);
644
+ margin-left: 0.25rem;
645
+ animation: blink 0.7s infinite;
646
+ vertical-align: text-bottom;
647
+ }
648
+
649
+ @keyframes blink {
650
+ 0%, 50% { opacity: 1; }
651
+ 51%, 100% { opacity: 0; }
652
+ }
653
+
654
+ /* Tool indicator in streaming */
655
+ .tool-indicator {
656
+ display: block;
657
+ font-size: 0.75rem;
658
+ color: var(--accent);
659
+ font-style: italic;
660
+ padding: 0.5rem 0;
661
+ border-top: 1px dashed var(--border);
662
+ margin-top: 0.5rem;
663
+ }
664
+
665
+ .tool-indicator::before {
666
+ content: '⚙ ';
667
+ }
668
+
575
669
  .chat-status {
576
670
  padding: 0.5rem 1rem;
577
671
  font-size: 0.75rem;
@@ -750,6 +844,51 @@ body.chat-open main {
750
844
  color: var(--bg-primary);
751
845
  }
752
846
 
847
+ /* Digest hierarchy */
848
+ .digest-level {
849
+ margin-bottom: 2rem;
850
+ }
851
+
852
+ .level-header {
853
+ display: flex;
854
+ align-items: center;
855
+ gap: 0.5rem;
856
+ font-size: 1rem;
857
+ font-weight: 500;
858
+ margin-bottom: 0.25rem;
859
+ }
860
+
861
+ .level-badge {
862
+ display: inline-flex;
863
+ align-items: center;
864
+ justify-content: center;
865
+ width: 1.75rem;
866
+ height: 1.75rem;
867
+ background: var(--accent);
868
+ color: white;
869
+ font-size: 0.6875rem;
870
+ font-weight: 600;
871
+ border-radius: 2px;
872
+ }
873
+
874
+ .level-count {
875
+ font-size: 0.8125rem;
876
+ font-weight: 400;
877
+ color: var(--text-muted);
878
+ }
879
+
880
+ .level-desc {
881
+ font-size: 0.8125rem;
882
+ color: var(--text-muted);
883
+ margin-bottom: 1rem;
884
+ }
885
+
886
+ .digest-list {
887
+ display: flex;
888
+ flex-direction: column;
889
+ gap: 0.75rem;
890
+ }
891
+
753
892
  /* Digest item */
754
893
  .digest-item {
755
894
  background: var(--bg-secondary);
@@ -757,6 +896,14 @@ body.chat-open main {
757
896
  padding: 1.25rem 1.5rem;
758
897
  }
759
898
 
899
+ .digest-item[data-level="3"] {
900
+ border-left: 3px solid var(--accent);
901
+ }
902
+
903
+ .digest-item[data-level="2"] {
904
+ border-left: 3px solid var(--success);
905
+ }
906
+
760
907
  .digest-item .content {
761
908
  font-size: 1rem;
762
909
  line-height: 1.7;