@agentmemory/agentmemory 0.9.16 → 0.9.18

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.mjs CHANGED
@@ -68,6 +68,12 @@ function hasRealValue(v) {
68
68
  }
69
69
  function detectProvider(env) {
70
70
  const maxTokens = parseInt(env["MAX_TOKENS"] || "4096", 10);
71
+ if (hasRealValue(env["OPENAI_API_KEY"]) && env["OPENAI_API_KEY_FOR_LLM"] !== "false") return {
72
+ provider: "openai",
73
+ model: env["OPENAI_MODEL"] || "gpt-4o-mini",
74
+ maxTokens,
75
+ baseURL: env["OPENAI_BASE_URL"]
76
+ };
71
77
  if (hasRealValue(env["MINIMAX_API_KEY"])) return {
72
78
  provider: "minimax",
73
79
  model: env["MINIMAX_MODEL"] || "MiniMax-M2.7",
@@ -93,7 +99,7 @@ function detectProvider(env) {
93
99
  maxTokens
94
100
  };
95
101
  if (!(env["AGENTMEMORY_ALLOW_AGENT_SDK"] === "true")) {
96
- process.stderr.write("[agentmemory] No LLM provider key found (ANTHROPIC_API_KEY, GEMINI_API_KEY, OPENROUTER_API_KEY, MINIMAX_API_KEY). LLM-backed compression and summarization are DISABLED — using no-op provider. This is the safe default: the agent-sdk fallback used to spawn Claude Agent SDK child sessions which inherit Claude Code's plugin hooks and cause infinite Stop-hook recursion (#149 follow-up). To opt in to the agent-sdk fallback anyway, set both AGENTMEMORY_AUTO_COMPRESS=true AND AGENTMEMORY_ALLOW_AGENT_SDK=true — but be aware it will burn your Claude Pro allocation and may still recurse if you use it from inside Claude Code itself.\n");
102
+ process.stderr.write("[agentmemory] No LLM provider key found (ANTHROPIC_API_KEY, GEMINI_API_KEY, OPENROUTER_API_KEY, MINIMAX_API_KEY, OPENAI_API_KEY). LLM-backed compression and summarization are DISABLED — using no-op provider. This is the safe default: the agent-sdk fallback used to spawn Claude Agent SDK child sessions which inherit Claude Code's plugin hooks and cause infinite Stop-hook recursion (#149 follow-up). To opt in to the agent-sdk fallback anyway, set both AGENTMEMORY_AUTO_COMPRESS=true AND AGENTMEMORY_ALLOW_AGENT_SDK=true — but be aware it will burn your Claude Pro allocation and may still recurse if you use it from inside Claude Code itself.\n");
97
103
  return {
98
104
  provider: "noop",
99
105
  model: "noop",
@@ -133,7 +139,7 @@ function getEnvVar(key) {
133
139
  }
134
140
  function detectLlmProviderKind() {
135
141
  const env = getMergedEnv();
136
- if (hasRealValue(env["ANTHROPIC_API_KEY"]) || hasRealValue(env["GEMINI_API_KEY"]) || hasRealValue(env["GOOGLE_API_KEY"]) || hasRealValue(env["OPENROUTER_API_KEY"]) || hasRealValue(env["MINIMAX_API_KEY"])) return "llm";
142
+ if (hasRealValue(env["ANTHROPIC_API_KEY"]) || hasRealValue(env["GEMINI_API_KEY"]) || hasRealValue(env["GOOGLE_API_KEY"]) || hasRealValue(env["OPENROUTER_API_KEY"]) || hasRealValue(env["MINIMAX_API_KEY"]) || hasRealValue(env["OPENAI_API_KEY"]) && env["OPENAI_API_KEY_FOR_LLM"] !== "false") return "llm";
137
143
  return "noop";
138
144
  }
139
145
  function loadEmbeddingConfig() {
@@ -215,7 +221,8 @@ const VALID_PROVIDERS = new Set([
215
221
  "gemini",
216
222
  "openrouter",
217
223
  "agent-sdk",
218
- "minimax"
224
+ "minimax",
225
+ "openai"
219
226
  ]);
220
227
  function loadFallbackConfig() {
221
228
  const env = getMergedEnv();
@@ -318,6 +325,20 @@ var AnthropicProvider = class {
318
325
  }
319
326
  };
320
327
 
328
+ //#endregion
329
+ //#region src/providers/_fetch.ts
330
+ function fetchWithTimeout(url, init, timeoutMs) {
331
+ const parsed = timeoutMs ?? Number.parseInt(getEnvVar("AGENTMEMORY_LLM_TIMEOUT_MS") ?? "60000", 10);
332
+ const ms = Number.isFinite(parsed) && parsed > 0 ? parsed : 6e4;
333
+ const ctl = new AbortController();
334
+ const signal = init.signal ? AbortSignal.any([init.signal, ctl.signal]) : ctl.signal;
335
+ const t = setTimeout(() => ctl.abort(), ms);
336
+ return fetch(url, {
337
+ ...init,
338
+ signal
339
+ }).finally(() => clearTimeout(t));
340
+ }
341
+
321
342
  //#endregion
322
343
  //#region src/providers/minimax.ts
323
344
  /**
@@ -353,8 +374,7 @@ var MinimaxProvider = class {
353
374
  return this.call(systemPrompt, userPrompt);
354
375
  }
355
376
  async call(systemPrompt, userPrompt) {
356
- const url = `${this.baseUrl}/v1/messages`;
357
- const response = await fetch(url, {
377
+ const response = await fetchWithTimeout(`${this.baseUrl}/v1/messages`, {
358
378
  method: "POST",
359
379
  headers: {
360
380
  "Content-Type": "application/json",
@@ -398,6 +418,146 @@ var NoopProvider = class {
398
418
  }
399
419
  };
400
420
 
421
+ //#endregion
422
+ //#region src/providers/openai.ts
423
+ const DEFAULT_BASE_URL$1 = "https://api.openai.com";
424
+ const DEFAULT_TIMEOUT_MS = 6e4;
425
+ const DEFAULT_AZURE_API_VERSION = "2024-08-01-preview";
426
+ /**
427
+ * OpenAI-compatible LLM provider.
428
+ *
429
+ * Uses raw fetch (no SDK) to support any OpenAI-compatible endpoint:
430
+ * - OpenAI official
431
+ * - Azure OpenAI (auto-detected from .openai.azure.com host)
432
+ * - DeepSeek
433
+ * - 硅基流动 (SiliconFlow)
434
+ * - vLLM / LM Studio / Ollama (with OpenAI compatibility layer)
435
+ * - Any other proxy implementing /v1/chat/completions
436
+ *
437
+ * Required env vars:
438
+ * OPENAI_API_KEY — API key
439
+ *
440
+ * Optional:
441
+ * OPENAI_BASE_URL — base URL without path (default: https://api.openai.com).
442
+ * Azure: https://<resource>.openai.azure.com/openai/deployments/<deployment>
443
+ * OPENAI_MODEL — model name (default: gpt-4o-mini)
444
+ * OPENAI_API_VERSION — Azure api-version query param (default: 2024-08-01-preview)
445
+ * OPENAI_TIMEOUT_MS — outbound fetch timeout in ms (OpenAI-scoped alias,
446
+ * takes precedence over AGENTMEMORY_LLM_TIMEOUT_MS
447
+ * for back-compat with the v0.9.17 shipping name).
448
+ * AGENTMEMORY_LLM_TIMEOUT_MS — outbound fetch timeout in ms shared across all
449
+ * raw-fetch LLM + embedding providers. Used when
450
+ * OPENAI_TIMEOUT_MS is not set. Default: 60000.
451
+ * MAX_TOKENS — max output tokens (default: from config or 4096)
452
+ * OPENAI_REASONING_EFFORT — "low" | "medium" | "high" | "none"
453
+ * Passthrough for reasoning models (e.g. Ollama Cloud
454
+ * thinking models). Set to "none" to ensure
455
+ * message.content is populated instead of only
456
+ * message.reasoning.
457
+ */
458
+ var OpenAIProvider = class {
459
+ name = "openai";
460
+ apiKey;
461
+ model;
462
+ maxTokens;
463
+ baseUrl;
464
+ reasoningEffort;
465
+ timeoutMs;
466
+ isAzure;
467
+ azureApiVersion;
468
+ constructor(apiKey, model, maxTokens, baseURL) {
469
+ this.apiKey = apiKey;
470
+ this.model = model;
471
+ this.maxTokens = maxTokens;
472
+ this.baseUrl = (baseURL || getEnvVar("OPENAI_BASE_URL") || DEFAULT_BASE_URL$1).replace(/\/+$/, "");
473
+ this.reasoningEffort = getEnvVar("OPENAI_REASONING_EFFORT") || void 0;
474
+ this.timeoutMs = resolveTimeout();
475
+ this.azureApiVersion = getEnvVar("OPENAI_API_VERSION") || DEFAULT_AZURE_API_VERSION;
476
+ this.isAzure = detectAzure(this.baseUrl);
477
+ }
478
+ async compress(systemPrompt, userPrompt) {
479
+ return this.call(systemPrompt, userPrompt);
480
+ }
481
+ async summarize(systemPrompt, userPrompt) {
482
+ return this.call(systemPrompt, userPrompt);
483
+ }
484
+ buildUrl() {
485
+ if (this.isAzure) {
486
+ const sep = this.baseUrl.includes("?") ? "&" : "?";
487
+ return `${this.baseUrl}/chat/completions${sep}api-version=${encodeURIComponent(this.azureApiVersion)}`;
488
+ }
489
+ return `${this.baseUrl}/v1/chat/completions`;
490
+ }
491
+ buildHeaders() {
492
+ if (this.isAzure) return {
493
+ "Content-Type": "application/json",
494
+ "api-key": this.apiKey
495
+ };
496
+ return {
497
+ "Content-Type": "application/json",
498
+ Authorization: `Bearer ${this.apiKey}`
499
+ };
500
+ }
501
+ async call(systemPrompt, userPrompt) {
502
+ const url = this.buildUrl();
503
+ const body = {
504
+ model: this.model,
505
+ max_tokens: this.maxTokens,
506
+ messages: [{
507
+ role: "system",
508
+ content: systemPrompt
509
+ }, {
510
+ role: "user",
511
+ content: userPrompt
512
+ }]
513
+ };
514
+ if (this.reasoningEffort) body.reasoning_effort = this.reasoningEffort;
515
+ let response;
516
+ try {
517
+ response = await fetchWithTimeout(url, {
518
+ method: "POST",
519
+ headers: this.buildHeaders(),
520
+ body: JSON.stringify(body)
521
+ }, this.timeoutMs);
522
+ } catch (err) {
523
+ if (err instanceof Error && err.name === "AbortError") throw new Error(`OpenAI API request timed out after ${this.timeoutMs}ms — set OPENAI_TIMEOUT_MS (or AGENTMEMORY_LLM_TIMEOUT_MS) to raise the bound or check the provider status.`);
524
+ throw err;
525
+ }
526
+ if (!response.ok) {
527
+ const text = await response.text();
528
+ throw new Error(`OpenAI API error (${response.status}): ${text}`);
529
+ }
530
+ const data = await response.json();
531
+ const message = data.choices?.[0]?.message;
532
+ const content = message?.content;
533
+ if (content) return content;
534
+ const reasoning = message?.reasoning;
535
+ if (reasoning) return reasoning;
536
+ throw new Error(`OpenAI returned unexpected response: ${JSON.stringify(data).slice(0, 200)}`);
537
+ }
538
+ };
539
+ function resolveTimeout() {
540
+ const openai = parsePositiveInt(getEnvVar("OPENAI_TIMEOUT_MS"));
541
+ if (openai !== void 0) return openai;
542
+ const globalMs = parsePositiveInt(getEnvVar("AGENTMEMORY_LLM_TIMEOUT_MS"));
543
+ if (globalMs !== void 0) return globalMs;
544
+ return DEFAULT_TIMEOUT_MS;
545
+ }
546
+ function parsePositiveInt(raw) {
547
+ if (!raw) return void 0;
548
+ const trimmed = raw.trim();
549
+ if (!/^\d+$/.test(trimmed)) return void 0;
550
+ const n = Number(trimmed);
551
+ return Number.isFinite(n) && n > 0 ? n : void 0;
552
+ }
553
+ function detectAzure(baseUrl) {
554
+ try {
555
+ return new URL(baseUrl).hostname.endsWith(".openai.azure.com");
556
+ } catch {
557
+ return false;
558
+ }
559
+ }
560
+
401
561
  //#endregion
402
562
  //#region src/providers/openrouter.ts
403
563
  var OpenRouterProvider = class {
@@ -420,7 +580,7 @@ var OpenRouterProvider = class {
420
580
  return this.call(systemPrompt, userPrompt);
421
581
  }
422
582
  async call(systemPrompt, userPrompt) {
423
- const response = await fetch(this.baseUrl, {
583
+ const response = await fetchWithTimeout(this.baseUrl, {
424
584
  method: "POST",
425
585
  headers: {
426
586
  "Content-Type": "application/json",
@@ -589,7 +749,7 @@ var GeminiEmbeddingProvider = class {
589
749
  const results = [];
590
750
  for (let i = 0; i < texts.length; i += BATCH_LIMIT) {
591
751
  const chunk = texts.slice(i, i + BATCH_LIMIT);
592
- const response = await fetch(`${API_BASE}?key=${this.apiKey}`, {
752
+ const response = await fetchWithTimeout(`${API_BASE}?key=${this.apiKey}`, {
593
753
  method: "POST",
594
754
  headers: { "Content-Type": "application/json" },
595
755
  body: JSON.stringify({ requests: chunk.map((t) => ({
@@ -678,8 +838,7 @@ var OpenAIEmbeddingProvider = class {
678
838
  return result;
679
839
  }
680
840
  async embedBatch(texts) {
681
- const url = `${this.baseUrl}/v1/embeddings`;
682
- const response = await fetch(url, {
841
+ const response = await fetchWithTimeout(`${this.baseUrl}/v1/embeddings`, {
683
842
  method: "POST",
684
843
  headers: {
685
844
  Authorization: `Bearer ${this.apiKey}`,
@@ -714,7 +873,7 @@ var VoyageEmbeddingProvider = class {
714
873
  return result;
715
874
  }
716
875
  async embedBatch(texts) {
717
- const response = await fetch(API_URL$2, {
876
+ const response = await fetchWithTimeout(API_URL$2, {
718
877
  method: "POST",
719
878
  headers: {
720
879
  Authorization: `Bearer ${this.apiKey}`,
@@ -750,7 +909,7 @@ var CohereEmbeddingProvider = class {
750
909
  return result;
751
910
  }
752
911
  async embedBatch(texts) {
753
- const response = await fetch(API_URL$1, {
912
+ const response = await fetchWithTimeout(API_URL$1, {
754
913
  method: "POST",
755
914
  headers: {
756
915
  Authorization: `Bearer ${this.apiKey}`,
@@ -788,7 +947,7 @@ var OpenRouterEmbeddingProvider = class {
788
947
  return result;
789
948
  }
790
949
  async embedBatch(texts) {
791
- const response = await fetch(API_URL, {
950
+ const response = await fetchWithTimeout(API_URL, {
792
951
  method: "POST",
793
952
  headers: {
794
953
  Authorization: `Bearer ${this.apiKey}`,
@@ -983,6 +1142,11 @@ function createBaseProvider(config) {
983
1142
  return new OpenRouterProvider(geminiKey, config.model, config.maxTokens, "https://generativelanguage.googleapis.com/v1beta/openai/chat/completions");
984
1143
  }
985
1144
  case "openrouter": return new OpenRouterProvider(requireEnvVar("OPENROUTER_API_KEY"), config.model, config.maxTokens, "https://openrouter.ai/api/v1/chat/completions");
1145
+ case "openai": {
1146
+ const openaiKey = getEnvVar("OPENAI_API_KEY");
1147
+ if (!openaiKey) throw new Error("OPENAI_API_KEY is required for the openai provider");
1148
+ return new OpenAIProvider(openaiKey, config.model, config.maxTokens, config.baseURL);
1149
+ }
986
1150
  case "noop": return new NoopProvider();
987
1151
  default: return new AgentSDKProvider();
988
1152
  }
@@ -4400,7 +4564,11 @@ function registerContextFunction(sdk, kv, tokenBudget) {
4400
4564
  sdk.registerFunction("mem::context", async (data) => {
4401
4565
  const budget = data.budget || tokenBudget;
4402
4566
  const blocks = [];
4403
- const [pinnedSlots, profile] = await Promise.all([isSlotsEnabled() ? listPinnedSlots(kv).catch(() => []) : Promise.resolve([]), kv.get(KV.profiles, data.project).catch(() => null)]);
4567
+ const [pinnedSlots, profile, lessons] = await Promise.all([
4568
+ isSlotsEnabled() ? listPinnedSlots(kv).catch(() => []) : Promise.resolve([]),
4569
+ kv.get(KV.profiles, data.project).catch(() => null),
4570
+ kv.list(KV.lessons).catch(() => [])
4571
+ ]);
4404
4572
  const slotContent = renderPinnedContext(pinnedSlots);
4405
4573
  if (slotContent) blocks.push({
4406
4574
  type: "memory",
@@ -4424,6 +4592,24 @@ function registerContextFunction(sdk, kv, tokenBudget) {
4424
4592
  });
4425
4593
  }
4426
4594
  }
4595
+ const relevantLessons = lessons.filter((l) => !l.deleted && (!l.project || l.project === data.project)).sort((a, b) => {
4596
+ const scoreA = (a.project === data.project ? 1.5 : 1) * a.confidence;
4597
+ return (b.project === data.project ? 1.5 : 1) * b.confidence - scoreA;
4598
+ }).slice(0, 10);
4599
+ if (relevantLessons.length > 0) {
4600
+ const lessonsContent = `## Lessons Learned\n${relevantLessons.map((l) => `- (${l.confidence.toFixed(2)}) ${l.content}${l.context ? ` — ${l.context}` : ""}`).join("\n")}`;
4601
+ const mostRecent = relevantLessons.reduce((acc, l) => {
4602
+ const t = new Date(l.lastReinforcedAt || l.updatedAt).getTime();
4603
+ return t > acc ? t : acc;
4604
+ }, 0);
4605
+ blocks.push({
4606
+ type: "memory",
4607
+ content: lessonsContent,
4608
+ tokens: estimateTokens$1(lessonsContent),
4609
+ recency: mostRecent,
4610
+ sourceIds: relevantLessons.map((l) => l.id)
4611
+ });
4612
+ }
4427
4613
  const sessions = (await kv.list(KV.sessions)).filter((s) => s.project === data.project && s.id !== data.sessionId).sort((a, b) => new Date(b.startedAt).getTime() - new Date(a.startedAt).getTime()).slice(0, 10);
4428
4614
  const summariesPerSession = await Promise.all(sessions.map((s) => kv.get(KV.summaries, s.id).catch(() => null)));
4429
4615
  const sessionsNeedingObs = [];
@@ -5246,6 +5432,46 @@ const DEFAULTS$1 = {
5246
5432
  lowImportanceThreshold: 3,
5247
5433
  maxObservationsPerProject: 1e4
5248
5434
  };
5435
+ function isValidRecoveryResult(result) {
5436
+ if (!result || typeof result !== "object") return false;
5437
+ if (!("success" in result)) return true;
5438
+ return result.success !== false;
5439
+ }
5440
+ function isCompressedObservation(observation) {
5441
+ return "title" in observation && typeof observation.title === "string" && observation.title.length > 0;
5442
+ }
5443
+ async function recoverStaleSession(sdk, sessionId) {
5444
+ try {
5445
+ const result = await sdk.trigger({
5446
+ function_id: "event::session::stopped",
5447
+ payload: { sessionId }
5448
+ });
5449
+ if (!isValidRecoveryResult(result)) {
5450
+ logger.warn("Stale session recovery failed", {
5451
+ sessionId,
5452
+ result
5453
+ });
5454
+ return false;
5455
+ }
5456
+ return true;
5457
+ } catch (err) {
5458
+ logger.warn("Stale session recovery failed", {
5459
+ sessionId,
5460
+ error: err instanceof Error ? err.message : String(err)
5461
+ });
5462
+ return false;
5463
+ }
5464
+ }
5465
+ async function runRecoveredSessionConsolidation(sdk) {
5466
+ try {
5467
+ await sdk.trigger({
5468
+ function_id: "mem::consolidate-pipeline",
5469
+ payload: { tier: "all" }
5470
+ });
5471
+ } catch (err) {
5472
+ logger.warn("Recovered session consolidation failed", { error: err instanceof Error ? err.message : String(err) });
5473
+ }
5474
+ }
5249
5475
  function registerEvictFunction(sdk, kv) {
5250
5476
  sdk.registerFunction("mem::evict", async (data) => {
5251
5477
  const dryRun = data?.dryRun ?? false;
@@ -5264,6 +5490,7 @@ function registerEvictFunction(sdk, kv) {
5264
5490
  nonLatestMemories: 0,
5265
5491
  dryRun
5266
5492
  };
5493
+ let recoveredStaleSessions = 0;
5267
5494
  const sessions = await kv.list(KV.sessions).catch(() => []);
5268
5495
  const summaries = await kv.list(KV.summaries).catch(() => []);
5269
5496
  const summaryIds = new Set(summaries.map((s) => s.sessionId));
@@ -5271,6 +5498,23 @@ function registerEvictFunction(sdk, kv) {
5271
5498
  if (!session.startedAt) continue;
5272
5499
  if (now - new Date(session.startedAt).getTime() > cfg.staleSessionDays * MS_PER_DAY$1 && !summaryIds.has(session.id)) if (dryRun) stats.staleSessions++;
5273
5500
  else {
5501
+ const observations = await kv.list(KV.observations(session.id)).catch((err) => {
5502
+ logger.warn("Stale session observation scan failed", {
5503
+ sessionId: session.id,
5504
+ error: err instanceof Error ? err.message : String(err)
5505
+ });
5506
+ return null;
5507
+ });
5508
+ if (!observations) continue;
5509
+ let recovered = false;
5510
+ if (observations.some(isCompressedObservation)) {
5511
+ recovered = await recoverStaleSession(sdk, session.id);
5512
+ if (!recovered) continue;
5513
+ recoveredStaleSessions++;
5514
+ } else if (observations.length > 0) {
5515
+ logger.warn("Stale session has no compressed observations", { sessionId: session.id });
5516
+ continue;
5517
+ }
5274
5518
  try {
5275
5519
  await kv.delete(KV.sessions, session.id);
5276
5520
  stats.staleSessions++;
@@ -5284,11 +5528,12 @@ function registerEvictFunction(sdk, kv) {
5284
5528
  }
5285
5529
  await recordAudit(kv, "delete", "mem::evict", [session.id], {
5286
5530
  resource: "session",
5287
- reason: "stale_session_without_summary",
5531
+ reason: recovered ? "stale_session_recovered_then_evicted" : "stale_session_without_summary",
5288
5532
  dryRun
5289
5533
  });
5290
5534
  }
5291
5535
  }
5536
+ if (!dryRun && recoveredStaleSessions > 0) await runRecoveredSessionConsolidation(sdk);
5292
5537
  const projectObs = /* @__PURE__ */ new Map();
5293
5538
  for (const session of sessions) {
5294
5539
  const compressed = (await kv.list(KV.observations(session.id)).catch(() => [])).filter((o) => o.title);
@@ -5967,7 +6212,7 @@ function registerAutoForgetFunction(sdk, kv) {
5967
6212
 
5968
6213
  //#endregion
5969
6214
  //#region src/version.ts
5970
- const VERSION = "0.9.16";
6215
+ const VERSION = "0.9.18";
5971
6216
 
5972
6217
  //#endregion
5973
6218
  //#region src/functions/export-import.ts
@@ -6101,7 +6346,9 @@ function registerExportImportFunction(sdk, kv) {
6101
6346
  "0.9.13",
6102
6347
  "0.9.14",
6103
6348
  "0.9.15",
6104
- "0.9.16"
6349
+ "0.9.16",
6350
+ "0.9.17",
6351
+ "0.9.18"
6105
6352
  ]).has(importData.version)) return {
6106
6353
  success: false,
6107
6354
  error: `Unsupported export version: ${importData.version}`
@@ -19368,7 +19615,38 @@ function registerMcpEndpoints(sdk, kv, secret) {
19368
19615
 
19369
19616
  //#endregion
19370
19617
  //#region src/viewer/server.ts
19618
+ function loadViewerFavicon() {
19619
+ const base = dirname(fileURLToPath(import.meta.url));
19620
+ const candidates = [
19621
+ join(base, "..", "src", "viewer", "favicon.svg"),
19622
+ join(base, "..", "viewer", "favicon.svg"),
19623
+ join(base, "viewer", "favicon.svg")
19624
+ ];
19625
+ for (const path of candidates) try {
19626
+ return readFileSync(path);
19627
+ } catch {}
19628
+ return null;
19629
+ }
19371
19630
  const ALLOWED_ORIGINS = (process.env.VIEWER_ALLOWED_ORIGINS || "http://localhost:3111,http://localhost:3113,http://127.0.0.1:3111,http://127.0.0.1:3113").split(",").map((o) => o.trim());
19631
+ const ALLOWED_HOSTS_OVERRIDE = (process.env.VIEWER_ALLOWED_HOSTS || "").split(",").map((h) => h.trim().toLowerCase()).filter(Boolean);
19632
+ function buildAllowedHosts(origins, listenPort) {
19633
+ const hosts = /* @__PURE__ */ new Set();
19634
+ for (const o of origins) try {
19635
+ const parsed = new URL(o);
19636
+ if (parsed.host) hosts.add(parsed.host.toLowerCase());
19637
+ } catch {}
19638
+ hosts.add(`localhost:${listenPort}`);
19639
+ hosts.add(`127.0.0.1:${listenPort}`);
19640
+ hosts.add(`[::1]:${listenPort}`);
19641
+ for (const h of ALLOWED_HOSTS_OVERRIDE) hosts.add(h);
19642
+ return hosts;
19643
+ }
19644
+ function isHostAllowed(headerHost, allowed) {
19645
+ if (typeof headerHost !== "string") return false;
19646
+ const lower = headerHost.toLowerCase().trim();
19647
+ if (!lower) return false;
19648
+ return allowed.has(lower);
19649
+ }
19372
19650
  function corsHeaders(req) {
19373
19651
  const origin = req.headers.origin || "";
19374
19652
  const allowed = ALLOWED_ORIGINS.includes(origin) ? origin : ALLOWED_ORIGINS[0];
@@ -19412,7 +19690,17 @@ const MAX_VIEWER_PORT_RETRIES = 10;
19412
19690
  function startViewerServer(port, _kv, _sdk, secret, restPort) {
19413
19691
  const resolvedRestPort = restPort ?? port - 2;
19414
19692
  const requestedPort = port;
19693
+ let allowedHosts = null;
19415
19694
  const server = createServer(async (req, res) => {
19695
+ if (!allowedHosts) {
19696
+ const addr = server.address();
19697
+ allowedHosts = buildAllowedHosts(ALLOWED_ORIGINS, addr && typeof addr === "object" && "port" in addr ? addr.port : port);
19698
+ }
19699
+ if (!isHostAllowed(req.headers.host, allowedHosts)) {
19700
+ res.writeHead(403, { "Content-Type": "text/plain" });
19701
+ res.end("forbidden host");
19702
+ return;
19703
+ }
19416
19704
  const raw = req.url || "/";
19417
19705
  const qIdx = raw.indexOf("?");
19418
19706
  const pathname = qIdx >= 0 ? raw.slice(0, qIdx) : raw;
@@ -19441,6 +19729,20 @@ function startViewerServer(port, _kv, _sdk, secret, restPort) {
19441
19729
  res.end("viewer not found");
19442
19730
  return;
19443
19731
  }
19732
+ if (method === "GET" && pathname === "/favicon.svg") {
19733
+ const favicon = loadViewerFavicon();
19734
+ if (favicon) {
19735
+ res.writeHead(200, {
19736
+ "Content-Type": "image/svg+xml",
19737
+ "Cache-Control": "public, max-age=3600"
19738
+ });
19739
+ res.end(favicon);
19740
+ return;
19741
+ }
19742
+ res.writeHead(404, { "Content-Type": "text/plain" });
19743
+ res.end("favicon not found");
19744
+ return;
19745
+ }
19444
19746
  try {
19445
19747
  await proxyToRestApi(resolvedRestPort, pathname, qs, method, req, res, secret);
19446
19748
  } catch (err) {
@@ -19676,6 +19978,11 @@ async function main() {
19676
19978
  serviceName: OTEL_CONFIG.serviceName,
19677
19979
  serviceVersion: OTEL_CONFIG.serviceVersion,
19678
19980
  metricsExportIntervalMs: OTEL_CONFIG.metricsExportIntervalMs
19981
+ },
19982
+ telemetry: {
19983
+ project_name: "agentmemory",
19984
+ language: "node",
19985
+ framework: "iii-sdk"
19679
19986
  }
19680
19987
  });
19681
19988
  const kv = new StateKV(sdk);
@@ -19835,7 +20142,7 @@ async function main() {
19835
20142
  console.warn(`[agentmemory] Failed to backfill memories into BM25:`, err);
19836
20143
  }
19837
20144
  bootLog(`Ready. ${embeddingProvider ? "Triple-stream (BM25+Vector+Graph)" : "BM25+Graph"} search active.`);
19838
- bootLog(`REST API: 107 endpoints at http://localhost:${config.restPort}/agentmemory/*`);
20145
+ bootLog(`REST API: 121 endpoints at http://localhost:${config.restPort}/agentmemory/*`);
19839
20146
  bootLog(`MCP surface (opt-in via \`npx @agentmemory/mcp\`): ${getAllTools().length} tools · 6 resources · 3 prompts`);
19840
20147
  const viewerServer = startViewerServer(config.restPort + 2, kv, sdk, secret, config.restPort);
19841
20148
  const autoForgetIntervalMs = parseInt(process.env.AUTO_FORGET_INTERVAL_MS || "3600000", 10);