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.
- package/README.md +351 -21
- 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-2GKPENIR.js +66 -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-ZZA73MFY.js → chunk-33DOSHTA.js} +176 -36
- package/dist/chunk-4BQTQMJP.js +93 -0
- package/dist/{chunk-MAKNAHAW.js → chunk-5PJ4STIC.js} +98 -8
- package/dist/{chunk-RVYA52PY.js → chunk-5UM4PMMM.js} +1 -1
- package/dist/{chunk-4VQTUVH7.js → chunk-7YZWHM36.js} +52 -26
- package/dist/{chunk-4VRIMU4O.js → chunk-A4EAUO7T.js} +5 -5
- package/dist/{chunk-R6SXNSFD.js → chunk-BV5KWZKR.js} +3 -3
- package/dist/chunk-FBITHIZF.js +351 -0
- package/dist/{chunk-Q2J5YTUF.js → chunk-FUSLEY6L.js} +751 -34
- package/dist/chunk-GNJL4YGR.js +79 -0
- package/dist/{chunk-42MXU7A6.js → chunk-K4GFGKFD.js} +51 -47
- package/dist/{chunk-PBEE567J.js → chunk-KSZROBFH.js} +2 -2
- package/dist/chunk-L4HSSQ6T.js +152 -0
- package/dist/{chunk-PZ2AUU2W.js → chunk-LMKQ7NIF.js} +206 -37
- package/dist/{chunk-6546Q4OR.js → chunk-M5O6FQ66.js} +6 -6
- package/dist/chunk-MM6QGW3P.js +207 -0
- package/dist/{chunk-T76H47ZS.js → chunk-MNPUYCHQ.js} +1 -1
- package/dist/{chunk-P5EPF6MB.js → chunk-MW5C6ZQA.js} +110 -13
- package/dist/{chunk-OZ7RIXTO.js → chunk-QSRRMEYM.js} +2 -2
- 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-ME37YNW3.js → chunk-SV7T4HRE.js} +4 -4
- package/dist/{chunk-IEVLHNLU.js → chunk-T3FKSZSN.js} +3 -3
- package/dist/{chunk-DTEHFAL7.js → chunk-TS6NDVOU.js} +2 -2
- package/dist/chunk-U4O6C46S.js +154 -0
- package/dist/{chunk-ITPEXLHA.js → chunk-URXDAUVH.js} +24 -5
- 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-RCBMXTWS.js → chunk-YD7SVXTF.js} +39 -7
- package/dist/{chunk-HIHOUSXS.js → chunk-YXQCA6B7.js} +105 -1
- package/dist/cli/index.js +20 -18
- 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 +2 -1
- package/dist/commands/checkpoint.js +1 -0
- package/dist/commands/compat.js +2 -1
- package/dist/commands/context.js +6 -4
- package/dist/commands/doctor.d.ts +10 -1
- package/dist/commands/doctor.js +13 -10
- package/dist/commands/embed.js +5 -3
- package/dist/commands/entities.js +2 -1
- package/dist/commands/graph.js +4 -3
- package/dist/commands/inject.d.ts +1 -1
- package/dist/commands/inject.js +5 -4
- package/dist/commands/kanban.js +1 -0
- package/dist/commands/link.js +5 -4
- package/dist/commands/migrate-observations.js +3 -2
- package/dist/commands/observe.js +9 -7
- 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 +10 -8
- package/dist/commands/status.js +13 -82
- 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 +5 -3
- package/dist/index.d.ts +254 -10
- package/dist/index.js +288 -155
- 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 +2 -1
- 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/handler.js +714 -2
- package/hooks/clawvault/handler.test.js +153 -0
- package/hooks/clawvault/openclaw.plugin.json +72 -0
- package/openclaw.plugin.json +14 -2
- package/package.json +5 -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;
|
|
@@ -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 =
|
|
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/
|
|
695
|
-
|
|
696
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
814
|
-
|
|
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 =
|
|
1157
|
-
|
|
1853
|
+
const customEntityDir = path2.join(this.vaultPath, category, customEntityPath);
|
|
1854
|
+
fs2.mkdirSync(customEntityDir, { recursive: true });
|
|
1158
1855
|
return {
|
|
1159
|
-
filePath:
|
|
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 =
|
|
1167
|
-
|
|
1863
|
+
const projectDir = path2.join(this.vaultPath, category, matchedProjectSlug);
|
|
1864
|
+
fs2.mkdirSync(projectDir, { recursive: true });
|
|
1168
1865
|
return {
|
|
1169
|
-
filePath:
|
|
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 =
|
|
1177
|
-
|
|
1873
|
+
const entityDir = path2.join(this.vaultPath, category, entitySlug);
|
|
1874
|
+
fs2.mkdirSync(entityDir, { recursive: true });
|
|
1178
1875
|
return {
|
|
1179
|
-
filePath:
|
|
1876
|
+
filePath: path2.join(entityDir, `${item.date}.md`),
|
|
1180
1877
|
headerLabel: `${category}/${entitySlug}`
|
|
1181
1878
|
};
|
|
1182
1879
|
}
|
|
1183
1880
|
}
|
|
1184
|
-
const categoryDir =
|
|
1185
|
-
|
|
1881
|
+
const categoryDir = path2.join(this.vaultPath, category);
|
|
1882
|
+
fs2.mkdirSync(categoryDir, { recursive: true });
|
|
1186
1883
|
return {
|
|
1187
|
-
filePath:
|
|
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
|
-
|
|
1195
|
-
const existing =
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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 (!
|
|
2293
|
+
if (!fs3.existsSync(filePath)) {
|
|
1587
2294
|
return "";
|
|
1588
2295
|
}
|
|
1589
|
-
return
|
|
2296
|
+
return fs3.readFileSync(filePath, "utf-8").trim();
|
|
1590
2297
|
}
|
|
1591
2298
|
writeObservationFile(filePath, content) {
|
|
1592
2299
|
ensureParentDir(filePath);
|
|
1593
|
-
|
|
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
|
-
|
|
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
|
};
|