clawvault 2.6.3 → 2.6.5

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.
Files changed (127) hide show
  1. package/README.md +351 -21
  2. package/bin/clawvault.js +8 -2
  3. package/bin/command-runtime.js +9 -1
  4. package/bin/register-maintenance-commands.js +19 -0
  5. package/bin/register-query-commands.js +58 -6
  6. package/bin/register-workgraph-commands.js +451 -0
  7. package/dist/chunk-2GKPENIR.js +66 -0
  8. package/dist/{chunk-VXEOHTSL.js → chunk-2JQ3O2YL.js} +1 -1
  9. package/dist/{chunk-VR5NE7PZ.js → chunk-2RAZ4ZFE.js} +1 -1
  10. package/dist/chunk-2ZDO52B4.js +52 -0
  11. package/dist/{chunk-ZZA73MFY.js → chunk-33DOSHTA.js} +176 -36
  12. package/dist/chunk-4BQTQMJP.js +93 -0
  13. package/dist/{chunk-MAKNAHAW.js → chunk-5PJ4STIC.js} +98 -8
  14. package/dist/{chunk-RVYA52PY.js → chunk-5UM4PMMM.js} +1 -1
  15. package/dist/{chunk-4VQTUVH7.js → chunk-7YZWHM36.js} +52 -26
  16. package/dist/{chunk-4VRIMU4O.js → chunk-A4EAUO7T.js} +5 -5
  17. package/dist/{chunk-R6SXNSFD.js → chunk-BV5KWZKR.js} +3 -3
  18. package/dist/chunk-FBITHIZF.js +351 -0
  19. package/dist/{chunk-Q2J5YTUF.js → chunk-FUSLEY6L.js} +751 -34
  20. package/dist/chunk-GNJL4YGR.js +79 -0
  21. package/dist/{chunk-42MXU7A6.js → chunk-K4GFGKFD.js} +51 -47
  22. package/dist/{chunk-PBEE567J.js → chunk-KSZROBFH.js} +2 -2
  23. package/dist/chunk-L4HSSQ6T.js +152 -0
  24. package/dist/{chunk-PZ2AUU2W.js → chunk-LMKQ7NIF.js} +206 -37
  25. package/dist/{chunk-6546Q4OR.js → chunk-M5O6FQ66.js} +6 -6
  26. package/dist/chunk-MM6QGW3P.js +207 -0
  27. package/dist/{chunk-T76H47ZS.js → chunk-MNPUYCHQ.js} +1 -1
  28. package/dist/{chunk-P5EPF6MB.js → chunk-MW5C6ZQA.js} +110 -13
  29. package/dist/{chunk-OZ7RIXTO.js → chunk-QSRRMEYM.js} +2 -2
  30. package/dist/chunk-RHISK3SZ.js +189 -0
  31. package/dist/{chunk-3BTHWPMB.js → chunk-S5OJEGFG.js} +2 -2
  32. package/dist/{chunk-MGDEINGP.js → chunk-SS4B7P7V.js} +1 -1
  33. package/dist/{chunk-ME37YNW3.js → chunk-SV7T4HRE.js} +4 -4
  34. package/dist/{chunk-IEVLHNLU.js → chunk-T3FKSZSN.js} +3 -3
  35. package/dist/{chunk-DTEHFAL7.js → chunk-TS6NDVOU.js} +2 -2
  36. package/dist/chunk-U4O6C46S.js +154 -0
  37. package/dist/{chunk-ITPEXLHA.js → chunk-URXDAUVH.js} +24 -5
  38. package/dist/chunk-WMGIIABP.js +15 -0
  39. package/dist/{chunk-QVMXF7FY.js → chunk-X3SPPUFG.js} +50 -0
  40. package/dist/{chunk-THRJVD4L.js → chunk-Y6VJKXGL.js} +1 -1
  41. package/dist/{chunk-RCBMXTWS.js → chunk-YD7SVXTF.js} +39 -7
  42. package/dist/{chunk-HIHOUSXS.js → chunk-YXQCA6B7.js} +105 -1
  43. package/dist/cli/index.js +20 -18
  44. package/dist/commands/archive.js +3 -2
  45. package/dist/commands/backlog.js +1 -0
  46. package/dist/commands/blocked.js +1 -0
  47. package/dist/commands/canvas.js +2 -1
  48. package/dist/commands/checkpoint.js +1 -0
  49. package/dist/commands/compat.js +2 -1
  50. package/dist/commands/context.js +6 -4
  51. package/dist/commands/doctor.d.ts +10 -1
  52. package/dist/commands/doctor.js +13 -10
  53. package/dist/commands/embed.js +5 -3
  54. package/dist/commands/entities.js +2 -1
  55. package/dist/commands/graph.js +4 -3
  56. package/dist/commands/inject.d.ts +1 -1
  57. package/dist/commands/inject.js +5 -4
  58. package/dist/commands/kanban.js +1 -0
  59. package/dist/commands/link.js +5 -4
  60. package/dist/commands/migrate-observations.js +3 -2
  61. package/dist/commands/observe.js +9 -7
  62. package/dist/commands/project.js +1 -0
  63. package/dist/commands/rebuild-embeddings.d.ts +21 -0
  64. package/dist/commands/rebuild-embeddings.js +91 -0
  65. package/dist/commands/rebuild.js +6 -4
  66. package/dist/commands/recover.js +1 -0
  67. package/dist/commands/reflect.js +5 -4
  68. package/dist/commands/repair-session.js +1 -0
  69. package/dist/commands/replay.js +7 -6
  70. package/dist/commands/session-recap.js +1 -0
  71. package/dist/commands/setup.js +3 -2
  72. package/dist/commands/shell-init.js +2 -0
  73. package/dist/commands/sleep.d.ts +1 -1
  74. package/dist/commands/sleep.js +10 -8
  75. package/dist/commands/status.js +13 -82
  76. package/dist/commands/sync-bd.js +3 -2
  77. package/dist/commands/tailscale.js +3 -2
  78. package/dist/commands/task.js +1 -0
  79. package/dist/commands/template.js +1 -0
  80. package/dist/commands/wake.d.ts +1 -1
  81. package/dist/commands/wake.js +5 -3
  82. package/dist/index.d.ts +254 -10
  83. package/dist/index.js +288 -155
  84. package/dist/{inject-x65KXWPk.d.ts → inject-DYUrDqQO.d.ts} +2 -2
  85. package/dist/ledger-B7g7jhqG.d.ts +44 -0
  86. package/dist/lib/auto-linker.js +2 -1
  87. package/dist/lib/canvas-layout.js +1 -0
  88. package/dist/lib/config.d.ts +27 -3
  89. package/dist/lib/config.js +4 -1
  90. package/dist/lib/entity-index.js +1 -0
  91. package/dist/lib/project-utils.js +1 -0
  92. package/dist/lib/session-repair.js +1 -0
  93. package/dist/lib/session-utils.js +1 -0
  94. package/dist/lib/tailscale.js +1 -0
  95. package/dist/lib/task-utils.js +1 -0
  96. package/dist/lib/template-engine.js +1 -0
  97. package/dist/lib/webdav.js +1 -0
  98. package/dist/onnxruntime_binding-5QEF3SUC.node +0 -0
  99. package/dist/onnxruntime_binding-BKPKNEGC.node +0 -0
  100. package/dist/onnxruntime_binding-FMOXGIUT.node +0 -0
  101. package/dist/onnxruntime_binding-OI2KMXC5.node +0 -0
  102. package/dist/onnxruntime_binding-UX44MLAZ.node +0 -0
  103. package/dist/onnxruntime_binding-Y2W7N7WY.node +0 -0
  104. package/dist/registry-BR4326o0.d.ts +30 -0
  105. package/dist/store-CA-6sKCJ.d.ts +34 -0
  106. package/dist/thread-B9LhXNU0.d.ts +41 -0
  107. package/dist/transformers.node-A2ZRORSQ.js +46775 -0
  108. package/dist/{types-C74wgGL1.d.ts → types-BbWJoC1c.d.ts} +1 -1
  109. package/dist/workgraph/index.d.ts +5 -0
  110. package/dist/workgraph/index.js +23 -0
  111. package/dist/workgraph/ledger.d.ts +2 -0
  112. package/dist/workgraph/ledger.js +25 -0
  113. package/dist/workgraph/registry.d.ts +2 -0
  114. package/dist/workgraph/registry.js +19 -0
  115. package/dist/workgraph/store.d.ts +2 -0
  116. package/dist/workgraph/store.js +25 -0
  117. package/dist/workgraph/thread.d.ts +2 -0
  118. package/dist/workgraph/thread.js +25 -0
  119. package/dist/workgraph/types.d.ts +54 -0
  120. package/dist/workgraph/types.js +7 -0
  121. package/hooks/clawvault/handler.js +714 -2
  122. package/hooks/clawvault/handler.test.js +153 -0
  123. package/hooks/clawvault/openclaw.plugin.json +72 -0
  124. package/openclaw.plugin.json +14 -2
  125. package/package.json +5 -4
  126. package/dist/chunk-4QYGFWRM.js +0 -88
  127. package/dist/chunk-MXSSG3QU.js +0 -42
