clawvault 2.6.1 → 2.6.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +352 -20
- package/bin/clawvault.js +8 -2
- package/bin/command-runtime.js +9 -1
- package/bin/register-maintenance-commands.js +19 -0
- package/bin/register-query-commands.js +58 -6
- package/bin/register-workgraph-commands.js +451 -0
- package/dist/{chunk-VXEOHTSL.js → chunk-2JQ3O2YL.js} +1 -1
- package/dist/{chunk-VR5NE7PZ.js → chunk-2RAZ4ZFE.js} +1 -1
- package/dist/chunk-2ZDO52B4.js +52 -0
- package/dist/chunk-4BQTQMJP.js +93 -0
- package/dist/{chunk-MAKNAHAW.js → chunk-5PJ4STIC.js} +98 -8
- package/dist/{chunk-IEVLHNLU.js → chunk-627Q3QWK.js} +3 -3
- package/dist/{chunk-R6SXNSFD.js → chunk-6NYYDNNG.js} +3 -3
- package/dist/chunk-ECRZL5XR.js +50 -0
- package/dist/chunk-GNJL4YGR.js +79 -0
- package/dist/{chunk-OZ7RIXTO.js → chunk-IIOU45CK.js} +1 -1
- package/dist/chunk-L4HSSQ6T.js +152 -0
- package/dist/{chunk-XAVB4GB4.js → chunk-LIGHWOH6.js} +1 -1
- package/dist/{chunk-PBEE567J.js → chunk-LUBZXECN.js} +2 -2
- package/dist/{chunk-UEOUADMO.js → chunk-MFL6EEPF.js} +204 -35
- package/dist/chunk-MM6QGW3P.js +207 -0
- package/dist/{chunk-T76H47ZS.js → chunk-MNPUYCHQ.js} +1 -1
- package/dist/{chunk-TLGBDTYT.js → chunk-MPOSMDMU.js} +6 -6
- package/dist/{chunk-RVYA52PY.js → chunk-NJYJL5AA.js} +1 -1
- package/dist/{chunk-Q2J5YTUF.js → chunk-OQGYFZ4A.js} +669 -33
- package/dist/{chunk-ME37YNW3.js → chunk-P7SY3D4E.js} +3 -3
- package/dist/chunk-RHISK3SZ.js +189 -0
- package/dist/{chunk-3BTHWPMB.js → chunk-S5OJEGFG.js} +2 -2
- package/dist/{chunk-MGDEINGP.js → chunk-SS4B7P7V.js} +1 -1
- package/dist/chunk-U4O6C46S.js +154 -0
- package/dist/{chunk-ITPEXLHA.js → chunk-URXDAUVH.js} +24 -5
- package/dist/chunk-WIOLLGAD.js +190 -0
- package/dist/chunk-WMGIIABP.js +15 -0
- package/dist/{chunk-QVMXF7FY.js → chunk-X3SPPUFG.js} +50 -0
- package/dist/{chunk-THRJVD4L.js → chunk-Y6VJKXGL.js} +1 -1
- package/dist/{chunk-KL4NAOMO.js → chunk-YDWHS4LJ.js} +49 -9
- package/dist/{chunk-4VRIMU4O.js → chunk-YNIPYN4F.js} +4 -4
- package/dist/{chunk-HIHOUSXS.js → chunk-YXQCA6B7.js} +105 -1
- package/dist/cli/index.js +18 -16
- package/dist/commands/archive.js +3 -2
- package/dist/commands/backlog.js +1 -0
- package/dist/commands/blocked.js +1 -0
- package/dist/commands/canvas.js +1 -0
- package/dist/commands/checkpoint.js +1 -0
- package/dist/commands/compat.js +2 -1
- package/dist/commands/context.js +5 -3
- package/dist/commands/doctor.d.ts +10 -1
- package/dist/commands/doctor.js +11 -8
- package/dist/commands/embed.js +5 -3
- package/dist/commands/entities.js +2 -1
- package/dist/commands/graph.js +3 -2
- package/dist/commands/inject.d.ts +1 -1
- package/dist/commands/inject.js +4 -3
- package/dist/commands/kanban.js +1 -0
- package/dist/commands/link.js +2 -1
- package/dist/commands/migrate-observations.js +3 -2
- package/dist/commands/observe.js +8 -6
- package/dist/commands/project.js +1 -0
- package/dist/commands/rebuild-embeddings.d.ts +21 -0
- package/dist/commands/rebuild-embeddings.js +91 -0
- package/dist/commands/rebuild.js +6 -4
- package/dist/commands/recover.js +1 -0
- package/dist/commands/reflect.js +5 -4
- package/dist/commands/repair-session.js +1 -0
- package/dist/commands/replay.js +7 -6
- package/dist/commands/session-recap.js +1 -0
- package/dist/commands/setup.js +3 -2
- package/dist/commands/shell-init.js +2 -0
- package/dist/commands/sleep.d.ts +1 -1
- package/dist/commands/sleep.js +8 -6
- package/dist/commands/status.d.ts +2 -0
- package/dist/commands/status.js +35 -24
- package/dist/commands/sync-bd.js +3 -2
- package/dist/commands/tailscale.js +3 -2
- package/dist/commands/task.js +1 -0
- package/dist/commands/template.js +1 -0
- package/dist/commands/wake.d.ts +1 -1
- package/dist/commands/wake.js +4 -2
- package/dist/index.d.ts +333 -10
- package/dist/index.js +320 -33
- package/dist/{inject-x65KXWPk.d.ts → inject-DYUrDqQO.d.ts} +2 -2
- package/dist/ledger-B7g7jhqG.d.ts +44 -0
- package/dist/lib/auto-linker.js +1 -0
- package/dist/lib/canvas-layout.js +1 -0
- package/dist/lib/config.d.ts +27 -3
- package/dist/lib/config.js +4 -1
- package/dist/lib/entity-index.js +1 -0
- package/dist/lib/project-utils.js +1 -0
- package/dist/lib/session-repair.js +1 -0
- package/dist/lib/session-utils.js +1 -0
- package/dist/lib/tailscale.js +1 -0
- package/dist/lib/task-utils.js +1 -0
- package/dist/lib/template-engine.js +1 -0
- package/dist/lib/webdav.js +1 -0
- package/dist/onnxruntime_binding-5QEF3SUC.node +0 -0
- package/dist/onnxruntime_binding-BKPKNEGC.node +0 -0
- package/dist/onnxruntime_binding-FMOXGIUT.node +0 -0
- package/dist/onnxruntime_binding-OI2KMXC5.node +0 -0
- package/dist/onnxruntime_binding-UX44MLAZ.node +0 -0
- package/dist/onnxruntime_binding-Y2W7N7WY.node +0 -0
- package/dist/registry-BR4326o0.d.ts +30 -0
- package/dist/store-CA-6sKCJ.d.ts +34 -0
- package/dist/thread-B9LhXNU0.d.ts +41 -0
- package/dist/transformers.node-A2ZRORSQ.js +46775 -0
- package/dist/{types-C74wgGL1.d.ts → types-BbWJoC1c.d.ts} +1 -1
- package/dist/workgraph/index.d.ts +5 -0
- package/dist/workgraph/index.js +23 -0
- package/dist/workgraph/ledger.d.ts +2 -0
- package/dist/workgraph/ledger.js +25 -0
- package/dist/workgraph/registry.d.ts +2 -0
- package/dist/workgraph/registry.js +19 -0
- package/dist/workgraph/store.d.ts +2 -0
- package/dist/workgraph/store.js +25 -0
- package/dist/workgraph/thread.d.ts +2 -0
- package/dist/workgraph/thread.js +25 -0
- package/dist/workgraph/types.d.ts +54 -0
- package/dist/workgraph/types.js +7 -0
- package/hooks/clawvault/HOOK.md +34 -4
- package/hooks/clawvault/handler.js +751 -8
- package/hooks/clawvault/handler.test.js +247 -0
- package/hooks/clawvault/openclaw.plugin.json +72 -0
- package/openclaw.plugin.json +84 -0
- package/package.json +8 -4
- package/dist/chunk-4QYGFWRM.js +0 -88
- package/dist/chunk-MXSSG3QU.js +0 -42
|
@@ -14,7 +14,11 @@ import {
|
|
|
14
14
|
listConfig,
|
|
15
15
|
listRouteRules,
|
|
16
16
|
matchRouteRule
|
|
17
|
-
} from "./chunk-
|
|
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;
|
|
@@ -72,7 +80,7 @@ var Compressor = class {
|
|
|
72
80
|
const backend = this.resolveProvider();
|
|
73
81
|
if (backend) {
|
|
74
82
|
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);
|
|
83
|
+
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
84
|
const normalized = this.normalizeLlmOutput(llmOutput);
|
|
77
85
|
if (normalized) {
|
|
78
86
|
return this.mergeObservations(existingObservations, normalized);
|
|
@@ -130,6 +138,18 @@ var Compressor = class {
|
|
|
130
138
|
baseUrl: this.resolveBaseUrl(provider)
|
|
131
139
|
};
|
|
132
140
|
}
|
|
141
|
+
if (provider === "xai") {
|
|
142
|
+
const apiKey2 = this.resolveApiKey(provider);
|
|
143
|
+
if (!apiKey2) {
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
return {
|
|
147
|
+
provider,
|
|
148
|
+
model,
|
|
149
|
+
apiKey: apiKey2,
|
|
150
|
+
baseUrl: XAI_BASE_URL
|
|
151
|
+
};
|
|
152
|
+
}
|
|
133
153
|
const apiKey = this.resolveApiKey(provider) ?? void 0;
|
|
134
154
|
return {
|
|
135
155
|
provider,
|
|
@@ -164,6 +184,15 @@ var Compressor = class {
|
|
|
164
184
|
apiKey: geminiApiKey
|
|
165
185
|
};
|
|
166
186
|
}
|
|
187
|
+
const xaiApiKey = this.readEnvValue("XAI_API_KEY");
|
|
188
|
+
if (xaiApiKey) {
|
|
189
|
+
return {
|
|
190
|
+
provider: "xai",
|
|
191
|
+
model: allowConfiguredModel ? this.resolveModel("xai") : DEFAULT_PROVIDER_MODELS.xai,
|
|
192
|
+
apiKey: xaiApiKey,
|
|
193
|
+
baseUrl: XAI_BASE_URL
|
|
194
|
+
};
|
|
195
|
+
}
|
|
167
196
|
return null;
|
|
168
197
|
}
|
|
169
198
|
resolveModel(provider) {
|
|
@@ -184,6 +213,9 @@ var Compressor = class {
|
|
|
184
213
|
if (provider === "gemini") {
|
|
185
214
|
return this.readEnvValue("GEMINI_API_KEY");
|
|
186
215
|
}
|
|
216
|
+
if (provider === "xai") {
|
|
217
|
+
return this.readEnvValue("XAI_API_KEY");
|
|
218
|
+
}
|
|
187
219
|
return this.readEnvValue("OPENAI_API_KEY");
|
|
188
220
|
}
|
|
189
221
|
resolveBaseUrl(provider) {
|
|
@@ -316,6 +348,9 @@ var Compressor = class {
|
|
|
316
348
|
async callOpenAI(prompt, backend) {
|
|
317
349
|
return this.callOpenAICompatible(prompt, backend);
|
|
318
350
|
}
|
|
351
|
+
async callXAI(prompt, backend) {
|
|
352
|
+
return this.callOpenAICompatible(prompt, backend);
|
|
353
|
+
}
|
|
319
354
|
async callOpenAICompatible(prompt, backend) {
|
|
320
355
|
const baseUrl = backend.baseUrl ?? this.resolveBaseUrl(backend.provider);
|
|
321
356
|
const response = await this.fetchImpl(this.buildOpenAICompatibleUrl(baseUrl), {
|
|
@@ -691,13 +726,496 @@ var Reflector = class {
|
|
|
691
726
|
}
|
|
692
727
|
};
|
|
693
728
|
|
|
694
|
-
// src/
|
|
695
|
-
|
|
696
|
-
|
|
729
|
+
// src/lib/fact-extractor.ts
|
|
730
|
+
function normalizeEntity(name) {
|
|
731
|
+
return name.toLowerCase().replace(/[^a-z0-9\s]/g, "").replace(/\s+/g, " ").trim();
|
|
732
|
+
}
|
|
733
|
+
function factId(entity, relation, value) {
|
|
734
|
+
const key = `${normalizeEntity(entity)}::${relation.toLowerCase()}::${value.toLowerCase().trim()}`;
|
|
735
|
+
let hash = 0;
|
|
736
|
+
for (let i = 0; i < key.length; i++) {
|
|
737
|
+
const char = key.charCodeAt(i);
|
|
738
|
+
hash = (hash << 5) - hash + char;
|
|
739
|
+
hash = hash & hash;
|
|
740
|
+
}
|
|
741
|
+
return Math.abs(hash).toString(36);
|
|
742
|
+
}
|
|
743
|
+
var PREFERENCE_PATTERNS = [
|
|
744
|
+
{
|
|
745
|
+
// "I prefer X" / "I like X" / "I love X" / "I enjoy X"
|
|
746
|
+
pattern: /\b(?:i|user|they)\s+(?:prefer|like|love|enjoy|want|favor)s?\s+(.+?)(?:\.|,|$)/i,
|
|
747
|
+
extract: (m) => ({
|
|
748
|
+
entity: "user",
|
|
749
|
+
relation: "prefers",
|
|
750
|
+
value: m[1].trim(),
|
|
751
|
+
category: "preference"
|
|
752
|
+
})
|
|
753
|
+
},
|
|
754
|
+
{
|
|
755
|
+
// "my favorite X is Y"
|
|
756
|
+
pattern: /\bmy\s+(?:favorite|favourite|preferred)\s+(\w+)\s+(?:is|are)\s+(.+?)(?:\.|,|$)/i,
|
|
757
|
+
extract: (m) => ({
|
|
758
|
+
entity: "user",
|
|
759
|
+
relation: `favorite_${m[1].toLowerCase()}`,
|
|
760
|
+
value: m[2].trim(),
|
|
761
|
+
category: "preference"
|
|
762
|
+
})
|
|
763
|
+
},
|
|
764
|
+
{
|
|
765
|
+
// "I don't like X" / "I hate X" / "I dislike X"
|
|
766
|
+
pattern: /\b(?:i|user)\s+(?:don'?t\s+like|hate|dislike|avoid)s?\s+(.+?)(?:\.|,|$)/i,
|
|
767
|
+
extract: (m) => ({
|
|
768
|
+
entity: "user",
|
|
769
|
+
relation: "dislikes",
|
|
770
|
+
value: m[1].trim(),
|
|
771
|
+
category: "preference"
|
|
772
|
+
})
|
|
773
|
+
},
|
|
774
|
+
{
|
|
775
|
+
// "I'm allergic to X" / "I have an allergy to X"
|
|
776
|
+
pattern: /\b(?:i'?m|i\s+am|i\s+have)\s+(?:an?\s+)?allerg(?:ic|y)\s+(?:to\s+)?(.+?)(?:\.|,|$)/i,
|
|
777
|
+
extract: (m) => ({
|
|
778
|
+
entity: "user",
|
|
779
|
+
relation: "allergic_to",
|
|
780
|
+
value: m[1].trim(),
|
|
781
|
+
category: "preference"
|
|
782
|
+
})
|
|
783
|
+
}
|
|
784
|
+
];
|
|
785
|
+
var FACT_PATTERNS = [
|
|
786
|
+
{
|
|
787
|
+
// "X works at Y" / "X is employed at Y"
|
|
788
|
+
pattern: /\b(\w+(?:\s+\w+)?)\s+(?:works?\s+(?:at|for)|is\s+employed\s+(?:at|by))\s+(.+?)(?:\.|,|$)/i,
|
|
789
|
+
extract: (m) => ({
|
|
790
|
+
entity: m[1].trim(),
|
|
791
|
+
relation: "works_at",
|
|
792
|
+
value: m[2].trim(),
|
|
793
|
+
category: "fact"
|
|
794
|
+
})
|
|
795
|
+
},
|
|
796
|
+
{
|
|
797
|
+
// "X lives in Y" / "X moved to Y"
|
|
798
|
+
pattern: /\b(\w+(?:\s+\w+)?)\s+(?:live[sd]?\s+in|moved?\s+to|relocated?\s+to)\s+(.+?)(?:\.|,|$)/i,
|
|
799
|
+
extract: (m) => ({
|
|
800
|
+
entity: m[1].trim(),
|
|
801
|
+
relation: "lives_in",
|
|
802
|
+
value: m[2].trim(),
|
|
803
|
+
category: "fact"
|
|
804
|
+
})
|
|
805
|
+
},
|
|
806
|
+
{
|
|
807
|
+
// "X is Y years old" / "X's age is Y"
|
|
808
|
+
pattern: /\b(\w+(?:\s+\w+)?)\s+(?:is|turned)\s+(\d+)\s+years?\s+old/i,
|
|
809
|
+
extract: (m) => ({
|
|
810
|
+
entity: m[1].trim(),
|
|
811
|
+
relation: "age",
|
|
812
|
+
value: m[2],
|
|
813
|
+
category: "fact"
|
|
814
|
+
})
|
|
815
|
+
},
|
|
816
|
+
{
|
|
817
|
+
// "X bought Y" / "X purchased Y"
|
|
818
|
+
pattern: /\b(\w+(?:\s+\w+)?)\s+(?:bought|purchased|got|acquired)\s+(?:a\s+|an\s+|the\s+)?(.+?)(?:\s+for\s+\$?([\d,.]+))?(?:\.|,|$)/i,
|
|
819
|
+
extract: (m) => ({
|
|
820
|
+
entity: m[1].trim(),
|
|
821
|
+
relation: "bought",
|
|
822
|
+
value: m[3] ? `${m[2].trim()} ($${m[3]})` : m[2].trim(),
|
|
823
|
+
category: "event"
|
|
824
|
+
})
|
|
825
|
+
},
|
|
826
|
+
{
|
|
827
|
+
// "X spent $Y on Z"
|
|
828
|
+
pattern: /\b(\w+(?:\s+\w+)?)\s+spent\s+\$?([\d,.]+)\s+on\s+(.+?)(?:\.|,|$)/i,
|
|
829
|
+
extract: (m) => ({
|
|
830
|
+
entity: m[1].trim(),
|
|
831
|
+
relation: "spent_on",
|
|
832
|
+
value: `$${m[2]} on ${m[3].trim()}`,
|
|
833
|
+
category: "event"
|
|
834
|
+
})
|
|
835
|
+
}
|
|
836
|
+
];
|
|
837
|
+
var DECISION_PATTERNS = [
|
|
838
|
+
{
|
|
839
|
+
// "decided to X" / "we decided X"
|
|
840
|
+
pattern: /\b(?:i|we|user)\s+decided\s+(?:to\s+)?(.+?)(?:\.|,|$)/i,
|
|
841
|
+
extract: (m) => ({
|
|
842
|
+
entity: "user",
|
|
843
|
+
relation: "decided",
|
|
844
|
+
value: m[1].trim(),
|
|
845
|
+
category: "decision"
|
|
846
|
+
})
|
|
847
|
+
},
|
|
848
|
+
{
|
|
849
|
+
// "chose X over Y"
|
|
850
|
+
pattern: /\b(?:i|we|user)\s+chose\s+(.+?)\s+over\s+(.+?)(?:\.|,|$)/i,
|
|
851
|
+
extract: (m) => ({
|
|
852
|
+
entity: "user",
|
|
853
|
+
relation: "chose",
|
|
854
|
+
value: `${m[1].trim()} (over ${m[2].trim()})`,
|
|
855
|
+
category: "decision"
|
|
856
|
+
})
|
|
857
|
+
}
|
|
858
|
+
];
|
|
859
|
+
var ALL_PATTERNS = [...PREFERENCE_PATTERNS, ...FACT_PATTERNS, ...DECISION_PATTERNS];
|
|
860
|
+
function extractFactsRuleBased(text, source, timestamp) {
|
|
861
|
+
const facts = [];
|
|
862
|
+
const now = timestamp || (/* @__PURE__ */ new Date()).toISOString();
|
|
863
|
+
const sentences = text.split(/[.!?\n]+/).filter((s) => s.trim().length > 5);
|
|
864
|
+
for (const sentence of sentences) {
|
|
865
|
+
const trimmed = sentence.trim();
|
|
866
|
+
for (const rule of ALL_PATTERNS) {
|
|
867
|
+
const match = trimmed.match(rule.pattern);
|
|
868
|
+
if (match) {
|
|
869
|
+
const extracted = rule.extract(match);
|
|
870
|
+
if (extracted && extracted.value.length > 1 && extracted.value.length < 200) {
|
|
871
|
+
facts.push({
|
|
872
|
+
id: factId(extracted.entity, extracted.relation, extracted.value),
|
|
873
|
+
entity: extracted.entity,
|
|
874
|
+
entityNorm: normalizeEntity(extracted.entity),
|
|
875
|
+
relation: extracted.relation,
|
|
876
|
+
value: extracted.value,
|
|
877
|
+
validFrom: now,
|
|
878
|
+
validUntil: null,
|
|
879
|
+
confidence: 0.7,
|
|
880
|
+
// Rule-based gets moderate confidence
|
|
881
|
+
category: extracted.category,
|
|
882
|
+
source,
|
|
883
|
+
rawText: trimmed
|
|
884
|
+
});
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
return facts;
|
|
890
|
+
}
|
|
891
|
+
var EXTRACTION_PROMPT = `Extract structured facts from the following text. Return a JSON array of objects with these fields:
|
|
892
|
+
- entity: the subject (person, place, thing, or "user" for the speaker)
|
|
893
|
+
- relation: the relationship type (e.g., "prefers", "works_at", "lives_in", "bought", "spent_on", "age", "decided", "allergic_to")
|
|
894
|
+
- value: the object of the relation
|
|
895
|
+
- category: one of "preference", "fact", "decision", "entity", "event"
|
|
896
|
+
- confidence: 0.0 to 1.0
|
|
697
897
|
|
|
698
|
-
|
|
898
|
+
Rules:
|
|
899
|
+
- Extract ALL facts, preferences, decisions, and events
|
|
900
|
+
- For preferences, use "user" as entity
|
|
901
|
+
- For monetary amounts, include the dollar sign
|
|
902
|
+
- Be precise \u2014 only extract what's explicitly stated
|
|
903
|
+
- Return empty array [] if no facts found
|
|
904
|
+
|
|
905
|
+
Text:
|
|
906
|
+
`;
|
|
907
|
+
async function extractFactsLlm(text, source, timestamp, llmFn) {
|
|
908
|
+
if (!llmFn) {
|
|
909
|
+
return extractFactsRuleBased(text, source, timestamp);
|
|
910
|
+
}
|
|
911
|
+
const now = timestamp || (/* @__PURE__ */ new Date()).toISOString();
|
|
912
|
+
try {
|
|
913
|
+
const response = await llmFn(EXTRACTION_PROMPT + text);
|
|
914
|
+
const jsonMatch = response.match(/\[[\s\S]*?\]/);
|
|
915
|
+
if (!jsonMatch) {
|
|
916
|
+
return extractFactsRuleBased(text, source, timestamp);
|
|
917
|
+
}
|
|
918
|
+
const parsed = JSON.parse(jsonMatch[0]);
|
|
919
|
+
return parsed.map((f) => ({
|
|
920
|
+
id: factId(f.entity, f.relation, f.value),
|
|
921
|
+
entity: f.entity,
|
|
922
|
+
entityNorm: normalizeEntity(f.entity),
|
|
923
|
+
relation: f.relation,
|
|
924
|
+
value: f.value,
|
|
925
|
+
validFrom: now,
|
|
926
|
+
validUntil: null,
|
|
927
|
+
confidence: Math.min(1, Math.max(0, f.confidence || 0.8)),
|
|
928
|
+
category: f.category || "fact",
|
|
929
|
+
source,
|
|
930
|
+
rawText: text.substring(0, 500)
|
|
931
|
+
}));
|
|
932
|
+
} catch {
|
|
933
|
+
return extractFactsRuleBased(text, source, timestamp);
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
// src/lib/fact-store.ts
|
|
699
938
|
import * as fs from "fs";
|
|
700
939
|
import * as path from "path";
|
|
940
|
+
var FactStore = class {
|
|
941
|
+
facts = /* @__PURE__ */ new Map();
|
|
942
|
+
byEntity = /* @__PURE__ */ new Map();
|
|
943
|
+
byRelation = /* @__PURE__ */ new Map();
|
|
944
|
+
byCategory = /* @__PURE__ */ new Map();
|
|
945
|
+
factsPath;
|
|
946
|
+
dirty = false;
|
|
947
|
+
constructor(vaultPath) {
|
|
948
|
+
this.factsPath = path.join(vaultPath, ".clawvault", "facts.jsonl");
|
|
949
|
+
}
|
|
950
|
+
/** Load facts from disk */
|
|
951
|
+
load() {
|
|
952
|
+
this.facts.clear();
|
|
953
|
+
this.byEntity.clear();
|
|
954
|
+
this.byRelation.clear();
|
|
955
|
+
this.byCategory.clear();
|
|
956
|
+
if (!fs.existsSync(this.factsPath)) return;
|
|
957
|
+
const lines = fs.readFileSync(this.factsPath, "utf-8").split("\n").filter((l) => l.trim());
|
|
958
|
+
for (const line of lines) {
|
|
959
|
+
try {
|
|
960
|
+
const fact = JSON.parse(line);
|
|
961
|
+
this.indexFact(fact);
|
|
962
|
+
} catch {
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
/** Add facts with conflict resolution. Returns number of conflicts resolved. */
|
|
967
|
+
addFacts(newFacts) {
|
|
968
|
+
let conflicts = 0;
|
|
969
|
+
for (const fact of newFacts) {
|
|
970
|
+
const existing = this.findConflict(fact);
|
|
971
|
+
if (existing) {
|
|
972
|
+
existing.validUntil = fact.validFrom;
|
|
973
|
+
conflicts++;
|
|
974
|
+
}
|
|
975
|
+
this.indexFact(fact);
|
|
976
|
+
this.dirty = true;
|
|
977
|
+
}
|
|
978
|
+
return conflicts;
|
|
979
|
+
}
|
|
980
|
+
/** Find an existing fact that conflicts with the new one */
|
|
981
|
+
findConflict(newFact) {
|
|
982
|
+
const entityFacts = this.byEntity.get(newFact.entityNorm);
|
|
983
|
+
if (!entityFacts) return null;
|
|
984
|
+
for (const id of entityFacts) {
|
|
985
|
+
const existing = this.facts.get(id);
|
|
986
|
+
if (!existing || existing.validUntil) continue;
|
|
987
|
+
if (existing.relation === newFact.relation) {
|
|
988
|
+
if (this.isSimilarValue(existing.value, newFact.value)) {
|
|
989
|
+
return existing;
|
|
990
|
+
}
|
|
991
|
+
if (this.isExclusiveRelation(newFact.relation)) {
|
|
992
|
+
return existing;
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
return null;
|
|
997
|
+
}
|
|
998
|
+
/** Check if two values are similar enough to be considered the same fact */
|
|
999
|
+
isSimilarValue(a, b) {
|
|
1000
|
+
const na = a.toLowerCase().trim();
|
|
1001
|
+
const nb = b.toLowerCase().trim();
|
|
1002
|
+
if (na === nb) return true;
|
|
1003
|
+
if (na.includes(nb) || nb.includes(na)) return true;
|
|
1004
|
+
return false;
|
|
1005
|
+
}
|
|
1006
|
+
/** Relations where only one value can be active at a time */
|
|
1007
|
+
isExclusiveRelation(relation) {
|
|
1008
|
+
const exclusive = /* @__PURE__ */ new Set([
|
|
1009
|
+
"lives_in",
|
|
1010
|
+
"works_at",
|
|
1011
|
+
"age",
|
|
1012
|
+
"favorite_color",
|
|
1013
|
+
"favorite_food",
|
|
1014
|
+
"favorite_restaurant",
|
|
1015
|
+
"favorite_movie",
|
|
1016
|
+
"favorite_book",
|
|
1017
|
+
"favorite_music",
|
|
1018
|
+
"favorite_sport",
|
|
1019
|
+
"job_title",
|
|
1020
|
+
"employer",
|
|
1021
|
+
"marital_status",
|
|
1022
|
+
"city",
|
|
1023
|
+
"country"
|
|
1024
|
+
]);
|
|
1025
|
+
return exclusive.has(relation);
|
|
1026
|
+
}
|
|
1027
|
+
/** Index a fact in all lookup maps */
|
|
1028
|
+
indexFact(fact) {
|
|
1029
|
+
this.facts.set(fact.id, fact);
|
|
1030
|
+
if (!this.byEntity.has(fact.entityNorm)) {
|
|
1031
|
+
this.byEntity.set(fact.entityNorm, /* @__PURE__ */ new Set());
|
|
1032
|
+
}
|
|
1033
|
+
this.byEntity.get(fact.entityNorm).add(fact.id);
|
|
1034
|
+
if (!this.byRelation.has(fact.relation)) {
|
|
1035
|
+
this.byRelation.set(fact.relation, /* @__PURE__ */ new Set());
|
|
1036
|
+
}
|
|
1037
|
+
this.byRelation.get(fact.relation).add(fact.id);
|
|
1038
|
+
if (!this.byCategory.has(fact.category)) {
|
|
1039
|
+
this.byCategory.set(fact.category, /* @__PURE__ */ new Set());
|
|
1040
|
+
}
|
|
1041
|
+
this.byCategory.get(fact.category).add(fact.id);
|
|
1042
|
+
}
|
|
1043
|
+
/** Save facts to disk (full rewrite for consistency) */
|
|
1044
|
+
save() {
|
|
1045
|
+
if (!this.dirty && fs.existsSync(this.factsPath)) return;
|
|
1046
|
+
const dir = path.dirname(this.factsPath);
|
|
1047
|
+
if (!fs.existsSync(dir)) {
|
|
1048
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
1049
|
+
}
|
|
1050
|
+
const lines = Array.from(this.facts.values()).map((f) => JSON.stringify(f)).join("\n");
|
|
1051
|
+
fs.writeFileSync(this.factsPath, lines + "\n", "utf-8");
|
|
1052
|
+
this.dirty = false;
|
|
1053
|
+
}
|
|
1054
|
+
/** Append new facts to disk (faster than full rewrite) */
|
|
1055
|
+
append(facts) {
|
|
1056
|
+
const dir = path.dirname(this.factsPath);
|
|
1057
|
+
if (!fs.existsSync(dir)) {
|
|
1058
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
1059
|
+
}
|
|
1060
|
+
const lines = facts.map((f) => JSON.stringify(f)).join("\n");
|
|
1061
|
+
fs.appendFileSync(this.factsPath, lines + "\n", "utf-8");
|
|
1062
|
+
}
|
|
1063
|
+
// ─── Query methods ──────────────────────────────────────────────────────
|
|
1064
|
+
/** Get all active facts for an entity */
|
|
1065
|
+
getEntityFacts(entity) {
|
|
1066
|
+
const norm = normalizeEntity(entity);
|
|
1067
|
+
const ids = this.byEntity.get(norm);
|
|
1068
|
+
if (!ids) return [];
|
|
1069
|
+
return Array.from(ids).map((id) => this.facts.get(id)).filter((f) => f && !f.validUntil);
|
|
1070
|
+
}
|
|
1071
|
+
/** Get all active facts for a relation */
|
|
1072
|
+
getRelationFacts(relation) {
|
|
1073
|
+
const ids = this.byRelation.get(relation);
|
|
1074
|
+
if (!ids) return [];
|
|
1075
|
+
return Array.from(ids).map((id) => this.facts.get(id)).filter((f) => f && !f.validUntil);
|
|
1076
|
+
}
|
|
1077
|
+
/** Get all active facts in a category */
|
|
1078
|
+
getCategoryFacts(category) {
|
|
1079
|
+
const ids = this.byCategory.get(category);
|
|
1080
|
+
if (!ids) return [];
|
|
1081
|
+
return Array.from(ids).map((id) => this.facts.get(id)).filter((f) => f && !f.validUntil);
|
|
1082
|
+
}
|
|
1083
|
+
/** Get all active preferences */
|
|
1084
|
+
getPreferences() {
|
|
1085
|
+
return this.getCategoryFacts("preference");
|
|
1086
|
+
}
|
|
1087
|
+
/** Search facts by text query (simple keyword match) */
|
|
1088
|
+
searchFacts(query) {
|
|
1089
|
+
const terms = query.toLowerCase().split(/\s+/);
|
|
1090
|
+
const results = [];
|
|
1091
|
+
for (const fact of this.facts.values()) {
|
|
1092
|
+
if (fact.validUntil) continue;
|
|
1093
|
+
const text = `${fact.entity} ${fact.relation} ${fact.value} ${fact.rawText}`.toLowerCase();
|
|
1094
|
+
const matches = terms.filter((t) => text.includes(t)).length;
|
|
1095
|
+
if (matches >= Math.ceil(terms.length * 0.5)) {
|
|
1096
|
+
results.push(fact);
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
return results;
|
|
1100
|
+
}
|
|
1101
|
+
/** Get facts valid at a specific time */
|
|
1102
|
+
getFactsAt(timestamp) {
|
|
1103
|
+
const t = new Date(timestamp).getTime();
|
|
1104
|
+
const results = [];
|
|
1105
|
+
for (const fact of this.facts.values()) {
|
|
1106
|
+
const from = new Date(fact.validFrom).getTime();
|
|
1107
|
+
const until = fact.validUntil ? new Date(fact.validUntil).getTime() : Infinity;
|
|
1108
|
+
if (t >= from && t < until) {
|
|
1109
|
+
results.push(fact);
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
return results;
|
|
1113
|
+
}
|
|
1114
|
+
/** Get stats */
|
|
1115
|
+
stats() {
|
|
1116
|
+
const active = Array.from(this.facts.values()).filter((f) => !f.validUntil);
|
|
1117
|
+
return {
|
|
1118
|
+
totalFacts: this.facts.size,
|
|
1119
|
+
activeFacts: active.length,
|
|
1120
|
+
supersededFacts: this.facts.size - active.length,
|
|
1121
|
+
entities: this.byEntity.size,
|
|
1122
|
+
relations: this.byRelation.size
|
|
1123
|
+
};
|
|
1124
|
+
}
|
|
1125
|
+
/** Get all facts (for testing/debugging) */
|
|
1126
|
+
getAllFacts() {
|
|
1127
|
+
return Array.from(this.facts.values());
|
|
1128
|
+
}
|
|
1129
|
+
};
|
|
1130
|
+
|
|
1131
|
+
// src/lib/llm-adapter.ts
|
|
1132
|
+
var GEMINI_FLASH_MODEL = "gemini-2.0-flash";
|
|
1133
|
+
function createGeminiFlashAdapter(options = {}) {
|
|
1134
|
+
const apiKey = process.env.GEMINI_API_KEY;
|
|
1135
|
+
return {
|
|
1136
|
+
async call(prompt) {
|
|
1137
|
+
if (!apiKey) {
|
|
1138
|
+
return "";
|
|
1139
|
+
}
|
|
1140
|
+
return requestLlmCompletion({
|
|
1141
|
+
prompt,
|
|
1142
|
+
provider: "gemini",
|
|
1143
|
+
model: options.model ?? GEMINI_FLASH_MODEL,
|
|
1144
|
+
temperature: options.temperature ?? 0.1,
|
|
1145
|
+
maxTokens: options.maxTokens ?? 2e3,
|
|
1146
|
+
fetchImpl: options.fetchImpl
|
|
1147
|
+
});
|
|
1148
|
+
},
|
|
1149
|
+
isAvailable() {
|
|
1150
|
+
return Boolean(apiKey);
|
|
1151
|
+
},
|
|
1152
|
+
getProvider() {
|
|
1153
|
+
return apiKey ? "gemini" : null;
|
|
1154
|
+
}
|
|
1155
|
+
};
|
|
1156
|
+
}
|
|
1157
|
+
function createDefaultAdapter(options = {}) {
|
|
1158
|
+
const resolvedProvider = options.provider !== void 0 ? options.provider : resolveLlmProvider();
|
|
1159
|
+
return {
|
|
1160
|
+
async call(prompt) {
|
|
1161
|
+
if (!resolvedProvider) {
|
|
1162
|
+
return "";
|
|
1163
|
+
}
|
|
1164
|
+
return requestLlmCompletion({
|
|
1165
|
+
prompt,
|
|
1166
|
+
provider: resolvedProvider,
|
|
1167
|
+
model: options.model,
|
|
1168
|
+
temperature: options.temperature ?? 0.1,
|
|
1169
|
+
maxTokens: options.maxTokens ?? 2e3,
|
|
1170
|
+
fetchImpl: options.fetchImpl
|
|
1171
|
+
});
|
|
1172
|
+
},
|
|
1173
|
+
isAvailable() {
|
|
1174
|
+
return resolvedProvider !== null;
|
|
1175
|
+
},
|
|
1176
|
+
getProvider() {
|
|
1177
|
+
return resolvedProvider;
|
|
1178
|
+
}
|
|
1179
|
+
};
|
|
1180
|
+
}
|
|
1181
|
+
function createFactExtractionAdapter(options = {}) {
|
|
1182
|
+
if (options.provider) {
|
|
1183
|
+
return createDefaultAdapter(options);
|
|
1184
|
+
}
|
|
1185
|
+
const geminiAdapter = createGeminiFlashAdapter(options);
|
|
1186
|
+
if (geminiAdapter.isAvailable()) {
|
|
1187
|
+
return geminiAdapter;
|
|
1188
|
+
}
|
|
1189
|
+
return createDefaultAdapter(options);
|
|
1190
|
+
}
|
|
1191
|
+
function createLlmFunction(adapter) {
|
|
1192
|
+
if (!adapter.isAvailable()) {
|
|
1193
|
+
return void 0;
|
|
1194
|
+
}
|
|
1195
|
+
return (prompt) => adapter.call(prompt);
|
|
1196
|
+
}
|
|
1197
|
+
function resolveFactExtractionMode(configuredMode, adapter) {
|
|
1198
|
+
const mode = configuredMode ?? "llm";
|
|
1199
|
+
if (mode === "off") {
|
|
1200
|
+
return { mode: "off", useLlm: false };
|
|
1201
|
+
}
|
|
1202
|
+
if (mode === "rule") {
|
|
1203
|
+
return { mode: "rule", useLlm: false };
|
|
1204
|
+
}
|
|
1205
|
+
const llmAvailable = adapter?.isAvailable() ?? resolveLlmProvider() !== null;
|
|
1206
|
+
if (mode === "llm" || mode === "hybrid") {
|
|
1207
|
+
return { mode, useLlm: llmAvailable };
|
|
1208
|
+
}
|
|
1209
|
+
return { mode: "rule", useLlm: false };
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
// src/observer/observer.ts
|
|
1213
|
+
import * as fs3 from "fs";
|
|
1214
|
+
import * as path3 from "path";
|
|
1215
|
+
|
|
1216
|
+
// src/observer/router.ts
|
|
1217
|
+
import * as fs2 from "fs";
|
|
1218
|
+
import * as path2 from "path";
|
|
701
1219
|
var CATEGORY_PATTERNS = [
|
|
702
1220
|
{
|
|
703
1221
|
category: "decisions",
|
|
@@ -764,17 +1282,41 @@ var FUTURE_TASK_HINT_RE = /\b(need to|should|todo|must|plan to)\b/i;
|
|
|
764
1282
|
var Router = class {
|
|
765
1283
|
vaultPath;
|
|
766
1284
|
extractTasks;
|
|
1285
|
+
extractFacts;
|
|
1286
|
+
factExtractionMode;
|
|
1287
|
+
llmAdapter;
|
|
1288
|
+
factStore;
|
|
767
1289
|
now;
|
|
768
1290
|
customRoutes;
|
|
769
1291
|
constructor(vaultPath, options = {}) {
|
|
770
|
-
this.vaultPath =
|
|
1292
|
+
this.vaultPath = path2.resolve(vaultPath);
|
|
771
1293
|
this.extractTasks = options.extractTasks ?? true;
|
|
1294
|
+
this.extractFacts = options.extractFacts ?? true;
|
|
1295
|
+
this.factExtractionMode = options.factExtractionMode ?? this.loadFactExtractionMode();
|
|
1296
|
+
this.llmAdapter = options.llmAdapter ?? createFactExtractionAdapter();
|
|
1297
|
+
this.factStore = new FactStore(this.vaultPath);
|
|
772
1298
|
this.now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
773
1299
|
this.customRoutes = this.loadCustomRoutes();
|
|
1300
|
+
if (this.extractFacts && this.factExtractionMode !== "off") {
|
|
1301
|
+
this.factStore.load();
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
loadFactExtractionMode() {
|
|
1305
|
+
try {
|
|
1306
|
+
const config = listConfig(this.vaultPath);
|
|
1307
|
+
const observer = config.observer;
|
|
1308
|
+
const mode = observer?.factExtractionMode;
|
|
1309
|
+
if (mode === "off" || mode === "rule" || mode === "llm" || mode === "hybrid") {
|
|
1310
|
+
return mode;
|
|
1311
|
+
}
|
|
1312
|
+
} catch {
|
|
1313
|
+
}
|
|
1314
|
+
return "llm";
|
|
774
1315
|
}
|
|
775
1316
|
/**
|
|
776
1317
|
* Takes observation markdown and routes items to appropriate vault categories.
|
|
777
1318
|
* Routes only items with importance >= 0.4.
|
|
1319
|
+
* Also extracts structured facts from observations when fact extraction is enabled.
|
|
778
1320
|
* Returns a summary of what was routed where.
|
|
779
1321
|
*/
|
|
780
1322
|
route(observationMarkdown, context = {}) {
|
|
@@ -784,6 +1326,7 @@ var Router = class {
|
|
|
784
1326
|
const knownWorkItems = this.extractTasks ? this.loadExistingWorkItems() : [];
|
|
785
1327
|
const knownProjectDefinitions = this.loadKnownProjectDefinitions();
|
|
786
1328
|
let dedupHits = 0;
|
|
1329
|
+
let factsExtracted = 0;
|
|
787
1330
|
for (const item of items) {
|
|
788
1331
|
if (item.importance < 0.4) continue;
|
|
789
1332
|
if (this.extractTasks && this.isTaskObservation(item.type)) {
|
|
@@ -810,8 +1353,81 @@ var Router = class {
|
|
|
810
1353
|
routed.push(routedItem);
|
|
811
1354
|
this.appendToCategory(category, routedItem, knownProjectDefinitions);
|
|
812
1355
|
}
|
|
813
|
-
|
|
814
|
-
|
|
1356
|
+
if (this.extractFacts && this.factExtractionMode !== "off") {
|
|
1357
|
+
const extractedCount = this.extractAndStoreFacts(observationMarkdown, context);
|
|
1358
|
+
factsExtracted = extractedCount;
|
|
1359
|
+
}
|
|
1360
|
+
const summary = this.buildSummary(routed, dedupHits, factsExtracted);
|
|
1361
|
+
return { routed, summary, factsExtracted };
|
|
1362
|
+
}
|
|
1363
|
+
/**
|
|
1364
|
+
* Extract facts from observation markdown and store them in the fact store.
|
|
1365
|
+
* Uses the configured extraction mode (rule-based, LLM, or hybrid).
|
|
1366
|
+
*/
|
|
1367
|
+
extractAndStoreFacts(observationMarkdown, context) {
|
|
1368
|
+
const { mode, useLlm } = resolveFactExtractionMode(this.factExtractionMode, this.llmAdapter);
|
|
1369
|
+
if (mode === "off") {
|
|
1370
|
+
return 0;
|
|
1371
|
+
}
|
|
1372
|
+
const source = context.source ?? "observer";
|
|
1373
|
+
const timestamp = context.timestamp?.toISOString() ?? this.now().toISOString();
|
|
1374
|
+
let facts = [];
|
|
1375
|
+
if (mode === "rule" || !useLlm) {
|
|
1376
|
+
facts = extractFactsRuleBased(observationMarkdown, source, timestamp);
|
|
1377
|
+
} else if (mode === "llm") {
|
|
1378
|
+
const llmFn = createLlmFunction(this.llmAdapter);
|
|
1379
|
+
extractFactsLlm(observationMarkdown, source, timestamp, llmFn).then((llmFacts) => {
|
|
1380
|
+
if (llmFacts.length > 0) {
|
|
1381
|
+
this.factStore.addFacts(llmFacts);
|
|
1382
|
+
this.factStore.save();
|
|
1383
|
+
}
|
|
1384
|
+
}).catch(() => {
|
|
1385
|
+
const ruleFacts = extractFactsRuleBased(observationMarkdown, source, timestamp);
|
|
1386
|
+
if (ruleFacts.length > 0) {
|
|
1387
|
+
this.factStore.addFacts(ruleFacts);
|
|
1388
|
+
this.factStore.save();
|
|
1389
|
+
}
|
|
1390
|
+
});
|
|
1391
|
+
return 0;
|
|
1392
|
+
} else if (mode === "hybrid") {
|
|
1393
|
+
facts = extractFactsRuleBased(observationMarkdown, source, timestamp);
|
|
1394
|
+
const llmFn = createLlmFunction(this.llmAdapter);
|
|
1395
|
+
extractFactsLlm(observationMarkdown, source, timestamp, llmFn).then((llmFacts) => {
|
|
1396
|
+
const merged = this.mergeFacts(facts, llmFacts);
|
|
1397
|
+
if (merged.length > facts.length) {
|
|
1398
|
+
this.factStore.addFacts(merged.slice(facts.length));
|
|
1399
|
+
this.factStore.save();
|
|
1400
|
+
}
|
|
1401
|
+
}).catch(() => {
|
|
1402
|
+
});
|
|
1403
|
+
}
|
|
1404
|
+
if (facts.length > 0) {
|
|
1405
|
+
this.factStore.addFacts(facts);
|
|
1406
|
+
this.factStore.save();
|
|
1407
|
+
}
|
|
1408
|
+
return facts.length;
|
|
1409
|
+
}
|
|
1410
|
+
/**
|
|
1411
|
+
* Merge facts from rule-based and LLM extraction, deduplicating by entity+relation.
|
|
1412
|
+
*/
|
|
1413
|
+
mergeFacts(ruleFacts, llmFacts) {
|
|
1414
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1415
|
+
const merged = [];
|
|
1416
|
+
for (const fact of ruleFacts) {
|
|
1417
|
+
const key = `${fact.entityNorm}::${fact.relation}`;
|
|
1418
|
+
if (!seen.has(key)) {
|
|
1419
|
+
seen.add(key);
|
|
1420
|
+
merged.push(fact);
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
for (const fact of llmFacts) {
|
|
1424
|
+
const key = `${fact.entityNorm}::${fact.relation}`;
|
|
1425
|
+
if (!seen.has(key)) {
|
|
1426
|
+
seen.add(key);
|
|
1427
|
+
merged.push(fact);
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1430
|
+
return merged;
|
|
815
1431
|
}
|
|
816
1432
|
isTaskObservation(type) {
|
|
817
1433
|
return type === "task" || type === "todo" || type === "commitment-unresolved";
|
|
@@ -1153,46 +1769,46 @@ var Router = class {
|
|
|
1153
1769
|
resolveFilePath(category, item, knownProjectDefinitions) {
|
|
1154
1770
|
const customEntityPath = this.resolveCustomEntityPath(item.content, category);
|
|
1155
1771
|
if (customEntityPath) {
|
|
1156
|
-
const customEntityDir =
|
|
1157
|
-
|
|
1772
|
+
const customEntityDir = path2.join(this.vaultPath, category, customEntityPath);
|
|
1773
|
+
fs2.mkdirSync(customEntityDir, { recursive: true });
|
|
1158
1774
|
return {
|
|
1159
|
-
filePath:
|
|
1775
|
+
filePath: path2.join(customEntityDir, `${item.date}.md`),
|
|
1160
1776
|
headerLabel: `${category}/${customEntityPath}`
|
|
1161
1777
|
};
|
|
1162
1778
|
}
|
|
1163
1779
|
if (category === "projects") {
|
|
1164
1780
|
const matchedProjectSlug = this.matchKnownProjectSlug(item.content, knownProjectDefinitions);
|
|
1165
1781
|
if (matchedProjectSlug) {
|
|
1166
|
-
const projectDir =
|
|
1167
|
-
|
|
1782
|
+
const projectDir = path2.join(this.vaultPath, category, matchedProjectSlug);
|
|
1783
|
+
fs2.mkdirSync(projectDir, { recursive: true });
|
|
1168
1784
|
return {
|
|
1169
|
-
filePath:
|
|
1785
|
+
filePath: path2.join(projectDir, `${item.date}.md`),
|
|
1170
1786
|
headerLabel: `${category}/${matchedProjectSlug}`
|
|
1171
1787
|
};
|
|
1172
1788
|
}
|
|
1173
1789
|
} else {
|
|
1174
1790
|
const entitySlug = this.extractEntitySlug(item.content, category);
|
|
1175
1791
|
if (entitySlug) {
|
|
1176
|
-
const entityDir =
|
|
1177
|
-
|
|
1792
|
+
const entityDir = path2.join(this.vaultPath, category, entitySlug);
|
|
1793
|
+
fs2.mkdirSync(entityDir, { recursive: true });
|
|
1178
1794
|
return {
|
|
1179
|
-
filePath:
|
|
1795
|
+
filePath: path2.join(entityDir, `${item.date}.md`),
|
|
1180
1796
|
headerLabel: `${category}/${entitySlug}`
|
|
1181
1797
|
};
|
|
1182
1798
|
}
|
|
1183
1799
|
}
|
|
1184
|
-
const categoryDir =
|
|
1185
|
-
|
|
1800
|
+
const categoryDir = path2.join(this.vaultPath, category);
|
|
1801
|
+
fs2.mkdirSync(categoryDir, { recursive: true });
|
|
1186
1802
|
return {
|
|
1187
|
-
filePath:
|
|
1803
|
+
filePath: path2.join(categoryDir, `${item.date}.md`),
|
|
1188
1804
|
headerLabel: category
|
|
1189
1805
|
};
|
|
1190
1806
|
}
|
|
1191
1807
|
appendToCategory(category, item, knownProjectDefinitions) {
|
|
1192
1808
|
const destination = this.resolveFilePath(category, item, knownProjectDefinitions);
|
|
1193
1809
|
const filePath = destination.filePath;
|
|
1194
|
-
|
|
1195
|
-
const existing =
|
|
1810
|
+
fs2.mkdirSync(path2.dirname(filePath), { recursive: true });
|
|
1811
|
+
const existing = fs2.existsSync(filePath) ? fs2.readFileSync(filePath, "utf-8").trim() : "";
|
|
1196
1812
|
const normalizedNew = this.normalizeForDedup(item.content);
|
|
1197
1813
|
const existingLines = existing.split(/\r?\n/);
|
|
1198
1814
|
for (const line of existingLines) {
|
|
@@ -1226,7 +1842,7 @@ ${entry}
|
|
|
1226
1842
|
` : `${header}
|
|
1227
1843
|
${entry}
|
|
1228
1844
|
`;
|
|
1229
|
-
|
|
1845
|
+
fs2.writeFileSync(filePath, newContent, "utf-8");
|
|
1230
1846
|
}
|
|
1231
1847
|
/**
|
|
1232
1848
|
* Auto-link proper nouns and known entities with [[wiki-links]].
|
|
@@ -1399,8 +2015,8 @@ ${entry}
|
|
|
1399
2015
|
for (const bg of setA) if (setB.has(bg)) intersection++;
|
|
1400
2016
|
return intersection / (setA.size + setB.size - intersection);
|
|
1401
2017
|
}
|
|
1402
|
-
buildSummary(routed, dedupHits) {
|
|
1403
|
-
if (routed.length === 0) {
|
|
2018
|
+
buildSummary(routed, dedupHits, factsExtracted = 0) {
|
|
2019
|
+
if (routed.length === 0 && factsExtracted === 0) {
|
|
1404
2020
|
if (dedupHits > 0) {
|
|
1405
2021
|
return `No items routed to vault categories (dedup hits: ${dedupHits}).`;
|
|
1406
2022
|
}
|
|
@@ -1411,7 +2027,17 @@ ${entry}
|
|
|
1411
2027
|
byCat.set(item.category, (byCat.get(item.category) ?? 0) + 1);
|
|
1412
2028
|
}
|
|
1413
2029
|
const parts = [...byCat.entries()].map(([cat, count]) => `${cat}: ${count}`);
|
|
1414
|
-
const
|
|
2030
|
+
const suffixParts = [];
|
|
2031
|
+
if (dedupHits > 0) {
|
|
2032
|
+
suffixParts.push(`dedup hits: ${dedupHits}`);
|
|
2033
|
+
}
|
|
2034
|
+
if (factsExtracted > 0) {
|
|
2035
|
+
suffixParts.push(`facts: ${factsExtracted}`);
|
|
2036
|
+
}
|
|
2037
|
+
const suffix = suffixParts.length > 0 ? ` (${suffixParts.join(", ")})` : "";
|
|
2038
|
+
if (routed.length === 0 && factsExtracted > 0) {
|
|
2039
|
+
return `Extracted ${factsExtracted} facts${suffix}`;
|
|
2040
|
+
}
|
|
1415
2041
|
return `Routed ${routed.length} observations \u2192 ${parts.join(", ")}${suffix}`;
|
|
1416
2042
|
}
|
|
1417
2043
|
};
|
|
@@ -1479,7 +2105,7 @@ var Observer = class {
|
|
|
1479
2105
|
observationsCache = "";
|
|
1480
2106
|
lastRoutingSummary = "";
|
|
1481
2107
|
constructor(vaultPath, options = {}) {
|
|
1482
|
-
this.vaultPath =
|
|
2108
|
+
this.vaultPath = path3.resolve(vaultPath);
|
|
1483
2109
|
this.tokenThreshold = options.tokenThreshold ?? 3e4;
|
|
1484
2110
|
this.reflectThreshold = options.reflectThreshold ?? 4e4;
|
|
1485
2111
|
this.now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
@@ -1583,14 +2209,14 @@ var Observer = class {
|
|
|
1583
2209
|
return this.readObservationFile(getLegacyObservationPath(this.vaultPath, toDateKey(date)));
|
|
1584
2210
|
}
|
|
1585
2211
|
readObservationFile(filePath) {
|
|
1586
|
-
if (!
|
|
2212
|
+
if (!fs3.existsSync(filePath)) {
|
|
1587
2213
|
return "";
|
|
1588
2214
|
}
|
|
1589
|
-
return
|
|
2215
|
+
return fs3.readFileSync(filePath, "utf-8").trim();
|
|
1590
2216
|
}
|
|
1591
2217
|
writeObservationFile(filePath, content) {
|
|
1592
2218
|
ensureParentDir(filePath);
|
|
1593
|
-
|
|
2219
|
+
fs3.writeFileSync(filePath, `${content.trim()}
|
|
1594
2220
|
`, "utf-8");
|
|
1595
2221
|
}
|
|
1596
2222
|
deduplicateObservationMarkdown(markdown) {
|
|
@@ -1638,7 +2264,7 @@ var Observer = class {
|
|
|
1638
2264
|
transcriptId: options.transcriptId ?? null,
|
|
1639
2265
|
message
|
|
1640
2266
|
}));
|
|
1641
|
-
|
|
2267
|
+
fs3.appendFileSync(rawPath, `${records.join("\n")}
|
|
1642
2268
|
`, "utf-8");
|
|
1643
2269
|
}
|
|
1644
2270
|
sanitizeSource(source) {
|
|
@@ -1661,5 +2287,15 @@ var Observer = class {
|
|
|
1661
2287
|
export {
|
|
1662
2288
|
Compressor,
|
|
1663
2289
|
Reflector,
|
|
2290
|
+
normalizeEntity,
|
|
2291
|
+
factId,
|
|
2292
|
+
extractFactsRuleBased,
|
|
2293
|
+
extractFactsLlm,
|
|
2294
|
+
FactStore,
|
|
2295
|
+
createGeminiFlashAdapter,
|
|
2296
|
+
createDefaultAdapter,
|
|
2297
|
+
createFactExtractionAdapter,
|
|
2298
|
+
createLlmFunction,
|
|
2299
|
+
resolveFactExtractionMode,
|
|
1664
2300
|
Observer
|
|
1665
2301
|
};
|