@@ -14,7 +14,11 @@ import {
14
14
  listConfig,
15
15
  listRouteRules,
16
16
  matchRouteRule
17
- } from "./chunk-ITPEXLHA.js";
17
+ } from "./chunk-URXDAUVH.js";
18
+ import {
19
+ requestLlmCompletion,
20
+ resolveLlmProvider
21
+ } from "./chunk-YXQCA6B7.js";
18
22
  import {
19
23
  ensureLedgerStructure,
20
24
  ensureParentDir,
@@ -33,13 +37,17 @@ import {
33
37
 
34
38
  // src/observer/compressor.ts
35
39
  var OPENAI_BASE_URL = "https://api.openai.com/v1";
40
+ var XAI_BASE_URL = "https://api.x.ai/v1";
36
41
  var OLLAMA_BASE_URL = "http://localhost:11434/v1";
37
42
  var DEFAULT_PROVIDER_MODELS = {
38
43
  anthropic: "claude-3-5-haiku-latest",
39
44
  openai: "gpt-4o-mini",
40
45
  gemini: "gemini-2.0-flash",
46
+ xai: "grok-2-latest",
41
47
  "openai-compatible": "gpt-4o-mini",
42
- ollama: "llama3.2"
48
+ ollama: "llama3.2",
49
+ minimax: "MiniMax-M2.1",
50
+ zai: "glm-4.5-air"
43
51
  };
44
52
  var CRITICAL_RE = /(?:\b(?:decision|decided|chose|chosen|selected|picked|opted|switched to)\s*:?|\bdecid(?:e|ed|ing|ion)\b|\berror\b|\bfail(?:ed|ure|ing)?\b|\bblock(?:ed|er)?\b|\bbreaking(?:\s+change)?s?\b|\bcritical\b|\b\w+\s+chosen\s+(?:for|over|as)\b|\bpublish(?:ed)?\b.*@?\d+\.\d+|\bmerge[d]?\s+(?:PR|pull\s+request)\b|\bshipped\b|\breleased?\b.*v?\d+\.\d+|\bsigned\b.*\b(?:contract|agreement|deal)\b|\bpricing\b.*\$|\bdemo\b.*\b(?:completed?|done|finished)\b|\bmeeting\b.*\b(?:completed?|done|finished)\b|\bstrategy\b.*\b(?:pivot|change|shift)\b)/i;
45
53
  var DEADLINE_WITH_DATE_RE = /(?:(?:\bdeadline\b|\bdue(?:\s+date)?\b|\bcutoff\b).*(?:\d{4}-\d{2}-\d{2}|\d{1,2}\/\d{1,2}(?:\/\d{2,4})?|(?:jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)[a-z]*\s+\d{1,2})|(?:\d{4}-\d{2}-\d{2}|\d{1,2}\/\d{1,2}(?:\/\d{2,4})?|(?:jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)[a-z]*\s+\d{1,2}).*(?:\bdeadline\b|\bdue(?:\s+date)?\b|\bcutoff\b))/i;
@@ -48,6 +56,11 @@ var TODO_SIGNAL_RE = /(?:\btodo:\s*|\bwe need to\b|\bdon't forget(?: to)?\b|\bre
48
56
  var COMMITMENT_TASK_SIGNAL_RE = /\b(?:i'?ll|i will|let me|(?:i'?m\s+)?going to|plan to|should)\b/i;
49
57
  var UNRESOLVED_COMMITMENT_RE = /\b(?:need to figure out|tbd|to be determined)\b/i;
50
58
  var DEADLINE_SIGNAL_RE = /\b(?:by\s+(?:monday|tuesday|wednesday|thursday|friday|saturday|sunday|tomorrow)|before\s+the\s+\w+|deadline is)\b/i;
59
+ var ROLE_PREFIX_RE = /^([a-z][a-z0-9_-]{1,31})\s*:\s*(.+)$/i;
60
+ var BASE64_DATA_URI_RE = /\bdata:[^;\s]+;base64,[A-Za-z0-9+/=]{24,}\b/gi;
61
+ var LONG_BASE64_TOKEN_RE = /\b[A-Za-z0-9+/]{80,}={0,2}\b/g;
62
+ var NOISE_PREFIX_RE = /^(?:metadata|system metadata|session metadata)\s*:/i;
63
+ var STRUCTURED_NOISE_MARKER_RE = /\b(?:tool[_-]?result|tool[_-]?use|toolcallid|tooluseid|function[_-]?(?:call|result)|stdout|stderr|exitcode|recordedat|trace(?:_|-)?id|parent(?:_|-)?id|session(?:_|-)?id|metadata|base64|mime(?:type)?)\b/i;
51
64
  var Compressor = class {
52
65
  provider;
53
66
  model;
@@ -64,7 +77,7 @@ var Compressor = class {
64
77
  this.fetchImpl = options.fetchImpl ?? fetch;
65
78
  }
66
79
  async compress(messages, existingObservations) {
67
- const cleanedMessages = messages.map((message) => message.trim()).filter(Boolean);
80
+ const cleanedMessages = this.sanitizeIncomingMessages(messages);
68
81
  if (cleanedMessages.length === 0) {
69
82
  return existingObservations.trim();
70
83
  }
@@ -72,7 +85,7 @@ var Compressor = class {
72
85
  const backend = this.resolveProvider();
73
86
  if (backend) {
74
87
  try {
75
- const llmOutput = backend.provider === "anthropic" ? await this.callAnthropic(prompt, backend) : backend.provider === "gemini" ? await this.callGemini(prompt, backend) : backend.provider === "openai" ? await this.callOpenAI(prompt, backend) : await this.callOpenAICompatible(prompt, backend);
88
+ const llmOutput = backend.provider === "anthropic" ? await this.callAnthropic(prompt, backend) : backend.provider === "gemini" ? await this.callGemini(prompt, backend) : backend.provider === "openai" ? await this.callOpenAI(prompt, backend) : backend.provider === "xai" ? await this.callXAI(prompt, backend) : await this.callOpenAICompatible(prompt, backend);
76
89
  const normalized = this.normalizeLlmOutput(llmOutput);
77
90
  if (normalized) {
78
91
  return this.mergeObservations(existingObservations, normalized);
@@ -83,6 +96,82 @@ var Compressor = class {
83
96
  const fallback = this.fallbackCompression(cleanedMessages);
84
97
  return this.mergeObservations(existingObservations, fallback);
85
98
  }
99
+ sanitizeIncomingMessages(messages) {
100
+ const sanitized = [];
101
+ for (const message of messages) {
102
+ const cleaned = this.sanitizeIncomingMessage(message);
103
+ if (cleaned) {
104
+ sanitized.push(cleaned);
105
+ }
106
+ }
107
+ return sanitized;
108
+ }
109
+ sanitizeIncomingMessage(message) {
110
+ const normalized = message.replace(/\s+/g, " ").trim();
111
+ if (!normalized) {
112
+ return "";
113
+ }
114
+ const roleMatch = ROLE_PREFIX_RE.exec(normalized);
115
+ if (roleMatch && this.isConversationRolePrefix(roleMatch[1])) {
116
+ const role = this.normalizeMessageRole(roleMatch[1]);
117
+ if (this.shouldDropMessageRole(role)) {
118
+ return "";
119
+ }
120
+ const content = this.stripNoisyData(roleMatch[2]);
121
+ if (!content || this.isLikelyStructuredNoise(content)) {
122
+ return "";
123
+ }
124
+ return `${role}: ${content}`;
125
+ }
126
+ const cleaned = this.stripNoisyData(normalized);
127
+ if (!cleaned || this.isLikelyStructuredNoise(cleaned)) {
128
+ return "";
129
+ }
130
+ return cleaned;
131
+ }
132
+ normalizeMessageRole(role) {
133
+ return role.trim().toLowerCase();
134
+ }
135
+ isConversationRolePrefix(role) {
136
+ const normalized = role.trim().toLowerCase().replace(/[\s_-]+/g, "");
137
+ if (!normalized) {
138
+ return false;
139
+ }
140
+ if (normalized === "user" || normalized === "assistant" || normalized === "system") {
141
+ return true;
142
+ }
143
+ if (normalized === "developer" || normalized === "metadata") {
144
+ return true;
145
+ }
146
+ return normalized.startsWith("tool");
147
+ }
148
+ shouldDropMessageRole(role) {
149
+ const normalized = role.replace(/[\s_-]+/g, "");
150
+ if (!normalized) {
151
+ return false;
152
+ }
153
+ if (normalized === "system" || normalized === "developer" || normalized === "metadata") {
154
+ return true;
155
+ }
156
+ return normalized.startsWith("tool");
157
+ }
158
+ stripNoisyData(value) {
159
+ return value.replace(BASE64_DATA_URI_RE, " ").replace(LONG_BASE64_TOKEN_RE, " ").replace(/\s+/g, " ").trim();
160
+ }
161
+ isLikelyStructuredNoise(value) {
162
+ const trimmed = value.trim();
163
+ if (!trimmed) {
164
+ return true;
165
+ }
166
+ if (NOISE_PREFIX_RE.test(trimmed)) {
167
+ return true;
168
+ }
169
+ const looksStructured = trimmed.startsWith("{") || trimmed.startsWith("[");
170
+ if (looksStructured && STRUCTURED_NOISE_MARKER_RE.test(trimmed) && trimmed.length >= 40) {
171
+ return true;
172
+ }
173
+ return false;
174
+ }
86
175
  resolveProvider() {
87
176
  if (process.env.CLAWVAULT_NO_LLM) return null;
88
177
  if (this.provider) {
@@ -130,6 +219,18 @@ var Compressor = class {
130
219
  baseUrl: this.resolveBaseUrl(provider)
131
220
  };
132
221
  }
222
+ if (provider === "xai") {
223
+ const apiKey2 = this.resolveApiKey(provider);
224
+ if (!apiKey2) {
225
+ return null;
226
+ }
227
+ return {
228
+ provider,
229
+ model,
230
+ apiKey: apiKey2,
231
+ baseUrl: XAI_BASE_URL
232
+ };
233
+ }
133
234
  const apiKey = this.resolveApiKey(provider) ?? void 0;
134
235
  return {
135
236
  provider,
@@ -164,6 +265,15 @@ var Compressor = class {
164
265
  apiKey: geminiApiKey
165
266
  };
166
267
  }
268
+ const xaiApiKey = this.readEnvValue("XAI_API_KEY");
269
+ if (xaiApiKey) {
270
+ return {
271
+ provider: "xai",
272
+ model: allowConfiguredModel ? this.resolveModel("xai") : DEFAULT_PROVIDER_MODELS.xai,
273
+ apiKey: xaiApiKey,
274
+ baseUrl: XAI_BASE_URL
275
+ };
276
+ }
167
277
  return null;
168
278
  }
169
279
  resolveModel(provider) {
@@ -184,6 +294,9 @@ var Compressor = class {
184
294
  if (provider === "gemini") {
185
295
  return this.readEnvValue("GEMINI_API_KEY");
186
296
  }
297
+ if (provider === "xai") {
298
+ return this.readEnvValue("XAI_API_KEY");
299
+ }
187
300
  return this.readEnvValue("OPENAI_API_KEY");
188
301
  }
189
302
  resolveBaseUrl(provider) {
@@ -316,6 +429,9 @@ var Compressor = class {
316
429
  async callOpenAI(prompt, backend) {
317
430
  return this.callOpenAICompatible(prompt, backend);
318
431
  }
432
+ async callXAI(prompt, backend) {
433
+ return this.callOpenAICompatible(prompt, backend);
434
+ }
319
435
  async callOpenAICompatible(prompt, backend) {
320
436
  const baseUrl = backend.baseUrl ?? this.resolveBaseUrl(backend.provider);
321
437
  const response = await this.fetchImpl(this.buildOpenAICompatibleUrl(baseUrl), {
@@ -691,13 +807,496 @@ var Reflector = class {
691
807
  }
692
808
  };
693
809
 
694
- // src/observer/observer.ts
695
- import * as fs2 from "fs";
696
- import * as path2 from "path";
810
+ // src/lib/fact-extractor.ts
811
+ function normalizeEntity(name) {
812
+ return name.toLowerCase().replace(/[^a-z0-9\s]/g, "").replace(/\s+/g, " ").trim();
813
+ }
814
+ function factId(entity, relation, value) {
815
+ const key = `${normalizeEntity(entity)}::${relation.toLowerCase()}::${value.toLowerCase().trim()}`;
816
+ let hash = 0;
817
+ for (let i = 0; i < key.length; i++) {
818
+ const char = key.charCodeAt(i);
819
+ hash = (hash << 5) - hash + char;
820
+ hash = hash & hash;
821
+ }
822
+ return Math.abs(hash).toString(36);
823
+ }
824
+ var PREFERENCE_PATTERNS = [
825
+ {
826
+ // "I prefer X" / "I like X" / "I love X" / "I enjoy X"
827
+ pattern: /\b(?:i|user|they)\s+(?:prefer|like|love|enjoy|want|favor)s?\s+(.+?)(?:\.|,|$)/i,
828
+ extract: (m) => ({
829
+ entity: "user",
830
+ relation: "prefers",
831
+ value: m[1].trim(),
832
+ category: "preference"
833
+ })
834
+ },
835
+ {
836
+ // "my favorite X is Y"
837
+ pattern: /\bmy\s+(?:favorite|favourite|preferred)\s+(\w+)\s+(?:is|are)\s+(.+?)(?:\.|,|$)/i,
838
+ extract: (m) => ({
839
+ entity: "user",
840
+ relation: `favorite_${m[1].toLowerCase()}`,
841
+ value: m[2].trim(),
842
+ category: "preference"
843
+ })
844
+ },
845
+ {
846
+ // "I don't like X" / "I hate X" / "I dislike X"
847
+ pattern: /\b(?:i|user)\s+(?:don'?t\s+like|hate|dislike|avoid)s?\s+(.+?)(?:\.|,|$)/i,
848
+ extract: (m) => ({
849
+ entity: "user",
850
+ relation: "dislikes",
851
+ value: m[1].trim(),
852
+ category: "preference"
853
+ })
854
+ },
855
+ {
856
+ // "I'm allergic to X" / "I have an allergy to X"
857
+ pattern: /\b(?:i'?m|i\s+am|i\s+have)\s+(?:an?\s+)?allerg(?:ic|y)\s+(?:to\s+)?(.+?)(?:\.|,|$)/i,
858
+ extract: (m) => ({
859
+ entity: "user",
860
+ relation: "allergic_to",
861
+ value: m[1].trim(),
862
+ category: "preference"
863
+ })
864
+ }
865
+ ];
866
+ var FACT_PATTERNS = [
867
+ {
868
+ // "X works at Y" / "X is employed at Y"
869
+ pattern: /\b(\w+(?:\s+\w+)?)\s+(?:works?\s+(?:at|for)|is\s+employed\s+(?:at|by))\s+(.+?)(?:\.|,|$)/i,
870
+ extract: (m) => ({
871
+ entity: m[1].trim(),
872
+ relation: "works_at",
873
+ value: m[2].trim(),
874
+ category: "fact"
875
+ })
876
+ },
877
+ {
878
+ // "X lives in Y" / "X moved to Y"
879
+ pattern: /\b(\w+(?:\s+\w+)?)\s+(?:live[sd]?\s+in|moved?\s+to|relocated?\s+to)\s+(.+?)(?:\.|,|$)/i,
880
+ extract: (m) => ({
881
+ entity: m[1].trim(),
882
+ relation: "lives_in",
883
+ value: m[2].trim(),
884
+ category: "fact"
885
+ })
886
+ },
887
+ {
888
+ // "X is Y years old" / "X's age is Y"
889
+ pattern: /\b(\w+(?:\s+\w+)?)\s+(?:is|turned)\s+(\d+)\s+years?\s+old/i,
890
+ extract: (m) => ({
891
+ entity: m[1].trim(),
892
+ relation: "age",
893
+ value: m[2],
894
+ category: "fact"
895
+ })
896
+ },
897
+ {
898
+ // "X bought Y" / "X purchased Y"
899
+ pattern: /\b(\w+(?:\s+\w+)?)\s+(?:bought|purchased|got|acquired)\s+(?:a\s+|an\s+|the\s+)?(.+?)(?:\s+for\s+\$?([\d,.]+))?(?:\.|,|$)/i,
900
+ extract: (m) => ({
901
+ entity: m[1].trim(),
902
+ relation: "bought",
903
+ value: m[3] ? `${m[2].trim()} ($${m[3]})` : m[2].trim(),
904
+ category: "event"
905
+ })
906
+ },
907
+ {
908
+ // "X spent $Y on Z"
909
+ pattern: /\b(\w+(?:\s+\w+)?)\s+spent\s+\$?([\d,.]+)\s+on\s+(.+?)(?:\.|,|$)/i,
910
+ extract: (m) => ({
911
+ entity: m[1].trim(),
912
+ relation: "spent_on",
913
+ value: `$${m[2]} on ${m[3].trim()}`,
914
+ category: "event"
915
+ })
916
+ }
917
+ ];
918
+ var DECISION_PATTERNS = [
919
+ {
920
+ // "decided to X" / "we decided X"
921
+ pattern: /\b(?:i|we|user)\s+decided\s+(?:to\s+)?(.+?)(?:\.|,|$)/i,
922
+ extract: (m) => ({
923
+ entity: "user",
924
+ relation: "decided",
925
+ value: m[1].trim(),
926
+ category: "decision"
927
+ })
928
+ },
929
+ {
930
+ // "chose X over Y"
931
+ pattern: /\b(?:i|we|user)\s+chose\s+(.+?)\s+over\s+(.+?)(?:\.|,|$)/i,
932
+ extract: (m) => ({
933
+ entity: "user",
934
+ relation: "chose",
935
+ value: `${m[1].trim()} (over ${m[2].trim()})`,
936
+ category: "decision"
937
+ })
938
+ }
939
+ ];
940
+ var ALL_PATTERNS = [...PREFERENCE_PATTERNS, ...FACT_PATTERNS, ...DECISION_PATTERNS];
941
+ function extractFactsRuleBased(text, source, timestamp) {
942
+ const facts = [];
943
+ const now = timestamp || (/* @__PURE__ */ new Date()).toISOString();
944
+ const sentences = text.split(/[.!?\n]+/).filter((s) => s.trim().length > 5);
945
+ for (const sentence of sentences) {
946
+ const trimmed = sentence.trim();
947
+ for (const rule of ALL_PATTERNS) {
948
+ const match = trimmed.match(rule.pattern);
949
+ if (match) {
950
+ const extracted = rule.extract(match);
951
+ if (extracted && extracted.value.length > 1 && extracted.value.length < 200) {
952
+ facts.push({
953
+ id: factId(extracted.entity, extracted.relation, extracted.value),
954
+ entity: extracted.entity,
955
+ entityNorm: normalizeEntity(extracted.entity),
956
+ relation: extracted.relation,
957
+ value: extracted.value,
958
+ validFrom: now,
959
+ validUntil: null,
960
+ confidence: 0.7,
961
+ // Rule-based gets moderate confidence
962
+ category: extracted.category,
963
+ source,
964
+ rawText: trimmed
965
+ });
966
+ }
967
+ }
968
+ }
969
+ }
970
+ return facts;
971
+ }
972
+ var EXTRACTION_PROMPT = `Extract structured facts from the following text. Return a JSON array of objects with these fields:
973
+ - entity: the subject (person, place, thing, or "user" for the speaker)
974
+ - relation: the relationship type (e.g., "prefers", "works_at", "lives_in", "bought", "spent_on", "age", "decided", "allergic_to")
975
+ - value: the object of the relation
976
+ - category: one of "preference", "fact", "decision", "entity", "event"
977
+ - confidence: 0.0 to 1.0
697
978
 
698
- // src/observer/router.ts
979
+ Rules:
980
+ - Extract ALL facts, preferences, decisions, and events
981
+ - For preferences, use "user" as entity
982
+ - For monetary amounts, include the dollar sign
983
+ - Be precise \u2014 only extract what's explicitly stated
984
+ - Return empty array [] if no facts found
985
+
986
+ Text:
987
+ `;
988
+ async function extractFactsLlm(text, source, timestamp, llmFn) {
989
+ if (!llmFn) {
990
+ return extractFactsRuleBased(text, source, timestamp);
991
+ }
992
+ const now = timestamp || (/* @__PURE__ */ new Date()).toISOString();
993
+ try {
994
+ const response = await llmFn(EXTRACTION_PROMPT + text);
995
+ const jsonMatch = response.match(/\[[\s\S]*?\]/);
996
+ if (!jsonMatch) {
997
+ return extractFactsRuleBased(text, source, timestamp);
998
+ }
999
+ const parsed = JSON.parse(jsonMatch[0]);
1000
+ return parsed.map((f) => ({
1001
+ id: factId(f.entity, f.relation, f.value),
1002
+ entity: f.entity,
1003
+ entityNorm: normalizeEntity(f.entity),
1004
+ relation: f.relation,
1005
+ value: f.value,
1006
+ validFrom: now,
1007
+ validUntil: null,
1008
+ confidence: Math.min(1, Math.max(0, f.confidence || 0.8)),
1009
+ category: f.category || "fact",
1010
+ source,
1011
+ rawText: text.substring(0, 500)
1012
+ }));
1013
+ } catch {
1014
+ return extractFactsRuleBased(text, source, timestamp);
1015
+ }
1016
+ }
1017
+
1018
+ // src/lib/fact-store.ts
699
1019
  import * as fs from "fs";
700
1020
  import * as path from "path";
1021
+ var FactStore = class {
1022
+ facts = /* @__PURE__ */ new Map();
1023
+ byEntity = /* @__PURE__ */ new Map();
1024
+ byRelation = /* @__PURE__ */ new Map();
1025
+ byCategory = /* @__PURE__ */ new Map();
1026
+ factsPath;
1027
+ dirty = false;
1028
+ constructor(vaultPath) {
1029
+ this.factsPath = path.join(vaultPath, ".clawvault", "facts.jsonl");
1030
+ }
1031
+ /** Load facts from disk */
1032
+ load() {
1033
+ this.facts.clear();
1034
+ this.byEntity.clear();
1035
+ this.byRelation.clear();
1036
+ this.byCategory.clear();
1037
+ if (!fs.existsSync(this.factsPath)) return;
1038
+ const lines = fs.readFileSync(this.factsPath, "utf-8").split("\n").filter((l) => l.trim());
1039
+ for (const line of lines) {
1040
+ try {
1041
+ const fact = JSON.parse(line);
1042
+ this.indexFact(fact);
1043
+ } catch {
1044
+ }
1045
+ }
1046
+ }
1047
+ /** Add facts with conflict resolution. Returns number of conflicts resolved. */
1048
+ addFacts(newFacts) {
1049
+ let conflicts = 0;
1050
+ for (const fact of newFacts) {
1051
+ const existing = this.findConflict(fact);
1052
+ if (existing) {
1053
+ existing.validUntil = fact.validFrom;
1054
+ conflicts++;
1055
+ }
1056
+ this.indexFact(fact);
1057
+ this.dirty = true;
1058
+ }
1059
+ return conflicts;
1060
+ }
1061
+ /** Find an existing fact that conflicts with the new one */
1062
+ findConflict(newFact) {
1063
+ const entityFacts = this.byEntity.get(newFact.entityNorm);
1064
+ if (!entityFacts) return null;
1065
+ for (const id of entityFacts) {
1066
+ const existing = this.facts.get(id);
1067
+ if (!existing || existing.validUntil) continue;
1068
+ if (existing.relation === newFact.relation) {
1069
+ if (this.isSimilarValue(existing.value, newFact.value)) {
1070
+ return existing;
1071
+ }
1072
+ if (this.isExclusiveRelation(newFact.relation)) {
1073
+ return existing;
1074
+ }
1075
+ }
1076
+ }
1077
+ return null;
1078
+ }
1079
+ /** Check if two values are similar enough to be considered the same fact */
1080
+ isSimilarValue(a, b) {
1081
+ const na = a.toLowerCase().trim();
1082
+ const nb = b.toLowerCase().trim();
1083
+ if (na === nb) return true;
1084
+ if (na.includes(nb) || nb.includes(na)) return true;
1085
+ return false;
1086
+ }
1087
+ /** Relations where only one value can be active at a time */
1088
+ isExclusiveRelation(relation) {
1089
+ const exclusive = /* @__PURE__ */ new Set([
1090
+ "lives_in",
1091
+ "works_at",
1092
+ "age",
1093
+ "favorite_color",
1094
+ "favorite_food",
1095
+ "favorite_restaurant",
1096
+ "favorite_movie",
1097
+ "favorite_book",
1098
+ "favorite_music",
1099
+ "favorite_sport",
1100
+ "job_title",
1101
+ "employer",
1102
+ "marital_status",
1103
+ "city",
1104
+ "country"
1105
+ ]);
1106
+ return exclusive.has(relation);
1107
+ }
1108
+ /** Index a fact in all lookup maps */
1109
+ indexFact(fact) {
1110
+ this.facts.set(fact.id, fact);
1111
+ if (!this.byEntity.has(fact.entityNorm)) {
1112
+ this.byEntity.set(fact.entityNorm, /* @__PURE__ */ new Set());
1113
+ }
1114
+ this.byEntity.get(fact.entityNorm).add(fact.id);
1115
+ if (!this.byRelation.has(fact.relation)) {
1116
+ this.byRelation.set(fact.relation, /* @__PURE__ */ new Set());
1117
+ }
1118
+ this.byRelation.get(fact.relation).add(fact.id);
1119
+ if (!this.byCategory.has(fact.category)) {
1120
+ this.byCategory.set(fact.category, /* @__PURE__ */ new Set());
1121
+ }
1122
+ this.byCategory.get(fact.category).add(fact.id);
1123
+ }
1124
+ /** Save facts to disk (full rewrite for consistency) */
1125
+ save() {
1126
+ if (!this.dirty && fs.existsSync(this.factsPath)) return;
1127
+ const dir = path.dirname(this.factsPath);
1128
+ if (!fs.existsSync(dir)) {
1129
+ fs.mkdirSync(dir, { recursive: true });
1130
+ }
1131
+ const lines = Array.from(this.facts.values()).map((f) => JSON.stringify(f)).join("\n");
1132
+ fs.writeFileSync(this.factsPath, lines + "\n", "utf-8");
1133
+ this.dirty = false;
1134
+ }
1135
+ /** Append new facts to disk (faster than full rewrite) */
1136
+ append(facts) {
1137
+ const dir = path.dirname(this.factsPath);
1138
+ if (!fs.existsSync(dir)) {
1139
+ fs.mkdirSync(dir, { recursive: true });
1140
+ }
1141
+ const lines = facts.map((f) => JSON.stringify(f)).join("\n");
1142
+ fs.appendFileSync(this.factsPath, lines + "\n", "utf-8");
1143
+ }
1144
+ // ─── Query methods ──────────────────────────────────────────────────────
1145
+ /** Get all active facts for an entity */
1146
+ getEntityFacts(entity) {
1147
+ const norm = normalizeEntity(entity);
1148
+ const ids = this.byEntity.get(norm);
1149
+ if (!ids) return [];
1150
+ return Array.from(ids).map((id) => this.facts.get(id)).filter((f) => f && !f.validUntil);
1151
+ }
1152
+ /** Get all active facts for a relation */
1153
+ getRelationFacts(relation) {
1154
+ const ids = this.byRelation.get(relation);
1155
+ if (!ids) return [];
1156
+ return Array.from(ids).map((id) => this.facts.get(id)).filter((f) => f && !f.validUntil);
1157
+ }
1158
+ /** Get all active facts in a category */
1159
+ getCategoryFacts(category) {
1160
+ const ids = this.byCategory.get(category);
1161
+ if (!ids) return [];
1162
+ return Array.from(ids).map((id) => this.facts.get(id)).filter((f) => f && !f.validUntil);
1163
+ }
1164
+ /** Get all active preferences */
1165
+ getPreferences() {
1166
+ return this.getCategoryFacts("preference");
1167
+ }
1168
+ /** Search facts by text query (simple keyword match) */
1169
+ searchFacts(query) {
1170
+ const terms = query.toLowerCase().split(/\s+/);
1171
+ const results = [];
1172
+ for (const fact of this.facts.values()) {
1173
+ if (fact.validUntil) continue;
1174
+ const text = `${fact.entity} ${fact.relation} ${fact.value} ${fact.rawText}`.toLowerCase();
1175
+ const matches = terms.filter((t) => text.includes(t)).length;
1176
+ if (matches >= Math.ceil(terms.length * 0.5)) {
1177
+ results.push(fact);
1178
+ }
1179
+ }
1180
+ return results;
1181
+ }
1182
+ /** Get facts valid at a specific time */
1183
+ getFactsAt(timestamp) {
1184
+ const t = new Date(timestamp).getTime();
1185
+ const results = [];
1186
+ for (const fact of this.facts.values()) {
1187
+ const from = new Date(fact.validFrom).getTime();
1188
+ const until = fact.validUntil ? new Date(fact.validUntil).getTime() : Infinity;
1189
+ if (t >= from && t < until) {
1190
+ results.push(fact);
1191
+ }
1192
+ }
1193
+ return results;
1194
+ }
1195
+ /** Get stats */
1196
+ stats() {
1197
+ const active = Array.from(this.facts.values()).filter((f) => !f.validUntil);
1198
+ return {
1199
+ totalFacts: this.facts.size,
1200
+ activeFacts: active.length,
1201
+ supersededFacts: this.facts.size - active.length,
1202
+ entities: this.byEntity.size,
1203
+ relations: this.byRelation.size
1204
+ };
1205
+ }
1206
+ /** Get all facts (for testing/debugging) */
1207
+ getAllFacts() {
1208
+ return Array.from(this.facts.values());
1209
+ }
1210
+ };
1211
+
1212
+ // src/lib/llm-adapter.ts
1213
+ var GEMINI_FLASH_MODEL = "gemini-2.0-flash";
1214
+ function createGeminiFlashAdapter(options = {}) {
1215
+ const apiKey = process.env.GEMINI_API_KEY;
1216
+ return {
1217
+ async call(prompt) {
1218
+ if (!apiKey) {
1219
+ return "";
1220
+ }
1221
+ return requestLlmCompletion({
1222
+ prompt,
1223
+ provider: "gemini",
1224
+ model: options.model ?? GEMINI_FLASH_MODEL,
1225
+ temperature: options.temperature ?? 0.1,
1226
+ maxTokens: options.maxTokens ?? 2e3,
1227
+ fetchImpl: options.fetchImpl
1228
+ });
1229
+ },
1230
+ isAvailable() {
1231
+ return Boolean(apiKey);
1232
+ },
1233
+ getProvider() {
1234
+ return apiKey ? "gemini" : null;
1235
+ }
1236
+ };
1237
+ }
1238
+ function createDefaultAdapter(options = {}) {
1239
+ const resolvedProvider = options.provider !== void 0 ? options.provider : resolveLlmProvider();
1240
+ return {
1241
+ async call(prompt) {
1242
+ if (!resolvedProvider) {
1243
+ return "";
1244
+ }
1245
+ return requestLlmCompletion({
1246
+ prompt,
1247
+ provider: resolvedProvider,
1248
+ model: options.model,
1249
+ temperature: options.temperature ?? 0.1,
1250
+ maxTokens: options.maxTokens ?? 2e3,
1251
+ fetchImpl: options.fetchImpl
1252
+ });
1253
+ },
1254
+ isAvailable() {
1255
+ return resolvedProvider !== null;
1256
+ },
1257
+ getProvider() {
1258
+ return resolvedProvider;
1259
+ }
1260
+ };
1261
+ }
1262
+ function createFactExtractionAdapter(options = {}) {
1263
+ if (options.provider) {
1264
+ return createDefaultAdapter(options);
1265
+ }
1266
+ const geminiAdapter = createGeminiFlashAdapter(options);
1267
+ if (geminiAdapter.isAvailable()) {
1268
+ return geminiAdapter;
1269
+ }
1270
+ return createDefaultAdapter(options);
1271
+ }
1272
+ function createLlmFunction(adapter) {
1273
+ if (!adapter.isAvailable()) {
1274
+ return void 0;
1275
+ }
1276
+ return (prompt) => adapter.call(prompt);
1277
+ }
1278
+ function resolveFactExtractionMode(configuredMode, adapter) {
1279
+ const mode = configuredMode ?? "llm";
1280
+ if (mode === "off") {
1281
+ return { mode: "off", useLlm: false };
1282
+ }
1283
+ if (mode === "rule") {
1284
+ return { mode: "rule", useLlm: false };
1285
+ }
1286
+ const llmAvailable = adapter?.isAvailable() ?? resolveLlmProvider() !== null;
1287
+ if (mode === "llm" || mode === "hybrid") {
1288
+ return { mode, useLlm: llmAvailable };
1289
+ }
1290
+ return { mode: "rule", useLlm: false };
1291
+ }
1292
+
1293
+ // src/observer/observer.ts
1294
+ import * as fs3 from "fs";
1295
+ import * as path3 from "path";
1296
+
1297
+ // src/observer/router.ts
1298
+ import * as fs2 from "fs";
1299
+ import * as path2 from "path";
701
1300
  var CATEGORY_PATTERNS = [
702
1301
  {
703
1302
  category: "decisions",
@@ -764,17 +1363,41 @@ var FUTURE_TASK_HINT_RE = /\b(need to|should|todo|must|plan to)\b/i;
764
1363
  var Router = class {
765
1364
  vaultPath;
766
1365
  extractTasks;
1366
+ extractFacts;
1367
+ factExtractionMode;
1368
+ llmAdapter;
1369
+ factStore;
767
1370
  now;
768
1371
  customRoutes;
769
1372
  constructor(vaultPath, options = {}) {
770
- this.vaultPath = path.resolve(vaultPath);
1373
+ this.vaultPath = path2.resolve(vaultPath);
771
1374
  this.extractTasks = options.extractTasks ?? true;
1375
+ this.extractFacts = options.extractFacts ?? true;
1376
+ this.factExtractionMode = options.factExtractionMode ?? this.loadFactExtractionMode();
1377
+ this.llmAdapter = options.llmAdapter ?? createFactExtractionAdapter();
1378
+ this.factStore = new FactStore(this.vaultPath);
772
1379
  this.now = options.now ?? (() => /* @__PURE__ */ new Date());
773
1380
  this.customRoutes = this.loadCustomRoutes();
1381
+ if (this.extractFacts && this.factExtractionMode !== "off") {
1382
+ this.factStore.load();
1383
+ }
1384
+ }
1385
+ loadFactExtractionMode() {
1386
+ try {
1387
+ const config = listConfig(this.vaultPath);
1388
+ const observer = config.observer;
1389
+ const mode = observer?.factExtractionMode;
1390
+ if (mode === "off" || mode === "rule" || mode === "llm" || mode === "hybrid") {
1391
+ return mode;
1392
+ }
1393
+ } catch {
1394
+ }
1395
+ return "llm";
774
1396
  }
775
1397
  /**
776
1398
  * Takes observation markdown and routes items to appropriate vault categories.
777
1399
  * Routes only items with importance >= 0.4.
1400
+ * Also extracts structured facts from observations when fact extraction is enabled.
778
1401
  * Returns a summary of what was routed where.
779
1402
  */
780
1403
  route(observationMarkdown, context = {}) {
@@ -784,6 +1407,7 @@ var Router = class {
784
1407
  const knownWorkItems = this.extractTasks ? this.loadExistingWorkItems() : [];
785
1408
  const knownProjectDefinitions = this.loadKnownProjectDefinitions();
786
1409
  let dedupHits = 0;
1410
+ let factsExtracted = 0;
787
1411
  for (const item of items) {
788
1412
  if (item.importance < 0.4) continue;
789
1413
  if (this.extractTasks && this.isTaskObservation(item.type)) {
@@ -810,8 +1434,81 @@ var Router = class {
810
1434
  routed.push(routedItem);
811
1435
  this.appendToCategory(category, routedItem, knownProjectDefinitions);
812
1436
  }
813
- const summary = this.buildSummary(routed, dedupHits);
814
- return { routed, summary };
1437
+ if (this.extractFacts && this.factExtractionMode !== "off") {
1438
+ const extractedCount = this.extractAndStoreFacts(observationMarkdown, context);
1439
+ factsExtracted = extractedCount;
1440
+ }
1441
+ const summary = this.buildSummary(routed, dedupHits, factsExtracted);
1442
+ return { routed, summary, factsExtracted };
1443
+ }
1444
+ /**
1445
+ * Extract facts from observation markdown and store them in the fact store.
1446
+ * Uses the configured extraction mode (rule-based, LLM, or hybrid).
1447
+ */
1448
+ extractAndStoreFacts(observationMarkdown, context) {
1449
+ const { mode, useLlm } = resolveFactExtractionMode(this.factExtractionMode, this.llmAdapter);
1450
+ if (mode === "off") {
1451
+ return 0;
1452
+ }
1453
+ const source = context.source ?? "observer";
1454
+ const timestamp = context.timestamp?.toISOString() ?? this.now().toISOString();
1455
+ let facts = [];
1456
+ if (mode === "rule" || !useLlm) {
1457
+ facts = extractFactsRuleBased(observationMarkdown, source, timestamp);
1458
+ } else if (mode === "llm") {
1459
+ const llmFn = createLlmFunction(this.llmAdapter);
1460
+ extractFactsLlm(observationMarkdown, source, timestamp, llmFn).then((llmFacts) => {
1461
+ if (llmFacts.length > 0) {
1462
+ this.factStore.addFacts(llmFacts);
1463
+ this.factStore.save();
1464
+ }
1465
+ }).catch(() => {
1466
+ const ruleFacts = extractFactsRuleBased(observationMarkdown, source, timestamp);
1467
+ if (ruleFacts.length > 0) {
1468
+ this.factStore.addFacts(ruleFacts);
1469
+ this.factStore.save();
1470
+ }
1471
+ });
1472
+ return 0;
1473
+ } else if (mode === "hybrid") {
1474
+ facts = extractFactsRuleBased(observationMarkdown, source, timestamp);
1475
+ const llmFn = createLlmFunction(this.llmAdapter);
1476
+ extractFactsLlm(observationMarkdown, source, timestamp, llmFn).then((llmFacts) => {
1477
+ const merged = this.mergeFacts(facts, llmFacts);
1478
+ if (merged.length > facts.length) {
1479
+ this.factStore.addFacts(merged.slice(facts.length));
1480
+ this.factStore.save();
1481
+ }
1482
+ }).catch(() => {
1483
+ });
1484
+ }
1485
+ if (facts.length > 0) {
1486
+ this.factStore.addFacts(facts);
1487
+ this.factStore.save();
1488
+ }
1489
+ return facts.length;
1490
+ }
1491
+ /**
1492
+ * Merge facts from rule-based and LLM extraction, deduplicating by entity+relation.
1493
+ */
1494
+ mergeFacts(ruleFacts, llmFacts) {
1495
+ const seen = /* @__PURE__ */ new Set();
1496
+ const merged = [];
1497
+ for (const fact of ruleFacts) {
1498
+ const key = `${fact.entityNorm}::${fact.relation}`;
1499
+ if (!seen.has(key)) {
1500
+ seen.add(key);
1501
+ merged.push(fact);
1502
+ }
1503
+ }
1504
+ for (const fact of llmFacts) {
1505
+ const key = `${fact.entityNorm}::${fact.relation}`;
1506
+ if (!seen.has(key)) {
1507
+ seen.add(key);
1508
+ merged.push(fact);
1509
+ }
1510
+ }
1511
+ return merged;
815
1512
  }
816
1513
  isTaskObservation(type) {
817
1514
  return type === "task" || type === "todo" || type === "commitment-unresolved";
@@ -1153,46 +1850,46 @@ var Router = class {
1153
1850
  resolveFilePath(category, item, knownProjectDefinitions) {
1154
1851
  const customEntityPath = this.resolveCustomEntityPath(item.content, category);
1155
1852
  if (customEntityPath) {
1156
- const customEntityDir = path.join(this.vaultPath, category, customEntityPath);
1157
- fs.mkdirSync(customEntityDir, { recursive: true });
1853
+ const customEntityDir = path2.join(this.vaultPath, category, customEntityPath);
1854
+ fs2.mkdirSync(customEntityDir, { recursive: true });
1158
1855
  return {
1159
- filePath: path.join(customEntityDir, `${item.date}.md`),
1856
+ filePath: path2.join(customEntityDir, `${item.date}.md`),
1160
1857
  headerLabel: `${category}/${customEntityPath}`
1161
1858
  };
1162
1859
  }
1163
1860
  if (category === "projects") {
1164
1861
  const matchedProjectSlug = this.matchKnownProjectSlug(item.content, knownProjectDefinitions);
1165
1862
  if (matchedProjectSlug) {
1166
- const projectDir = path.join(this.vaultPath, category, matchedProjectSlug);
1167
- fs.mkdirSync(projectDir, { recursive: true });
1863
+ const projectDir = path2.join(this.vaultPath, category, matchedProjectSlug);
1864
+ fs2.mkdirSync(projectDir, { recursive: true });
1168
1865
  return {
1169
- filePath: path.join(projectDir, `${item.date}.md`),
1866
+ filePath: path2.join(projectDir, `${item.date}.md`),
1170
1867
  headerLabel: `${category}/${matchedProjectSlug}`
1171
1868
  };
1172
1869
  }
1173
1870
  } else {
1174
1871
  const entitySlug = this.extractEntitySlug(item.content, category);
1175
1872
  if (entitySlug) {
1176
- const entityDir = path.join(this.vaultPath, category, entitySlug);
1177
- fs.mkdirSync(entityDir, { recursive: true });
1873
+ const entityDir = path2.join(this.vaultPath, category, entitySlug);
1874
+ fs2.mkdirSync(entityDir, { recursive: true });
1178
1875
  return {
1179
- filePath: path.join(entityDir, `${item.date}.md`),
1876
+ filePath: path2.join(entityDir, `${item.date}.md`),
1180
1877
  headerLabel: `${category}/${entitySlug}`
1181
1878
  };
1182
1879
  }
1183
1880
  }
1184
- const categoryDir = path.join(this.vaultPath, category);
1185
- fs.mkdirSync(categoryDir, { recursive: true });
1881
+ const categoryDir = path2.join(this.vaultPath, category);
1882
+ fs2.mkdirSync(categoryDir, { recursive: true });
1186
1883
  return {
1187
- filePath: path.join(categoryDir, `${item.date}.md`),
1884
+ filePath: path2.join(categoryDir, `${item.date}.md`),
1188
1885
  headerLabel: category
1189
1886
  };
1190
1887
  }
1191
1888
  appendToCategory(category, item, knownProjectDefinitions) {
1192
1889
  const destination = this.resolveFilePath(category, item, knownProjectDefinitions);
1193
1890
  const filePath = destination.filePath;
1194
- fs.mkdirSync(path.dirname(filePath), { recursive: true });
1195
- const existing = fs.existsSync(filePath) ? fs.readFileSync(filePath, "utf-8").trim() : "";
1891
+ fs2.mkdirSync(path2.dirname(filePath), { recursive: true });
1892
+ const existing = fs2.existsSync(filePath) ? fs2.readFileSync(filePath, "utf-8").trim() : "";
1196
1893
  const normalizedNew = this.normalizeForDedup(item.content);
1197
1894
  const existingLines = existing.split(/\r?\n/);
1198
1895
  for (const line of existingLines) {
@@ -1226,7 +1923,7 @@ ${entry}
1226
1923
  ` : `${header}
1227
1924
  ${entry}
1228
1925
  `;
1229
- fs.writeFileSync(filePath, newContent, "utf-8");
1926
+ fs2.writeFileSync(filePath, newContent, "utf-8");
1230
1927
  }
1231
1928
  /**
1232
1929
  * Auto-link proper nouns and known entities with [[wiki-links]].
@@ -1399,8 +2096,8 @@ ${entry}
1399
2096
  for (const bg of setA) if (setB.has(bg)) intersection++;
1400
2097
  return intersection / (setA.size + setB.size - intersection);
1401
2098
  }
1402
- buildSummary(routed, dedupHits) {
1403
- if (routed.length === 0) {
2099
+ buildSummary(routed, dedupHits, factsExtracted = 0) {
2100
+ if (routed.length === 0 && factsExtracted === 0) {
1404
2101
  if (dedupHits > 0) {
1405
2102
  return `No items routed to vault categories (dedup hits: ${dedupHits}).`;
1406
2103
  }
@@ -1411,7 +2108,17 @@ ${entry}
1411
2108
  byCat.set(item.category, (byCat.get(item.category) ?? 0) + 1);
1412
2109
  }
1413
2110
  const parts = [...byCat.entries()].map(([cat, count]) => `${cat}: ${count}`);
1414
- const suffix = dedupHits > 0 ? ` (dedup hits: ${dedupHits})` : "";
2111
+ const suffixParts = [];
2112
+ if (dedupHits > 0) {
2113
+ suffixParts.push(`dedup hits: ${dedupHits}`);
2114
+ }
2115
+ if (factsExtracted > 0) {
2116
+ suffixParts.push(`facts: ${factsExtracted}`);
2117
+ }
2118
+ const suffix = suffixParts.length > 0 ? ` (${suffixParts.join(", ")})` : "";
2119
+ if (routed.length === 0 && factsExtracted > 0) {
2120
+ return `Extracted ${factsExtracted} facts${suffix}`;
2121
+ }
1415
2122
  return `Routed ${routed.length} observations \u2192 ${parts.join(", ")}${suffix}`;
1416
2123
  }
1417
2124
  };
@@ -1479,7 +2186,7 @@ var Observer = class {
1479
2186
  observationsCache = "";
1480
2187
  lastRoutingSummary = "";
1481
2188
  constructor(vaultPath, options = {}) {
1482
- this.vaultPath = path2.resolve(vaultPath);
2189
+ this.vaultPath = path3.resolve(vaultPath);
1483
2190
  this.tokenThreshold = options.tokenThreshold ?? 3e4;
1484
2191
  this.reflectThreshold = options.reflectThreshold ?? 4e4;
1485
2192
  this.now = options.now ?? (() => /* @__PURE__ */ new Date());
@@ -1583,14 +2290,14 @@ var Observer = class {
1583
2290
  return this.readObservationFile(getLegacyObservationPath(this.vaultPath, toDateKey(date)));
1584
2291
  }
1585
2292
  readObservationFile(filePath) {
1586
- if (!fs2.existsSync(filePath)) {
2293
+ if (!fs3.existsSync(filePath)) {
1587
2294
  return "";
1588
2295
  }
1589
- return fs2.readFileSync(filePath, "utf-8").trim();
2296
+ return fs3.readFileSync(filePath, "utf-8").trim();
1590
2297
  }
1591
2298
  writeObservationFile(filePath, content) {
1592
2299
  ensureParentDir(filePath);
1593
- fs2.writeFileSync(filePath, `${content.trim()}
2300
+ fs3.writeFileSync(filePath, `${content.trim()}
1594
2301
  `, "utf-8");
1595
2302
  }
1596
2303
  deduplicateObservationMarkdown(markdown) {
@@ -1638,7 +2345,7 @@ var Observer = class {
1638
2345
  transcriptId: options.transcriptId ?? null,
1639
2346
  message
1640
2347
  }));
1641
- fs2.appendFileSync(rawPath, `${records.join("\n")}
2348
+ fs3.appendFileSync(rawPath, `${records.join("\n")}
1642
2349
  `, "utf-8");
1643
2350
  }
1644
2351
  sanitizeSource(source) {
@@ -1661,5 +2368,15 @@ var Observer = class {
1661
2368
  export {
1662
2369
  Compressor,
1663
2370
  Reflector,
2371
+ normalizeEntity,
2372
+ factId,
2373
+ extractFactsRuleBased,
2374
+ extractFactsLlm,
2375
+ FactStore,
2376
+ createGeminiFlashAdapter,
2377
+ createDefaultAdapter,
2378
+ createFactExtractionAdapter,
2379
+ createLlmFunction,
2380
+ resolveFactExtractionMode,
1664
2381
  Observer
1665
2382
  };