@mandays/obsidian-memory-mcp 0.1.0
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/LICENSE +21 -0
- package/README.md +206 -0
- package/dist/chunk-5U2LXK3W.js +2655 -0
- package/dist/chunk-5U2LXK3W.js.map +1 -0
- package/dist/cli.js +109 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +538 -0
- package/dist/index.js +55 -0
- package/dist/index.js.map +1 -0
- package/package.json +64 -0
|
@@ -0,0 +1,2655 @@
|
|
|
1
|
+
// src/core/vault.ts
|
|
2
|
+
import * as fs from "fs/promises";
|
|
3
|
+
import * as path from "path";
|
|
4
|
+
import * as yaml from "js-yaml";
|
|
5
|
+
var Vault = class {
|
|
6
|
+
paths;
|
|
7
|
+
config;
|
|
8
|
+
constructor(config) {
|
|
9
|
+
this.config = config;
|
|
10
|
+
const root = path.resolve(config.vaultPath);
|
|
11
|
+
const memory = path.join(root, "memory");
|
|
12
|
+
this.paths = {
|
|
13
|
+
root,
|
|
14
|
+
memory,
|
|
15
|
+
facts: path.join(memory, "facts"),
|
|
16
|
+
events: path.join(memory, "events"),
|
|
17
|
+
schema: path.join(memory, "schema"),
|
|
18
|
+
views: path.join(memory, "_views"),
|
|
19
|
+
inbox: path.join(memory, "_inbox"),
|
|
20
|
+
claims: path.join(memory, "_claims"),
|
|
21
|
+
ops: path.join(memory, "_ops"),
|
|
22
|
+
opsApplied: path.join(memory, "_ops", "applied"),
|
|
23
|
+
archive: path.join(memory, "_archive"),
|
|
24
|
+
entities: path.join(memory, "entities.md"),
|
|
25
|
+
predicates: path.join(memory, "schema", "predicates.yaml"),
|
|
26
|
+
versionYaml: path.join(memory, "schema", "version.yaml"),
|
|
27
|
+
sources: path.join(root, "sources"),
|
|
28
|
+
people: path.join(memory, "people"),
|
|
29
|
+
projects: path.join(memory, "projects"),
|
|
30
|
+
decisions: path.join(memory, "decisions"),
|
|
31
|
+
insights: path.join(memory, "insights"),
|
|
32
|
+
context: path.join(memory, "context")
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Assert that the vault path exists and contains a valid v3 version marker.
|
|
37
|
+
*/
|
|
38
|
+
async assertValid() {
|
|
39
|
+
try {
|
|
40
|
+
await fs.access(this.paths.root);
|
|
41
|
+
} catch {
|
|
42
|
+
throw new VaultError(
|
|
43
|
+
`Vault path does not exist: ${this.paths.root}`,
|
|
44
|
+
"VAULT_NOT_FOUND"
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
try {
|
|
48
|
+
await fs.access(this.paths.memory);
|
|
49
|
+
} catch {
|
|
50
|
+
throw new VaultError(
|
|
51
|
+
`No memory/ directory found in vault: ${this.paths.root}`,
|
|
52
|
+
"VAULT_INVALID"
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
try {
|
|
56
|
+
const versionContent = await fs.readFile(
|
|
57
|
+
this.paths.versionYaml,
|
|
58
|
+
"utf-8"
|
|
59
|
+
);
|
|
60
|
+
const version = yaml.load(versionContent);
|
|
61
|
+
if (version?.spec_version !== "3.0") {
|
|
62
|
+
throw new VaultError(
|
|
63
|
+
`Expected spec_version: "3.0" in ${this.paths.versionYaml}, got: ${version?.spec_version}`,
|
|
64
|
+
"VAULT_VERSION_MISMATCH"
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
} catch (err) {
|
|
68
|
+
if (err instanceof VaultError) throw err;
|
|
69
|
+
throw new VaultError(
|
|
70
|
+
`Cannot read version marker: ${this.paths.versionYaml}`,
|
|
71
|
+
"VAULT_INVALID"
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Get the relative path from vault root.
|
|
77
|
+
*/
|
|
78
|
+
relativePath(absolutePath) {
|
|
79
|
+
return path.relative(this.paths.root, absolutePath);
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Resolve a relative path to absolute from vault root.
|
|
83
|
+
*/
|
|
84
|
+
resolvePath(relativePath) {
|
|
85
|
+
return path.resolve(this.paths.root, relativePath);
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Get the fact file path for a given entity and predicate.
|
|
89
|
+
*/
|
|
90
|
+
factPath(entity, predicate, suffix) {
|
|
91
|
+
const filename = suffix ? `${predicate}--${suffix}.md` : `${predicate}.md`;
|
|
92
|
+
return path.join(this.paths.facts, entity, filename);
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Get the event directory path for a given date.
|
|
96
|
+
*/
|
|
97
|
+
eventDir(date) {
|
|
98
|
+
return path.join(this.paths.events, date);
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Get the inbox ops path for a given agent and operation.
|
|
102
|
+
*/
|
|
103
|
+
inboxOpPath(agentId, operationId) {
|
|
104
|
+
return path.join(this.paths.inbox, agentId, "ops", `${operationId}.md`);
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Get the applied operation receipt path.
|
|
108
|
+
*/
|
|
109
|
+
appliedOpPath(operationId) {
|
|
110
|
+
return path.join(this.paths.opsApplied, `${operationId}.md`);
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Get the claim path for a target ID.
|
|
114
|
+
*/
|
|
115
|
+
claimPath(targetId) {
|
|
116
|
+
return path.join(this.paths.claims, `${targetId}.yaml`);
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Get the archive path for a fact.
|
|
120
|
+
*/
|
|
121
|
+
archivePath(year, entity, predicate) {
|
|
122
|
+
return path.join(this.paths.archive, year, "facts", entity, `${predicate}.md`);
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Get the view path for a named view.
|
|
126
|
+
*/
|
|
127
|
+
viewPath(viewName) {
|
|
128
|
+
return path.join(this.paths.views, `${viewName}.md`);
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Get entity-specific view path.
|
|
132
|
+
*/
|
|
133
|
+
entityViewPath(entity) {
|
|
134
|
+
return path.join(this.paths.views, "by-entity", `${entity}.md`);
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
var VaultError = class extends Error {
|
|
138
|
+
code;
|
|
139
|
+
constructor(message, code) {
|
|
140
|
+
super(message);
|
|
141
|
+
this.name = "VaultError";
|
|
142
|
+
this.code = code;
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
// src/core/markdown.ts
|
|
147
|
+
import matter from "gray-matter";
|
|
148
|
+
import * as fs2 from "fs/promises";
|
|
149
|
+
import * as path2 from "path";
|
|
150
|
+
function normalizeDates(obj) {
|
|
151
|
+
if (obj instanceof Date) {
|
|
152
|
+
return obj.toISOString();
|
|
153
|
+
}
|
|
154
|
+
if (Array.isArray(obj)) {
|
|
155
|
+
return obj.map(normalizeDates);
|
|
156
|
+
}
|
|
157
|
+
if (obj !== null && typeof obj === "object") {
|
|
158
|
+
const result = {};
|
|
159
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
160
|
+
result[key] = normalizeDates(value);
|
|
161
|
+
}
|
|
162
|
+
return result;
|
|
163
|
+
}
|
|
164
|
+
return obj;
|
|
165
|
+
}
|
|
166
|
+
async function parseMarkdownFile(filePath) {
|
|
167
|
+
const raw = await fs2.readFile(filePath, "utf-8");
|
|
168
|
+
const { data, content } = matter(raw);
|
|
169
|
+
const normalized = normalizeDates(data);
|
|
170
|
+
return { data: normalized, content, path: filePath };
|
|
171
|
+
}
|
|
172
|
+
function serializeMarkdown(data, body) {
|
|
173
|
+
const content = body ?? "";
|
|
174
|
+
return matter.stringify(content, data);
|
|
175
|
+
}
|
|
176
|
+
async function writeMarkdownFile(filePath, data, body) {
|
|
177
|
+
const dir = path2.dirname(filePath);
|
|
178
|
+
await fs2.mkdir(dir, { recursive: true });
|
|
179
|
+
const content = serializeMarkdown(data, body);
|
|
180
|
+
const tmpPath = `${filePath}.tmp`;
|
|
181
|
+
await fs2.writeFile(tmpPath, content, "utf-8");
|
|
182
|
+
await fs2.rename(tmpPath, filePath);
|
|
183
|
+
}
|
|
184
|
+
async function readFrontmatter(filePath) {
|
|
185
|
+
const raw = await fs2.readFile(filePath, "utf-8");
|
|
186
|
+
const { data } = matter(raw);
|
|
187
|
+
return data;
|
|
188
|
+
}
|
|
189
|
+
async function fileExists(filePath) {
|
|
190
|
+
try {
|
|
191
|
+
await fs2.access(filePath);
|
|
192
|
+
return true;
|
|
193
|
+
} catch {
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// src/core/entity-registry.ts
|
|
199
|
+
import * as fs3 from "fs/promises";
|
|
200
|
+
import matter2 from "gray-matter";
|
|
201
|
+
var EntityRegistry = class {
|
|
202
|
+
cache = null;
|
|
203
|
+
vault;
|
|
204
|
+
constructor(vault) {
|
|
205
|
+
this.vault = vault;
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Load all entities from entities.md. Uses cache if available.
|
|
209
|
+
*/
|
|
210
|
+
async getAll() {
|
|
211
|
+
if (this.cache) return this.cache;
|
|
212
|
+
const raw = await fs3.readFile(this.vault.paths.entities, "utf-8");
|
|
213
|
+
const { data } = matter2(raw);
|
|
214
|
+
const index = data;
|
|
215
|
+
this.cache = index.entities ?? [];
|
|
216
|
+
return this.cache;
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Get entities filtered by kind.
|
|
220
|
+
*/
|
|
221
|
+
async getByKind(kind) {
|
|
222
|
+
const all = await this.getAll();
|
|
223
|
+
return all.filter((e) => e.kind === kind);
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Find a single entity by ID.
|
|
227
|
+
*/
|
|
228
|
+
async findById(id) {
|
|
229
|
+
const all = await this.getAll();
|
|
230
|
+
return all.find((e) => e.id === id);
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Check if an entity exists.
|
|
234
|
+
*/
|
|
235
|
+
async exists(id) {
|
|
236
|
+
const entity = await this.findById(id);
|
|
237
|
+
return entity !== void 0;
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Add a new entity. Returns the updated list.
|
|
241
|
+
*/
|
|
242
|
+
async add(entity) {
|
|
243
|
+
const all = await this.getAll();
|
|
244
|
+
if (all.some((e) => e.id === entity.id)) {
|
|
245
|
+
throw new Error(`Entity already exists: ${entity.id}`);
|
|
246
|
+
}
|
|
247
|
+
all.push(entity);
|
|
248
|
+
await this.save(all);
|
|
249
|
+
return all;
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Invalidate the in-memory cache.
|
|
253
|
+
*/
|
|
254
|
+
invalidate() {
|
|
255
|
+
this.cache = null;
|
|
256
|
+
}
|
|
257
|
+
async save(entities) {
|
|
258
|
+
const raw = await fs3.readFile(this.vault.paths.entities, "utf-8");
|
|
259
|
+
const { content } = matter2(raw);
|
|
260
|
+
const data = {
|
|
261
|
+
type: "entity-index",
|
|
262
|
+
entities
|
|
263
|
+
};
|
|
264
|
+
await writeMarkdownFile(
|
|
265
|
+
this.vault.paths.entities,
|
|
266
|
+
data,
|
|
267
|
+
content
|
|
268
|
+
);
|
|
269
|
+
this.cache = entities;
|
|
270
|
+
}
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
// src/core/predicate-registry.ts
|
|
274
|
+
import * as fs4 from "fs/promises";
|
|
275
|
+
import * as yaml2 from "js-yaml";
|
|
276
|
+
var PredicateRegistry = class {
|
|
277
|
+
cache = null;
|
|
278
|
+
vault;
|
|
279
|
+
constructor(vault) {
|
|
280
|
+
this.vault = vault;
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Load all predicates from predicates.yaml.
|
|
284
|
+
* Supports both formats:
|
|
285
|
+
* - Flat array: ["role", "base", ...]
|
|
286
|
+
* - Object with entries: { predicates: [{id: "role", description: "..."}] }
|
|
287
|
+
*/
|
|
288
|
+
async getAll() {
|
|
289
|
+
if (this.cache) return this.cache;
|
|
290
|
+
const raw = await fs4.readFile(this.vault.paths.predicates, "utf-8");
|
|
291
|
+
const parsed = yaml2.load(raw);
|
|
292
|
+
if (Array.isArray(parsed)) {
|
|
293
|
+
this.cache = parsed;
|
|
294
|
+
} else if (parsed && typeof parsed === "object" && "predicates" in parsed) {
|
|
295
|
+
const entries = parsed.predicates;
|
|
296
|
+
this.cache = entries.map((e) => typeof e === "string" ? e : e.id);
|
|
297
|
+
} else {
|
|
298
|
+
this.cache = [];
|
|
299
|
+
}
|
|
300
|
+
return this.cache;
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* Check if a predicate exists.
|
|
304
|
+
*/
|
|
305
|
+
async exists(predicate) {
|
|
306
|
+
const all = await this.getAll();
|
|
307
|
+
return all.includes(predicate);
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Add a new predicate. Returns the updated list.
|
|
311
|
+
*/
|
|
312
|
+
async add(predicate) {
|
|
313
|
+
const all = await this.getAll();
|
|
314
|
+
if (all.includes(predicate)) {
|
|
315
|
+
throw new Error(`Predicate already exists: ${predicate}`);
|
|
316
|
+
}
|
|
317
|
+
all.push(predicate);
|
|
318
|
+
all.sort();
|
|
319
|
+
await this.save(all);
|
|
320
|
+
return all;
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* Invalidate the in-memory cache.
|
|
324
|
+
*/
|
|
325
|
+
invalidate() {
|
|
326
|
+
this.cache = null;
|
|
327
|
+
}
|
|
328
|
+
async save(predicates) {
|
|
329
|
+
const raw = await fs4.readFile(this.vault.paths.predicates, "utf-8");
|
|
330
|
+
const parsed = yaml2.load(raw);
|
|
331
|
+
let content;
|
|
332
|
+
if (parsed && typeof parsed === "object" && "predicates" in parsed) {
|
|
333
|
+
const existing = parsed.predicates;
|
|
334
|
+
const existingMap = new Map(existing.map((e) => [typeof e === "string" ? e : e.id, e]));
|
|
335
|
+
const entries = predicates.map((id) => {
|
|
336
|
+
const existing2 = existingMap.get(id);
|
|
337
|
+
if (existing2 && typeof existing2 === "object") return existing2;
|
|
338
|
+
return { id };
|
|
339
|
+
});
|
|
340
|
+
content = yaml2.dump({ predicates: entries }, { flowLevel: -1 });
|
|
341
|
+
} else {
|
|
342
|
+
content = yaml2.dump(predicates, { flowLevel: -1 });
|
|
343
|
+
}
|
|
344
|
+
const tmpPath = `${this.vault.paths.predicates}.tmp`;
|
|
345
|
+
await fs4.writeFile(tmpPath, content, "utf-8");
|
|
346
|
+
await fs4.rename(tmpPath, this.vault.paths.predicates);
|
|
347
|
+
this.cache = predicates;
|
|
348
|
+
}
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
// src/core/query-engine.ts
|
|
352
|
+
import * as fs5 from "fs/promises";
|
|
353
|
+
import * as path3 from "path";
|
|
354
|
+
import { glob } from "glob";
|
|
355
|
+
import { parseISO, isValid, isBefore, isAfter } from "date-fns";
|
|
356
|
+
var QueryEngine = class {
|
|
357
|
+
vault;
|
|
358
|
+
constructor(vault) {
|
|
359
|
+
this.vault = vault;
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* Find facts matching the given filters.
|
|
363
|
+
*/
|
|
364
|
+
async findFacts(filter) {
|
|
365
|
+
if (filter.entity && filter.predicate && !filter.id) {
|
|
366
|
+
const directPath = this.vault.factPath(filter.entity, filter.predicate);
|
|
367
|
+
try {
|
|
368
|
+
await fs5.access(directPath);
|
|
369
|
+
const parsed = await parseMarkdownFile(directPath);
|
|
370
|
+
const fact = parsed.data;
|
|
371
|
+
if (this.matchesFactFilter(fact, filter)) {
|
|
372
|
+
return [{ data: fact, content: parsed.content, path: directPath }];
|
|
373
|
+
}
|
|
374
|
+
} catch {
|
|
375
|
+
}
|
|
376
|
+
const pattern2 = path3.join(
|
|
377
|
+
this.vault.paths.facts,
|
|
378
|
+
filter.entity,
|
|
379
|
+
`${filter.predicate}*.md`
|
|
380
|
+
);
|
|
381
|
+
const files2 = await glob(pattern2);
|
|
382
|
+
const results2 = [];
|
|
383
|
+
for (const file of files2) {
|
|
384
|
+
const parsed = await parseMarkdownFile(file);
|
|
385
|
+
const fact = parsed.data;
|
|
386
|
+
if (this.matchesFactFilter(fact, filter)) {
|
|
387
|
+
results2.push({ data: fact, content: parsed.content, path: file });
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
return this.applyLimit(results2, filter.limit);
|
|
391
|
+
}
|
|
392
|
+
if (filter.id) {
|
|
393
|
+
const result = await this.findFactById(filter.id);
|
|
394
|
+
return result ? [result] : [];
|
|
395
|
+
}
|
|
396
|
+
const pattern = filter.entity ? path3.join(this.vault.paths.facts, filter.entity, "*.md") : path3.join(this.vault.paths.facts, "**", "*.md");
|
|
397
|
+
const files = await glob(pattern);
|
|
398
|
+
const results = [];
|
|
399
|
+
for (const file of files) {
|
|
400
|
+
const parsed = await parseMarkdownFile(file);
|
|
401
|
+
const fact = parsed.data;
|
|
402
|
+
if (fact.type !== "fact") continue;
|
|
403
|
+
if (this.matchesFactFilter(fact, filter)) {
|
|
404
|
+
results.push({ data: fact, content: parsed.content, path: file });
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
return this.applyLimit(results, filter.limit);
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* Find a fact by its stable ID.
|
|
411
|
+
*/
|
|
412
|
+
async findFactById(id) {
|
|
413
|
+
const files = await glob(path3.join(this.vault.paths.facts, "**", "*.md"));
|
|
414
|
+
for (const file of files) {
|
|
415
|
+
const parsed = await parseMarkdownFile(file);
|
|
416
|
+
const fact = parsed.data;
|
|
417
|
+
if (fact.id === id) {
|
|
418
|
+
return { data: fact, content: parsed.content, path: file };
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
return null;
|
|
422
|
+
}
|
|
423
|
+
/**
|
|
424
|
+
* Find events matching the given filters.
|
|
425
|
+
*/
|
|
426
|
+
async findEvents(filter) {
|
|
427
|
+
const pattern = path3.join(this.vault.paths.events, "**", "*.md");
|
|
428
|
+
const files = await glob(pattern);
|
|
429
|
+
const results = [];
|
|
430
|
+
for (const file of files) {
|
|
431
|
+
const parsed = await parseMarkdownFile(file);
|
|
432
|
+
const event = parsed.data;
|
|
433
|
+
if (event.type !== "event") continue;
|
|
434
|
+
if (this.matchesEventFilter(event, filter)) {
|
|
435
|
+
results.push({ data: event, content: parsed.content, path: file });
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
results.sort((a, b) => {
|
|
439
|
+
const dateA = String(a.data.occurred_at);
|
|
440
|
+
const dateB = String(b.data.occurred_at);
|
|
441
|
+
return dateB.localeCompare(dateA);
|
|
442
|
+
});
|
|
443
|
+
return this.applyLimit(results, filter.limit);
|
|
444
|
+
}
|
|
445
|
+
/**
|
|
446
|
+
* Find any record (fact, event, operation) by its stable ID.
|
|
447
|
+
*/
|
|
448
|
+
async findById(id) {
|
|
449
|
+
const fact = await this.findFactById(id);
|
|
450
|
+
if (fact) return { type: "fact", path: fact.path, data: fact.data };
|
|
451
|
+
const eventFiles = await glob(path3.join(this.vault.paths.events, "**", "*.md"));
|
|
452
|
+
for (const file of eventFiles) {
|
|
453
|
+
const parsed = await parseMarkdownFile(file);
|
|
454
|
+
if (parsed.data.id === id) {
|
|
455
|
+
return { type: "event", path: file, data: parsed.data };
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
const inboxFiles = await glob(path3.join(this.vault.paths.inbox, "**", "*.md"));
|
|
459
|
+
for (const file of inboxFiles) {
|
|
460
|
+
const parsed = await parseMarkdownFile(file);
|
|
461
|
+
const data = parsed.data;
|
|
462
|
+
if (data.operation_id === id || data.id === id) {
|
|
463
|
+
return { type: "operation", path: file, data };
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
const appliedFiles = await glob(path3.join(this.vault.paths.opsApplied, "*.md"));
|
|
467
|
+
for (const file of appliedFiles) {
|
|
468
|
+
const parsed = await parseMarkdownFile(file);
|
|
469
|
+
const data = parsed.data;
|
|
470
|
+
if (data.operation_id === id || data.id === id) {
|
|
471
|
+
return { type: "operation", path: file, data };
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
return null;
|
|
475
|
+
}
|
|
476
|
+
matchesFactFilter(fact, filter) {
|
|
477
|
+
if (filter.entity && fact.entity !== filter.entity) return false;
|
|
478
|
+
if (filter.predicate && fact.predicate !== filter.predicate) return false;
|
|
479
|
+
if (filter.confidence && fact.confidence !== filter.confidence) return false;
|
|
480
|
+
if (filter.tags && filter.tags.length > 0) {
|
|
481
|
+
if (!fact.tags || !filter.tags.some((t) => fact.tags.includes(t))) {
|
|
482
|
+
return false;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
if (filter.query) {
|
|
486
|
+
const q = filter.query.toLowerCase();
|
|
487
|
+
const inValue = String(fact.value).toLowerCase().includes(q);
|
|
488
|
+
const inEntity = fact.entity.toLowerCase().includes(q);
|
|
489
|
+
const inPredicate = fact.predicate.toLowerCase().includes(q);
|
|
490
|
+
if (!inValue && !inEntity && !inPredicate) return false;
|
|
491
|
+
}
|
|
492
|
+
if (filter.onDate) {
|
|
493
|
+
if (!this.isTemporallyValid(fact, filter.onDate)) return false;
|
|
494
|
+
}
|
|
495
|
+
return true;
|
|
496
|
+
}
|
|
497
|
+
matchesEventFilter(event, filter) {
|
|
498
|
+
if (filter.entity) {
|
|
499
|
+
if (!event.entities || !event.entities.includes(filter.entity)) {
|
|
500
|
+
return false;
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
if (filter.kind && event.kind !== filter.kind) return false;
|
|
504
|
+
if (filter.since) {
|
|
505
|
+
const since = parseISO(filter.since);
|
|
506
|
+
const occurred = parseISO(String(event.occurred_at));
|
|
507
|
+
if (isValid(since) && isValid(occurred) && isBefore(occurred, since)) {
|
|
508
|
+
return false;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
if (filter.until) {
|
|
512
|
+
const until = parseISO(filter.until);
|
|
513
|
+
const occurred = parseISO(String(event.occurred_at));
|
|
514
|
+
if (isValid(until) && isValid(occurred) && isAfter(occurred, until)) {
|
|
515
|
+
return false;
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
return true;
|
|
519
|
+
}
|
|
520
|
+
/**
|
|
521
|
+
* Check if a fact is temporally valid on a given date.
|
|
522
|
+
* valid_from <= date <= valid_to (with null meaning unbounded)
|
|
523
|
+
*/
|
|
524
|
+
isTemporallyValid(fact, dateStr) {
|
|
525
|
+
const date = parseISO(dateStr);
|
|
526
|
+
if (!isValid(date)) return true;
|
|
527
|
+
if (fact.valid_from) {
|
|
528
|
+
const from = parseISO(fact.valid_from);
|
|
529
|
+
if (isValid(from) && isBefore(date, from)) return false;
|
|
530
|
+
}
|
|
531
|
+
if (fact.valid_to) {
|
|
532
|
+
const to = parseISO(fact.valid_to);
|
|
533
|
+
if (isValid(to) && isAfter(date, to)) return false;
|
|
534
|
+
}
|
|
535
|
+
return true;
|
|
536
|
+
}
|
|
537
|
+
applyLimit(results, limit) {
|
|
538
|
+
if (limit && limit > 0) {
|
|
539
|
+
return results.slice(0, limit);
|
|
540
|
+
}
|
|
541
|
+
return results;
|
|
542
|
+
}
|
|
543
|
+
};
|
|
544
|
+
|
|
545
|
+
// src/core/hash.ts
|
|
546
|
+
import * as crypto from "crypto";
|
|
547
|
+
import * as fs6 from "fs/promises";
|
|
548
|
+
async function computeFileHash(filePath) {
|
|
549
|
+
const content = await fs6.readFile(filePath);
|
|
550
|
+
const hash = crypto.createHash("sha256").update(content).digest("hex");
|
|
551
|
+
return `sha256:${hash}`;
|
|
552
|
+
}
|
|
553
|
+
function computeStringHash(content) {
|
|
554
|
+
const hash = crypto.createHash("sha256").update(content, "utf-8").digest("hex");
|
|
555
|
+
return `sha256:${hash}`;
|
|
556
|
+
}
|
|
557
|
+
function isValidHash(hash) {
|
|
558
|
+
return /^sha256:[0-9a-f]{64}$/.test(hash);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// src/core/id-generator.ts
|
|
562
|
+
import * as crypto2 from "crypto";
|
|
563
|
+
function generateOperationId() {
|
|
564
|
+
const now = /* @__PURE__ */ new Date();
|
|
565
|
+
const timestamp = now.toISOString().replace(/[-:]/g, "").replace(/\.\d+Z$/, "z").toLowerCase();
|
|
566
|
+
const hex = crypto2.randomBytes(4).toString("hex").slice(0, 4);
|
|
567
|
+
return `op-${timestamp}-${hex}`;
|
|
568
|
+
}
|
|
569
|
+
function generateAgentId(name) {
|
|
570
|
+
const slug = (name ?? "mcp").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 20);
|
|
571
|
+
const hex = crypto2.randomBytes(4).toString("hex");
|
|
572
|
+
return `agent-${slug}-${hex}`;
|
|
573
|
+
}
|
|
574
|
+
function generateFactId(entity, predicate) {
|
|
575
|
+
return `fact-${entity}-${predicate}`;
|
|
576
|
+
}
|
|
577
|
+
function generateEventId(date, slug) {
|
|
578
|
+
return `event-${date}-${slug}`;
|
|
579
|
+
}
|
|
580
|
+
function slugify(text) {
|
|
581
|
+
return text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 60);
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// src/core/operation-manager.ts
|
|
585
|
+
import * as fs7 from "fs/promises";
|
|
586
|
+
import * as path4 from "path";
|
|
587
|
+
import { glob as glob2 } from "glob";
|
|
588
|
+
import * as yaml3 from "js-yaml";
|
|
589
|
+
import { format } from "date-fns";
|
|
590
|
+
var OperationManager = class {
|
|
591
|
+
vault;
|
|
592
|
+
agentId;
|
|
593
|
+
constructor(vault, agentId) {
|
|
594
|
+
this.vault = vault;
|
|
595
|
+
this.agentId = agentId;
|
|
596
|
+
}
|
|
597
|
+
/**
|
|
598
|
+
* Create a fact directly (single-agent mode).
|
|
599
|
+
*/
|
|
600
|
+
async createFactDirect(params) {
|
|
601
|
+
const factId = params.id ?? generateFactId(params.entity, params.predicate);
|
|
602
|
+
const factPath = this.vault.factPath(params.entity, params.predicate);
|
|
603
|
+
if (await fileExists(factPath)) {
|
|
604
|
+
throw new OperationError(
|
|
605
|
+
`Fact already exists at ${this.vault.relativePath(factPath)}. Use update_fact instead.`,
|
|
606
|
+
"FACT_EXISTS"
|
|
607
|
+
);
|
|
608
|
+
}
|
|
609
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
610
|
+
const data = {
|
|
611
|
+
type: "fact",
|
|
612
|
+
id: factId,
|
|
613
|
+
entity: params.entity,
|
|
614
|
+
predicate: params.predicate,
|
|
615
|
+
value: params.value,
|
|
616
|
+
valid_from: params.valid_from ?? null,
|
|
617
|
+
valid_to: params.valid_to ?? null,
|
|
618
|
+
recorded_at: now,
|
|
619
|
+
confidence: params.confidence ?? "medium",
|
|
620
|
+
sources: params.sources ?? [],
|
|
621
|
+
last_reviewed: format(/* @__PURE__ */ new Date(), "yyyy-MM-dd"),
|
|
622
|
+
tags: params.tags ?? []
|
|
623
|
+
};
|
|
624
|
+
await writeMarkdownFile(factPath, data);
|
|
625
|
+
return { path: this.vault.relativePath(factPath), id: factId };
|
|
626
|
+
}
|
|
627
|
+
/**
|
|
628
|
+
* Create an operation envelope for creating a fact (inbox mode).
|
|
629
|
+
*/
|
|
630
|
+
async createFactEnvelope(params) {
|
|
631
|
+
const operationId = generateOperationId();
|
|
632
|
+
const factId = params.id ?? generateFactId(params.entity, params.predicate);
|
|
633
|
+
const targetPath = `memory/facts/${params.entity}/${params.predicate}.md`;
|
|
634
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
635
|
+
const data = {
|
|
636
|
+
type: "operation",
|
|
637
|
+
operation_id: operationId,
|
|
638
|
+
op: "create_fact",
|
|
639
|
+
agent_id: this.agentId,
|
|
640
|
+
created_at: now,
|
|
641
|
+
target_id: factId,
|
|
642
|
+
target_path: targetPath,
|
|
643
|
+
precondition_hash: null,
|
|
644
|
+
status: "proposed",
|
|
645
|
+
reason: params.reason,
|
|
646
|
+
sources: params.sources ?? [],
|
|
647
|
+
payload: {
|
|
648
|
+
type: "fact",
|
|
649
|
+
id: factId,
|
|
650
|
+
entity: params.entity,
|
|
651
|
+
predicate: params.predicate,
|
|
652
|
+
value: params.value,
|
|
653
|
+
recorded_at: now,
|
|
654
|
+
confidence: params.confidence ?? "medium",
|
|
655
|
+
sources: params.sources ?? [],
|
|
656
|
+
valid_from: params.valid_from ?? null,
|
|
657
|
+
valid_to: params.valid_to ?? null,
|
|
658
|
+
tags: params.tags ?? []
|
|
659
|
+
}
|
|
660
|
+
};
|
|
661
|
+
const opPath = this.vault.inboxOpPath(this.agentId, operationId);
|
|
662
|
+
await writeMarkdownFile(opPath, data, `
|
|
663
|
+
# Create fact: ${params.entity}/${params.predicate}
|
|
664
|
+
|
|
665
|
+
${params.reason}
|
|
666
|
+
`);
|
|
667
|
+
return { path: this.vault.relativePath(opPath), operationId };
|
|
668
|
+
}
|
|
669
|
+
/**
|
|
670
|
+
* Update a fact directly (single-agent mode).
|
|
671
|
+
*/
|
|
672
|
+
async updateFactDirect(params) {
|
|
673
|
+
const factPath = await this.resolveFactPath(params);
|
|
674
|
+
const currentHash = await computeFileHash(factPath);
|
|
675
|
+
const parsed = await parseMarkdownFile(factPath);
|
|
676
|
+
const data = parsed.data;
|
|
677
|
+
if (params.value !== void 0) data.value = params.value;
|
|
678
|
+
if (params.valid_to !== void 0) data.valid_to = params.valid_to;
|
|
679
|
+
if (params.confidence !== void 0) data.confidence = params.confidence;
|
|
680
|
+
if (params.tags !== void 0) data.tags = params.tags;
|
|
681
|
+
if (params.last_reviewed !== void 0) {
|
|
682
|
+
data.last_reviewed = params.last_reviewed;
|
|
683
|
+
} else {
|
|
684
|
+
data.last_reviewed = format(/* @__PURE__ */ new Date(), "yyyy-MM-dd");
|
|
685
|
+
}
|
|
686
|
+
await writeMarkdownFile(factPath, data, parsed.content);
|
|
687
|
+
const newHash = await computeFileHash(factPath);
|
|
688
|
+
return { path: this.vault.relativePath(factPath), hash: newHash };
|
|
689
|
+
}
|
|
690
|
+
/**
|
|
691
|
+
* Create an operation envelope for updating a fact (inbox mode).
|
|
692
|
+
*/
|
|
693
|
+
async updateFactEnvelope(params) {
|
|
694
|
+
const factPath = await this.resolveFactPath(params);
|
|
695
|
+
const operationId = generateOperationId();
|
|
696
|
+
const currentHash = await computeFileHash(factPath);
|
|
697
|
+
const parsed = await parseMarkdownFile(factPath);
|
|
698
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
699
|
+
const payload = { ...parsed.data };
|
|
700
|
+
if (params.value !== void 0) payload.value = params.value;
|
|
701
|
+
if (params.valid_to !== void 0) payload.valid_to = params.valid_to;
|
|
702
|
+
if (params.confidence !== void 0) payload.confidence = params.confidence;
|
|
703
|
+
if (params.tags !== void 0) payload.tags = params.tags;
|
|
704
|
+
const data = {
|
|
705
|
+
type: "operation",
|
|
706
|
+
operation_id: operationId,
|
|
707
|
+
op: "update_fact",
|
|
708
|
+
agent_id: this.agentId,
|
|
709
|
+
created_at: now,
|
|
710
|
+
target_id: parsed.data.id ?? null,
|
|
711
|
+
target_path: this.vault.relativePath(factPath),
|
|
712
|
+
precondition_hash: currentHash,
|
|
713
|
+
status: "proposed",
|
|
714
|
+
reason: params.reason,
|
|
715
|
+
sources: parsed.data.sources ?? [],
|
|
716
|
+
payload
|
|
717
|
+
};
|
|
718
|
+
const opPath = this.vault.inboxOpPath(this.agentId, operationId);
|
|
719
|
+
await writeMarkdownFile(opPath, data, `
|
|
720
|
+
# Update fact
|
|
721
|
+
|
|
722
|
+
${params.reason}
|
|
723
|
+
`);
|
|
724
|
+
return { path: this.vault.relativePath(opPath), operationId };
|
|
725
|
+
}
|
|
726
|
+
/**
|
|
727
|
+
* Add an event directly.
|
|
728
|
+
*/
|
|
729
|
+
async addEventDirect(params) {
|
|
730
|
+
const now = /* @__PURE__ */ new Date();
|
|
731
|
+
const dateStr = format(now, "yyyy-MM-dd");
|
|
732
|
+
const slug = slugify(params.summary);
|
|
733
|
+
const eventId = generateEventId(dateStr, slug);
|
|
734
|
+
const eventDir = this.vault.eventDir(dateStr);
|
|
735
|
+
const eventPath = path4.join(eventDir, `${slug}.md`);
|
|
736
|
+
const data = {
|
|
737
|
+
type: "event",
|
|
738
|
+
id: eventId,
|
|
739
|
+
occurred_at: now.toISOString(),
|
|
740
|
+
summary: params.summary,
|
|
741
|
+
entities: params.entities ?? [],
|
|
742
|
+
kind: params.kind ?? "observation",
|
|
743
|
+
sources: params.sources ?? [],
|
|
744
|
+
derived_facts: []
|
|
745
|
+
};
|
|
746
|
+
await writeMarkdownFile(eventPath, data, params.body ? `
|
|
747
|
+
${params.body}
|
|
748
|
+
` : "");
|
|
749
|
+
return { path: this.vault.relativePath(eventPath), id: eventId };
|
|
750
|
+
}
|
|
751
|
+
/**
|
|
752
|
+
* Create an operation envelope for adding an event (inbox mode).
|
|
753
|
+
*/
|
|
754
|
+
async addEventEnvelope(params) {
|
|
755
|
+
const operationId = generateOperationId();
|
|
756
|
+
const now = /* @__PURE__ */ new Date();
|
|
757
|
+
const dateStr = format(now, "yyyy-MM-dd");
|
|
758
|
+
const slug = slugify(params.summary);
|
|
759
|
+
const data = {
|
|
760
|
+
type: "operation",
|
|
761
|
+
operation_id: operationId,
|
|
762
|
+
op: "add_event",
|
|
763
|
+
agent_id: this.agentId,
|
|
764
|
+
created_at: now.toISOString(),
|
|
765
|
+
target_id: generateEventId(dateStr, slug),
|
|
766
|
+
target_path: `memory/events/${dateStr}/${slug}.md`,
|
|
767
|
+
precondition_hash: null,
|
|
768
|
+
status: "proposed",
|
|
769
|
+
reason: params.reason ?? params.summary,
|
|
770
|
+
sources: params.sources ?? [],
|
|
771
|
+
payload: {
|
|
772
|
+
type: "event",
|
|
773
|
+
occurred_at: now.toISOString(),
|
|
774
|
+
summary: params.summary,
|
|
775
|
+
entities: params.entities ?? [],
|
|
776
|
+
kind: params.kind ?? "observation",
|
|
777
|
+
sources: params.sources ?? [],
|
|
778
|
+
body: params.body ?? ""
|
|
779
|
+
}
|
|
780
|
+
};
|
|
781
|
+
const opPath = this.vault.inboxOpPath(this.agentId, operationId);
|
|
782
|
+
await writeMarkdownFile(opPath, data, `
|
|
783
|
+
# Add event
|
|
784
|
+
|
|
785
|
+
${params.summary}
|
|
786
|
+
`);
|
|
787
|
+
return { path: this.vault.relativePath(opPath), operationId };
|
|
788
|
+
}
|
|
789
|
+
/**
|
|
790
|
+
* Archive a fact directly.
|
|
791
|
+
*/
|
|
792
|
+
async archiveFactDirect(params) {
|
|
793
|
+
const factPath = await this.resolveFactPath(params);
|
|
794
|
+
const parsed = await parseMarkdownFile(factPath);
|
|
795
|
+
const fact = parsed.data;
|
|
796
|
+
const year = format(/* @__PURE__ */ new Date(), "yyyy");
|
|
797
|
+
const archiveDest = this.vault.archivePath(year, fact.entity, fact.predicate);
|
|
798
|
+
await fs7.mkdir(path4.dirname(archiveDest), { recursive: true });
|
|
799
|
+
await fs7.rename(factPath, archiveDest);
|
|
800
|
+
return { archivePath: this.vault.relativePath(archiveDest) };
|
|
801
|
+
}
|
|
802
|
+
/**
|
|
803
|
+
* Compact inbox: validate and apply all proposed operations.
|
|
804
|
+
*/
|
|
805
|
+
async compact(autoApply = true) {
|
|
806
|
+
const result = { applied: 0, conflicts: 0, archived: 0, errors: [] };
|
|
807
|
+
const opFiles = await glob2(path4.join(this.vault.paths.inbox, "**", "ops", "*.md"));
|
|
808
|
+
for (const opFile of opFiles) {
|
|
809
|
+
const parsed = await parseMarkdownFile(opFile);
|
|
810
|
+
const op = parsed.data;
|
|
811
|
+
if (op.status !== "proposed") continue;
|
|
812
|
+
if (op.target_id) {
|
|
813
|
+
const claimConflict = await this.checkClaim(op.target_id, op.agent_id);
|
|
814
|
+
if (claimConflict) {
|
|
815
|
+
await this.markConflict(opFile, parsed, `Blocked by active claim: ${claimConflict}`);
|
|
816
|
+
result.conflicts++;
|
|
817
|
+
continue;
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
if (op.precondition_hash && op.target_path) {
|
|
821
|
+
const targetAbsPath = this.vault.resolvePath(op.target_path);
|
|
822
|
+
if (await fileExists(targetAbsPath)) {
|
|
823
|
+
const currentHash = await computeFileHash(targetAbsPath);
|
|
824
|
+
if (currentHash !== op.precondition_hash) {
|
|
825
|
+
await this.markConflict(opFile, parsed, "precondition hash mismatch");
|
|
826
|
+
result.conflicts++;
|
|
827
|
+
continue;
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
if (!autoApply) continue;
|
|
832
|
+
try {
|
|
833
|
+
await this.applyOperation(op, parsed.content);
|
|
834
|
+
await this.moveToApplied(opFile, parsed);
|
|
835
|
+
result.applied++;
|
|
836
|
+
} catch (err) {
|
|
837
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
838
|
+
await this.markConflict(opFile, parsed, msg);
|
|
839
|
+
result.conflicts++;
|
|
840
|
+
result.errors.push(`${op.operation_id}: ${msg}`);
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
const factFiles = await glob2(path4.join(this.vault.paths.facts, "**", "*.md"));
|
|
844
|
+
const today = process.env.MEMORY_TODAY ?? format(/* @__PURE__ */ new Date(), "yyyy-MM-dd");
|
|
845
|
+
for (const file of factFiles) {
|
|
846
|
+
const parsed = await parseMarkdownFile(file);
|
|
847
|
+
const fact = parsed.data;
|
|
848
|
+
if (fact.valid_to && fact.valid_to < today) {
|
|
849
|
+
const year = fact.valid_to.slice(0, 4);
|
|
850
|
+
const archiveDest = this.vault.archivePath(year, fact.entity, fact.predicate);
|
|
851
|
+
await fs7.mkdir(path4.dirname(archiveDest), { recursive: true });
|
|
852
|
+
await fs7.rename(file, archiveDest);
|
|
853
|
+
result.archived++;
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
return result;
|
|
857
|
+
}
|
|
858
|
+
async applyOperation(op, body) {
|
|
859
|
+
const payload = op.payload;
|
|
860
|
+
switch (op.op) {
|
|
861
|
+
case "create_fact": {
|
|
862
|
+
const targetPath = this.vault.resolvePath(op.target_path);
|
|
863
|
+
if (await fileExists(targetPath)) {
|
|
864
|
+
throw new Error(`Target already exists: ${op.target_path}`);
|
|
865
|
+
}
|
|
866
|
+
const factData = { ...payload };
|
|
867
|
+
if (!factData.last_reviewed) {
|
|
868
|
+
factData.last_reviewed = format(/* @__PURE__ */ new Date(), "yyyy-MM-dd");
|
|
869
|
+
}
|
|
870
|
+
await writeMarkdownFile(targetPath, factData);
|
|
871
|
+
break;
|
|
872
|
+
}
|
|
873
|
+
case "update_fact": {
|
|
874
|
+
const targetPath = this.vault.resolvePath(op.target_path);
|
|
875
|
+
if (!await fileExists(targetPath)) {
|
|
876
|
+
throw new Error(`Target not found: ${op.target_path}`);
|
|
877
|
+
}
|
|
878
|
+
const existing = await parseMarkdownFile(targetPath);
|
|
879
|
+
const updated = { ...existing.data, ...payload };
|
|
880
|
+
updated.last_reviewed = format(/* @__PURE__ */ new Date(), "yyyy-MM-dd");
|
|
881
|
+
const bodyContent = payload.body ?? existing.content;
|
|
882
|
+
await writeMarkdownFile(targetPath, updated, bodyContent);
|
|
883
|
+
break;
|
|
884
|
+
}
|
|
885
|
+
case "add_event": {
|
|
886
|
+
const targetPath = this.vault.resolvePath(op.target_path);
|
|
887
|
+
const eventBody = payload.body ?? "";
|
|
888
|
+
const eventData = { ...payload };
|
|
889
|
+
delete eventData.body;
|
|
890
|
+
await writeMarkdownFile(targetPath, eventData, eventBody ? `
|
|
891
|
+
${eventBody}
|
|
892
|
+
` : "");
|
|
893
|
+
break;
|
|
894
|
+
}
|
|
895
|
+
case "archive_fact": {
|
|
896
|
+
const targetPath = this.vault.resolvePath(op.target_path);
|
|
897
|
+
if (!await fileExists(targetPath)) {
|
|
898
|
+
throw new Error(`Target not found for archival: ${op.target_path}`);
|
|
899
|
+
}
|
|
900
|
+
const parsed = await parseMarkdownFile(targetPath);
|
|
901
|
+
const fact = parsed.data;
|
|
902
|
+
const year = format(/* @__PURE__ */ new Date(), "yyyy");
|
|
903
|
+
const archiveDest = this.vault.archivePath(year, fact.entity, fact.predicate);
|
|
904
|
+
await fs7.mkdir(path4.dirname(archiveDest), { recursive: true });
|
|
905
|
+
await fs7.rename(targetPath, archiveDest);
|
|
906
|
+
break;
|
|
907
|
+
}
|
|
908
|
+
default:
|
|
909
|
+
throw new Error(`Unsupported operation type: ${op.op}`);
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
async markConflict(opFile, parsed, reason) {
|
|
913
|
+
parsed.data.status = "conflict";
|
|
914
|
+
parsed.data.conflict_reason = reason;
|
|
915
|
+
await writeMarkdownFile(opFile, parsed.data, parsed.content);
|
|
916
|
+
}
|
|
917
|
+
async moveToApplied(opFile, parsed) {
|
|
918
|
+
const opId = parsed.data.operation_id;
|
|
919
|
+
parsed.data.status = "applied";
|
|
920
|
+
parsed.data.applied_at = (/* @__PURE__ */ new Date()).toISOString();
|
|
921
|
+
const receiptPath = this.vault.appliedOpPath(opId);
|
|
922
|
+
await writeMarkdownFile(receiptPath, parsed.data, parsed.content);
|
|
923
|
+
await fs7.unlink(opFile);
|
|
924
|
+
}
|
|
925
|
+
async checkClaim(targetId, agentId) {
|
|
926
|
+
const claimPath = this.vault.claimPath(targetId);
|
|
927
|
+
if (!await fileExists(claimPath)) return null;
|
|
928
|
+
const raw = await fs7.readFile(claimPath, "utf-8");
|
|
929
|
+
const claim = yaml3.load(raw);
|
|
930
|
+
if (claim.status !== "active") return null;
|
|
931
|
+
const now = /* @__PURE__ */ new Date();
|
|
932
|
+
const expires = new Date(claim.expires_at);
|
|
933
|
+
if (now > expires) return null;
|
|
934
|
+
if (claim.agent_id === agentId) return null;
|
|
935
|
+
return `${claim.agent_id} (expires ${claim.expires_at})`;
|
|
936
|
+
}
|
|
937
|
+
async resolveFactPath(params) {
|
|
938
|
+
if (params.targetPath) {
|
|
939
|
+
const absPath = this.vault.resolvePath(params.targetPath);
|
|
940
|
+
if (await fileExists(absPath)) return absPath;
|
|
941
|
+
throw new OperationError(`Fact not found: ${params.targetPath}`, "FACT_NOT_FOUND");
|
|
942
|
+
}
|
|
943
|
+
if (params.targetId) {
|
|
944
|
+
const files = await glob2(path4.join(this.vault.paths.facts, "**", "*.md"));
|
|
945
|
+
for (const file of files) {
|
|
946
|
+
const parsed = await parseMarkdownFile(file);
|
|
947
|
+
if (parsed.data.id === params.targetId) {
|
|
948
|
+
return file;
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
throw new OperationError(`Fact not found with id: ${params.targetId}`, "FACT_NOT_FOUND");
|
|
952
|
+
}
|
|
953
|
+
throw new OperationError("Either targetId or targetPath is required", "INVALID_PARAMS");
|
|
954
|
+
}
|
|
955
|
+
};
|
|
956
|
+
var OperationError = class extends Error {
|
|
957
|
+
code;
|
|
958
|
+
constructor(message, code) {
|
|
959
|
+
super(message);
|
|
960
|
+
this.name = "OperationError";
|
|
961
|
+
this.code = code;
|
|
962
|
+
}
|
|
963
|
+
};
|
|
964
|
+
|
|
965
|
+
// src/core/schema-validator.ts
|
|
966
|
+
import * as fs8 from "fs/promises";
|
|
967
|
+
import * as path5 from "path";
|
|
968
|
+
import { glob as glob3 } from "glob";
|
|
969
|
+
import * as yaml4 from "js-yaml";
|
|
970
|
+
var SchemaValidator = class {
|
|
971
|
+
vault;
|
|
972
|
+
entities;
|
|
973
|
+
predicates;
|
|
974
|
+
constructor(vault, entities, predicates) {
|
|
975
|
+
this.vault = vault;
|
|
976
|
+
this.entities = entities;
|
|
977
|
+
this.predicates = predicates;
|
|
978
|
+
}
|
|
979
|
+
/**
|
|
980
|
+
* Run full vault validation. Returns findings array. Empty = valid.
|
|
981
|
+
*/
|
|
982
|
+
async lint() {
|
|
983
|
+
const findings = [];
|
|
984
|
+
await this.checkVersion(findings);
|
|
985
|
+
await this.validateFacts(findings);
|
|
986
|
+
await this.validateEvents(findings);
|
|
987
|
+
await this.validateOperations(findings);
|
|
988
|
+
await this.checkDuplicateIds(findings);
|
|
989
|
+
await this.checkContradictions(findings);
|
|
990
|
+
await this.validateWikilinks(findings);
|
|
991
|
+
return findings;
|
|
992
|
+
}
|
|
993
|
+
async checkVersion(findings) {
|
|
994
|
+
try {
|
|
995
|
+
const content = await fs8.readFile(this.vault.paths.versionYaml, "utf-8");
|
|
996
|
+
const version = yaml4.load(content);
|
|
997
|
+
if (version?.spec_version !== "3.0") {
|
|
998
|
+
findings.push({
|
|
999
|
+
level: "ERROR",
|
|
1000
|
+
path: this.vault.relativePath(this.vault.paths.versionYaml),
|
|
1001
|
+
message: `missing v3 schema version marker (expected spec_version: "3.0")`
|
|
1002
|
+
});
|
|
1003
|
+
}
|
|
1004
|
+
} catch {
|
|
1005
|
+
findings.push({
|
|
1006
|
+
level: "ERROR",
|
|
1007
|
+
path: "memory/schema/version.yaml",
|
|
1008
|
+
message: "missing v3 schema version marker"
|
|
1009
|
+
});
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
async validateFacts(findings) {
|
|
1013
|
+
const factFiles = await glob3(path5.join(this.vault.paths.facts, "**", "*.md"));
|
|
1014
|
+
for (const file of factFiles) {
|
|
1015
|
+
const relPath = this.vault.relativePath(file);
|
|
1016
|
+
try {
|
|
1017
|
+
const parsed = await parseMarkdownFile(file);
|
|
1018
|
+
const data = parsed.data;
|
|
1019
|
+
if (data.type !== "fact") {
|
|
1020
|
+
findings.push({ level: "ERROR", path: relPath, message: `expected type: fact, got: ${data.type}` });
|
|
1021
|
+
continue;
|
|
1022
|
+
}
|
|
1023
|
+
if (!data.entity) {
|
|
1024
|
+
findings.push({ level: "ERROR", path: relPath, message: "missing required field: entity" });
|
|
1025
|
+
}
|
|
1026
|
+
if (!data.predicate) {
|
|
1027
|
+
findings.push({ level: "ERROR", path: relPath, message: "missing required field: predicate" });
|
|
1028
|
+
}
|
|
1029
|
+
if (data.value === void 0 || data.value === null) {
|
|
1030
|
+
findings.push({ level: "ERROR", path: relPath, message: "missing required field: value" });
|
|
1031
|
+
}
|
|
1032
|
+
if (!data.recorded_at) {
|
|
1033
|
+
findings.push({ level: "ERROR", path: relPath, message: "missing required field: recorded_at" });
|
|
1034
|
+
}
|
|
1035
|
+
if (data.entity && !await this.entities.exists(data.entity)) {
|
|
1036
|
+
findings.push({ level: "ERROR", path: relPath, message: `unknown entity '${data.entity}'` });
|
|
1037
|
+
}
|
|
1038
|
+
if (data.predicate && !await this.predicates.exists(data.predicate)) {
|
|
1039
|
+
findings.push({
|
|
1040
|
+
level: "ERROR",
|
|
1041
|
+
path: relPath,
|
|
1042
|
+
message: `unknown predicate '${data.predicate}' -- add it to memory/schema/predicates.yaml`
|
|
1043
|
+
});
|
|
1044
|
+
}
|
|
1045
|
+
const expectedDir = path5.join(this.vault.paths.facts, data.entity);
|
|
1046
|
+
const actualDir = path5.dirname(file);
|
|
1047
|
+
if (actualDir !== expectedDir) {
|
|
1048
|
+
findings.push({
|
|
1049
|
+
level: "ERROR",
|
|
1050
|
+
path: relPath,
|
|
1051
|
+
message: `fact for entity '${data.entity}' should be in memory/facts/${data.entity}/`
|
|
1052
|
+
});
|
|
1053
|
+
}
|
|
1054
|
+
if (data.valid_from && data.valid_to && data.valid_from > data.valid_to) {
|
|
1055
|
+
findings.push({
|
|
1056
|
+
level: "ERROR",
|
|
1057
|
+
path: relPath,
|
|
1058
|
+
message: `valid_from (${data.valid_from}) must be <= valid_to (${data.valid_to})`
|
|
1059
|
+
});
|
|
1060
|
+
}
|
|
1061
|
+
if (data.sources) {
|
|
1062
|
+
for (const source of data.sources) {
|
|
1063
|
+
const sourcePath = this.vault.resolvePath(source);
|
|
1064
|
+
if (!await fileExists(sourcePath)) {
|
|
1065
|
+
findings.push({ level: "WARN", path: relPath, message: `source not found: ${source}` });
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
} catch (err) {
|
|
1070
|
+
findings.push({
|
|
1071
|
+
level: "ERROR",
|
|
1072
|
+
path: relPath,
|
|
1073
|
+
message: `parse error: ${err instanceof Error ? err.message : String(err)}`
|
|
1074
|
+
});
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
async validateEvents(findings) {
|
|
1079
|
+
const eventFiles = await glob3(path5.join(this.vault.paths.events, "**", "*.md"));
|
|
1080
|
+
for (const file of eventFiles) {
|
|
1081
|
+
const relPath = this.vault.relativePath(file);
|
|
1082
|
+
try {
|
|
1083
|
+
const parsed = await parseMarkdownFile(file);
|
|
1084
|
+
const data = parsed.data;
|
|
1085
|
+
if (data.type !== "event") {
|
|
1086
|
+
findings.push({ level: "ERROR", path: relPath, message: `expected type: event, got: ${data.type}` });
|
|
1087
|
+
continue;
|
|
1088
|
+
}
|
|
1089
|
+
if (!data.occurred_at) {
|
|
1090
|
+
findings.push({ level: "ERROR", path: relPath, message: "missing required field: occurred_at" });
|
|
1091
|
+
}
|
|
1092
|
+
if (!data.summary) {
|
|
1093
|
+
findings.push({ level: "ERROR", path: relPath, message: "missing required field: summary" });
|
|
1094
|
+
}
|
|
1095
|
+
if (Array.isArray(data.entities)) {
|
|
1096
|
+
for (const entity of data.entities) {
|
|
1097
|
+
if (!await this.entities.exists(entity)) {
|
|
1098
|
+
findings.push({ level: "WARN", path: relPath, message: `event references unknown entity '${entity}'` });
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
} catch (err) {
|
|
1103
|
+
findings.push({
|
|
1104
|
+
level: "ERROR",
|
|
1105
|
+
path: relPath,
|
|
1106
|
+
message: `parse error: ${err instanceof Error ? err.message : String(err)}`
|
|
1107
|
+
});
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
async validateOperations(findings) {
|
|
1112
|
+
const opFiles = await glob3(path5.join(this.vault.paths.inbox, "**", "ops", "*.md"));
|
|
1113
|
+
const appliedFiles = await glob3(path5.join(this.vault.paths.opsApplied, "*.md"));
|
|
1114
|
+
for (const file of [...opFiles, ...appliedFiles]) {
|
|
1115
|
+
const relPath = this.vault.relativePath(file);
|
|
1116
|
+
try {
|
|
1117
|
+
const parsed = await parseMarkdownFile(file);
|
|
1118
|
+
const data = parsed.data;
|
|
1119
|
+
if (data.type !== "operation") continue;
|
|
1120
|
+
if (!data.operation_id) {
|
|
1121
|
+
findings.push({ level: "ERROR", path: relPath, message: "missing required field: operation_id" });
|
|
1122
|
+
}
|
|
1123
|
+
if (!data.op) {
|
|
1124
|
+
findings.push({ level: "ERROR", path: relPath, message: "missing required field: op" });
|
|
1125
|
+
}
|
|
1126
|
+
if (!data.agent_id) {
|
|
1127
|
+
findings.push({ level: "ERROR", path: relPath, message: "missing required field: agent_id" });
|
|
1128
|
+
}
|
|
1129
|
+
if (!data.reason) {
|
|
1130
|
+
findings.push({ level: "ERROR", path: relPath, message: "missing required field: reason" });
|
|
1131
|
+
}
|
|
1132
|
+
if (data.agent_id && !/^agent-[a-z0-9-]+-[a-f0-9]{8}$/.test(data.agent_id)) {
|
|
1133
|
+
findings.push({
|
|
1134
|
+
level: "WARN",
|
|
1135
|
+
path: relPath,
|
|
1136
|
+
message: `agent_id '${data.agent_id}' doesn't match pattern: agent-{slug}-{8hex}`
|
|
1137
|
+
});
|
|
1138
|
+
}
|
|
1139
|
+
if (data.op === "create_fact" && data.payload) {
|
|
1140
|
+
const payload = data.payload;
|
|
1141
|
+
if (payload.predicate && typeof payload.predicate === "string") {
|
|
1142
|
+
if (!await this.predicates.exists(payload.predicate)) {
|
|
1143
|
+
findings.push({
|
|
1144
|
+
level: "ERROR",
|
|
1145
|
+
path: relPath,
|
|
1146
|
+
message: `payload unknown predicate '${payload.predicate}' -- add it to memory/schema/predicates.yaml`
|
|
1147
|
+
});
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
} catch (err) {
|
|
1152
|
+
findings.push({
|
|
1153
|
+
level: "ERROR",
|
|
1154
|
+
path: relPath,
|
|
1155
|
+
message: `parse error: ${err instanceof Error ? err.message : String(err)}`
|
|
1156
|
+
});
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
async checkDuplicateIds(findings) {
|
|
1161
|
+
const idMap = /* @__PURE__ */ new Map();
|
|
1162
|
+
const allFiles = [
|
|
1163
|
+
...await glob3(path5.join(this.vault.paths.facts, "**", "*.md")),
|
|
1164
|
+
...await glob3(path5.join(this.vault.paths.events, "**", "*.md"))
|
|
1165
|
+
];
|
|
1166
|
+
for (const file of allFiles) {
|
|
1167
|
+
try {
|
|
1168
|
+
const parsed = await parseMarkdownFile(file);
|
|
1169
|
+
const id = parsed.data.id;
|
|
1170
|
+
if (id) {
|
|
1171
|
+
const existing = idMap.get(id);
|
|
1172
|
+
if (existing) {
|
|
1173
|
+
findings.push({
|
|
1174
|
+
level: "ERROR",
|
|
1175
|
+
path: this.vault.relativePath(file),
|
|
1176
|
+
message: `duplicate id '${id}' (first seen in ${existing})`
|
|
1177
|
+
});
|
|
1178
|
+
} else {
|
|
1179
|
+
idMap.set(id, this.vault.relativePath(file));
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
} catch {
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
async checkContradictions(findings) {
|
|
1187
|
+
const factFiles = await glob3(path5.join(this.vault.paths.facts, "**", "*.md"));
|
|
1188
|
+
const factsByKey = /* @__PURE__ */ new Map();
|
|
1189
|
+
for (const file of factFiles) {
|
|
1190
|
+
try {
|
|
1191
|
+
const parsed = await parseMarkdownFile(file);
|
|
1192
|
+
const fact = parsed.data;
|
|
1193
|
+
if (fact.type !== "fact") continue;
|
|
1194
|
+
const key = `${fact.entity}/${fact.predicate}`;
|
|
1195
|
+
if (!factsByKey.has(key)) factsByKey.set(key, []);
|
|
1196
|
+
factsByKey.get(key).push({ fact, path: file });
|
|
1197
|
+
} catch {
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
for (const [key, facts] of factsByKey) {
|
|
1201
|
+
if (facts.length < 2) continue;
|
|
1202
|
+
for (let i = 0; i < facts.length; i++) {
|
|
1203
|
+
for (let j = i + 1; j < facts.length; j++) {
|
|
1204
|
+
const a = facts[i];
|
|
1205
|
+
const b = facts[j];
|
|
1206
|
+
if (a.fact.superseded_by || b.fact.superseded_by) continue;
|
|
1207
|
+
if (String(a.fact.value) === String(b.fact.value)) continue;
|
|
1208
|
+
if (this.temporalOverlap(a.fact, b.fact)) {
|
|
1209
|
+
findings.push({
|
|
1210
|
+
level: "ERROR",
|
|
1211
|
+
path: this.vault.relativePath(b.path),
|
|
1212
|
+
message: `contradicts overlapping fact at ${this.vault.relativePath(a.path)}`
|
|
1213
|
+
});
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
temporalOverlap(a, b) {
|
|
1220
|
+
const aFrom = a.valid_from ?? "0000-01-01";
|
|
1221
|
+
const aTo = a.valid_to ?? "9999-12-31";
|
|
1222
|
+
const bFrom = b.valid_from ?? "0000-01-01";
|
|
1223
|
+
const bTo = b.valid_to ?? "9999-12-31";
|
|
1224
|
+
return aFrom <= bTo && bFrom <= aTo;
|
|
1225
|
+
}
|
|
1226
|
+
async validateWikilinks(findings) {
|
|
1227
|
+
const allFiles = await glob3(path5.join(this.vault.paths.memory, "**", "*.md"));
|
|
1228
|
+
const stemMap = /* @__PURE__ */ new Map();
|
|
1229
|
+
const fullPathSet = /* @__PURE__ */ new Set();
|
|
1230
|
+
for (const file of allFiles) {
|
|
1231
|
+
const relPath = this.vault.relativePath(file);
|
|
1232
|
+
fullPathSet.add(relPath);
|
|
1233
|
+
const noExt = relPath.replace(/\.md$/, "");
|
|
1234
|
+
fullPathSet.add(noExt);
|
|
1235
|
+
const stem = path5.basename(file, ".md");
|
|
1236
|
+
if (!stemMap.has(stem)) stemMap.set(stem, []);
|
|
1237
|
+
stemMap.get(stem).push(relPath);
|
|
1238
|
+
}
|
|
1239
|
+
const wikilinkRegex = /\[\[([^\]|#]+)(?:#[^\]|]*)?\|?[^\]]*\]\]/g;
|
|
1240
|
+
for (const file of allFiles) {
|
|
1241
|
+
if (file.startsWith(this.vault.paths.views)) continue;
|
|
1242
|
+
const relPath = this.vault.relativePath(file);
|
|
1243
|
+
try {
|
|
1244
|
+
const content = await fs8.readFile(file, "utf-8");
|
|
1245
|
+
let match;
|
|
1246
|
+
wikilinkRegex.lastIndex = 0;
|
|
1247
|
+
while ((match = wikilinkRegex.exec(content)) !== null) {
|
|
1248
|
+
const target = match[1].trim();
|
|
1249
|
+
if (fullPathSet.has(target) || fullPathSet.has(`${target}.md`)) continue;
|
|
1250
|
+
const stem = path5.basename(target, ".md");
|
|
1251
|
+
const candidates = stemMap.get(stem);
|
|
1252
|
+
if (!candidates || candidates.length === 0) {
|
|
1253
|
+
findings.push({
|
|
1254
|
+
level: "ERROR",
|
|
1255
|
+
path: relPath,
|
|
1256
|
+
message: `unresolved wikilink: ${target}`
|
|
1257
|
+
});
|
|
1258
|
+
} else if (candidates.length > 1) {
|
|
1259
|
+
findings.push({
|
|
1260
|
+
level: "ERROR",
|
|
1261
|
+
path: relPath,
|
|
1262
|
+
message: `ambiguous wikilink: ${target} (resolves to ${candidates.length} files)`
|
|
1263
|
+
});
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
} catch {
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
};
|
|
1271
|
+
|
|
1272
|
+
// src/core/view-generator.ts
|
|
1273
|
+
import * as fs9 from "fs/promises";
|
|
1274
|
+
import * as path6 from "path";
|
|
1275
|
+
import { glob as glob4 } from "glob";
|
|
1276
|
+
import { format as format2, differenceInDays, parseISO as parseISO2 } from "date-fns";
|
|
1277
|
+
var ViewGenerator = class {
|
|
1278
|
+
vault;
|
|
1279
|
+
staleDays;
|
|
1280
|
+
constructor(vault, staleDays = 180) {
|
|
1281
|
+
this.vault = vault;
|
|
1282
|
+
this.staleDays = staleDays;
|
|
1283
|
+
}
|
|
1284
|
+
/**
|
|
1285
|
+
* Rebuild all materialized views. Returns list of files written.
|
|
1286
|
+
*/
|
|
1287
|
+
async rebuildAll() {
|
|
1288
|
+
const written = [];
|
|
1289
|
+
await fs9.mkdir(this.vault.paths.views, { recursive: true });
|
|
1290
|
+
await fs9.mkdir(path6.join(this.vault.paths.views, "by-entity"), { recursive: true });
|
|
1291
|
+
const facts = await this.loadAllFacts();
|
|
1292
|
+
const events = await this.loadAllEvents();
|
|
1293
|
+
const operations = await this.loadAllOperations();
|
|
1294
|
+
const claims = await this.loadAllClaims();
|
|
1295
|
+
const byEntity = this.groupByEntity(facts);
|
|
1296
|
+
for (const [entity, entityFacts] of byEntity) {
|
|
1297
|
+
const filePath = this.vault.entityViewPath(entity);
|
|
1298
|
+
await this.writeView(filePath, this.renderByEntity(entity, entityFacts));
|
|
1299
|
+
written.push(this.vault.relativePath(filePath));
|
|
1300
|
+
}
|
|
1301
|
+
const byIdPath = this.vault.viewPath("by-id");
|
|
1302
|
+
await this.writeView(byIdPath, this.renderById(facts, events, operations));
|
|
1303
|
+
written.push(this.vault.relativePath(byIdPath));
|
|
1304
|
+
const byPredPath = this.vault.viewPath("by-predicate");
|
|
1305
|
+
await this.writeView(byPredPath, this.renderByPredicate(facts));
|
|
1306
|
+
written.push(this.vault.relativePath(byPredPath));
|
|
1307
|
+
const timelinePath = this.vault.viewPath("timeline");
|
|
1308
|
+
await this.writeView(timelinePath, this.renderTimeline(events));
|
|
1309
|
+
written.push(this.vault.relativePath(timelinePath));
|
|
1310
|
+
const contradictionsPath = this.vault.viewPath("contradictions");
|
|
1311
|
+
await this.writeView(contradictionsPath, this.renderContradictions(facts));
|
|
1312
|
+
written.push(this.vault.relativePath(contradictionsPath));
|
|
1313
|
+
const stalePath = this.vault.viewPath("stale");
|
|
1314
|
+
await this.writeView(stalePath, this.renderStale(facts));
|
|
1315
|
+
written.push(this.vault.relativePath(stalePath));
|
|
1316
|
+
const graphPath = this.vault.viewPath("graph");
|
|
1317
|
+
await this.writeView(graphPath, await this.renderGraph());
|
|
1318
|
+
written.push(this.vault.relativePath(graphPath));
|
|
1319
|
+
const inboxPath = this.vault.viewPath("inbox");
|
|
1320
|
+
await this.writeView(inboxPath, this.renderInbox(operations));
|
|
1321
|
+
written.push(this.vault.relativePath(inboxPath));
|
|
1322
|
+
const opsPath = this.vault.viewPath("operations");
|
|
1323
|
+
await this.writeView(opsPath, this.renderOperations(operations));
|
|
1324
|
+
written.push(this.vault.relativePath(opsPath));
|
|
1325
|
+
const claimsPath = this.vault.viewPath("claims");
|
|
1326
|
+
await this.writeView(claimsPath, this.renderClaims(claims));
|
|
1327
|
+
written.push(this.vault.relativePath(claimsPath));
|
|
1328
|
+
const conflictsPath = this.vault.viewPath("conflicts");
|
|
1329
|
+
await this.writeView(conflictsPath, this.renderConflicts(operations));
|
|
1330
|
+
written.push(this.vault.relativePath(conflictsPath));
|
|
1331
|
+
return written;
|
|
1332
|
+
}
|
|
1333
|
+
async writeView(filePath, content) {
|
|
1334
|
+
await fs9.mkdir(path6.dirname(filePath), { recursive: true });
|
|
1335
|
+
await fs9.writeFile(filePath, content, "utf-8");
|
|
1336
|
+
}
|
|
1337
|
+
async loadAllFacts() {
|
|
1338
|
+
const files = await glob4(path6.join(this.vault.paths.facts, "**", "*.md"));
|
|
1339
|
+
const results = [];
|
|
1340
|
+
for (const file of files.sort()) {
|
|
1341
|
+
try {
|
|
1342
|
+
const parsed = await parseMarkdownFile(file);
|
|
1343
|
+
const fact = parsed.data;
|
|
1344
|
+
if (fact.type === "fact") {
|
|
1345
|
+
results.push({ fact, path: this.vault.relativePath(file) });
|
|
1346
|
+
}
|
|
1347
|
+
} catch {
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
return results;
|
|
1351
|
+
}
|
|
1352
|
+
async loadAllEvents() {
|
|
1353
|
+
const files = await glob4(path6.join(this.vault.paths.events, "**", "*.md"));
|
|
1354
|
+
const results = [];
|
|
1355
|
+
for (const file of files.sort()) {
|
|
1356
|
+
try {
|
|
1357
|
+
const parsed = await parseMarkdownFile(file);
|
|
1358
|
+
const event = parsed.data;
|
|
1359
|
+
if (event.type === "event") {
|
|
1360
|
+
results.push({ event, path: this.vault.relativePath(file) });
|
|
1361
|
+
}
|
|
1362
|
+
} catch {
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
return results;
|
|
1366
|
+
}
|
|
1367
|
+
async loadAllOperations() {
|
|
1368
|
+
const inboxFiles = await glob4(path6.join(this.vault.paths.inbox, "**", "ops", "*.md"));
|
|
1369
|
+
const appliedFiles = await glob4(path6.join(this.vault.paths.opsApplied, "*.md"));
|
|
1370
|
+
const results = [];
|
|
1371
|
+
for (const file of [...inboxFiles, ...appliedFiles].sort()) {
|
|
1372
|
+
try {
|
|
1373
|
+
const parsed = await parseMarkdownFile(file);
|
|
1374
|
+
const op = parsed.data;
|
|
1375
|
+
if (op.type === "operation") {
|
|
1376
|
+
results.push({ op, path: this.vault.relativePath(file) });
|
|
1377
|
+
}
|
|
1378
|
+
} catch {
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
return results;
|
|
1382
|
+
}
|
|
1383
|
+
async loadAllClaims() {
|
|
1384
|
+
const files = await glob4(path6.join(this.vault.paths.claims, "*.yaml"));
|
|
1385
|
+
const results = [];
|
|
1386
|
+
for (const file of files.sort()) {
|
|
1387
|
+
try {
|
|
1388
|
+
const raw = await fs9.readFile(file, "utf-8");
|
|
1389
|
+
const claim = (await import("js-yaml")).load(raw);
|
|
1390
|
+
if (claim.type === "claim") {
|
|
1391
|
+
results.push({ claim, path: this.vault.relativePath(file) });
|
|
1392
|
+
}
|
|
1393
|
+
} catch {
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
return results;
|
|
1397
|
+
}
|
|
1398
|
+
groupByEntity(facts) {
|
|
1399
|
+
const map = /* @__PURE__ */ new Map();
|
|
1400
|
+
for (const entry of facts) {
|
|
1401
|
+
const entity = entry.fact.entity;
|
|
1402
|
+
if (!map.has(entity)) map.set(entity, []);
|
|
1403
|
+
map.get(entity).push(entry);
|
|
1404
|
+
}
|
|
1405
|
+
return map;
|
|
1406
|
+
}
|
|
1407
|
+
renderByEntity(entity, facts) {
|
|
1408
|
+
const lines = [`# Facts \u2014 ${entity}
|
|
1409
|
+
`];
|
|
1410
|
+
const sorted = [...facts].sort((a, b) => a.fact.predicate.localeCompare(b.fact.predicate));
|
|
1411
|
+
for (const { fact, path: fpath } of sorted) {
|
|
1412
|
+
const from = fact.valid_from ?? "unknown";
|
|
1413
|
+
const to = fact.valid_to ?? "present";
|
|
1414
|
+
lines.push(`- **${fact.predicate}**: ${fact.value} (${from} \u2192 ${to}) \u2014 \`${fpath}\``);
|
|
1415
|
+
}
|
|
1416
|
+
lines.push("");
|
|
1417
|
+
return lines.join("\n");
|
|
1418
|
+
}
|
|
1419
|
+
renderById(facts, events, operations) {
|
|
1420
|
+
const lines = ["# Records by ID\n"];
|
|
1421
|
+
const allRecords = [];
|
|
1422
|
+
for (const { fact, path: p } of facts) {
|
|
1423
|
+
if (fact.id) allRecords.push({ id: fact.id, type: "fact", path: p });
|
|
1424
|
+
}
|
|
1425
|
+
for (const { event, path: p } of events) {
|
|
1426
|
+
if (event.id) allRecords.push({ id: event.id, type: "event", path: p });
|
|
1427
|
+
}
|
|
1428
|
+
for (const { op, path: p } of operations) {
|
|
1429
|
+
if (op.operation_id) allRecords.push({ id: op.operation_id, type: "operation", path: p });
|
|
1430
|
+
}
|
|
1431
|
+
allRecords.sort((a, b) => a.id.localeCompare(b.id));
|
|
1432
|
+
for (const rec of allRecords) {
|
|
1433
|
+
lines.push(`- \`${rec.id}\`: ${rec.type} \u2014 \`${rec.path}\``);
|
|
1434
|
+
}
|
|
1435
|
+
lines.push("");
|
|
1436
|
+
return lines.join("\n");
|
|
1437
|
+
}
|
|
1438
|
+
renderByPredicate(facts) {
|
|
1439
|
+
const lines = ["# Facts by Predicate\n"];
|
|
1440
|
+
const byPred = /* @__PURE__ */ new Map();
|
|
1441
|
+
for (const entry of facts) {
|
|
1442
|
+
const pred = entry.fact.predicate;
|
|
1443
|
+
if (!byPred.has(pred)) byPred.set(pred, []);
|
|
1444
|
+
byPred.get(pred).push(entry);
|
|
1445
|
+
}
|
|
1446
|
+
const sortedPreds = [...byPred.keys()].sort();
|
|
1447
|
+
for (const pred of sortedPreds) {
|
|
1448
|
+
lines.push(`## ${pred}
|
|
1449
|
+
`);
|
|
1450
|
+
const entries = byPred.get(pred).sort((a, b) => a.fact.entity.localeCompare(b.fact.entity));
|
|
1451
|
+
for (const { fact, path: p } of entries) {
|
|
1452
|
+
lines.push(`- ${fact.entity} = ${fact.value} \u2014 \`${p}\``);
|
|
1453
|
+
}
|
|
1454
|
+
lines.push("");
|
|
1455
|
+
}
|
|
1456
|
+
return lines.join("\n");
|
|
1457
|
+
}
|
|
1458
|
+
renderTimeline(events) {
|
|
1459
|
+
const lines = ["# Timeline\n"];
|
|
1460
|
+
const sorted = [...events].sort((a, b) => String(b.event.occurred_at).localeCompare(String(a.event.occurred_at)));
|
|
1461
|
+
for (const { event, path: p } of sorted) {
|
|
1462
|
+
const date = String(event.occurred_at).slice(0, 10);
|
|
1463
|
+
const entities = event.entities?.join(", ") ?? "";
|
|
1464
|
+
lines.push(`- **${date}** ${event.summary}${entities ? ` [${entities}]` : ""} \u2014 \`${p}\``);
|
|
1465
|
+
}
|
|
1466
|
+
lines.push("");
|
|
1467
|
+
return lines.join("\n");
|
|
1468
|
+
}
|
|
1469
|
+
renderContradictions(facts) {
|
|
1470
|
+
const lines = ["# Contradictions\n"];
|
|
1471
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
1472
|
+
for (const entry of facts) {
|
|
1473
|
+
const key = `${entry.fact.entity}/${entry.fact.predicate}`;
|
|
1474
|
+
if (!byKey.has(key)) byKey.set(key, []);
|
|
1475
|
+
byKey.get(key).push(entry);
|
|
1476
|
+
}
|
|
1477
|
+
let found = false;
|
|
1478
|
+
for (const [key, entries] of byKey) {
|
|
1479
|
+
if (entries.length < 2) continue;
|
|
1480
|
+
for (let i = 0; i < entries.length; i++) {
|
|
1481
|
+
for (let j = i + 1; j < entries.length; j++) {
|
|
1482
|
+
const a = entries[i];
|
|
1483
|
+
const b = entries[j];
|
|
1484
|
+
if (a.fact.superseded_by || b.fact.superseded_by) continue;
|
|
1485
|
+
if (String(a.fact.value) === String(b.fact.value)) continue;
|
|
1486
|
+
const aFrom = a.fact.valid_from ?? "0000-01-01";
|
|
1487
|
+
const aTo = a.fact.valid_to ?? "9999-12-31";
|
|
1488
|
+
const bFrom = b.fact.valid_from ?? "0000-01-01";
|
|
1489
|
+
const bTo = b.fact.valid_to ?? "9999-12-31";
|
|
1490
|
+
if (aFrom <= bTo && bFrom <= aTo) {
|
|
1491
|
+
lines.push(`- **${key}**: "${a.fact.value}" vs "${b.fact.value}"`);
|
|
1492
|
+
lines.push(` - \`${a.path}\` vs \`${b.path}\``);
|
|
1493
|
+
found = true;
|
|
1494
|
+
}
|
|
1495
|
+
}
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
if (!found) lines.push("No contradictions found.\n");
|
|
1499
|
+
lines.push("");
|
|
1500
|
+
return lines.join("\n");
|
|
1501
|
+
}
|
|
1502
|
+
renderStale(facts) {
|
|
1503
|
+
const lines = ["# Stale Facts\n"];
|
|
1504
|
+
const today = process.env.MEMORY_TODAY ?? format2(/* @__PURE__ */ new Date(), "yyyy-MM-dd");
|
|
1505
|
+
const todayDate = parseISO2(today);
|
|
1506
|
+
let found = false;
|
|
1507
|
+
for (const { fact, path: p } of facts) {
|
|
1508
|
+
if (!fact.last_reviewed) {
|
|
1509
|
+
lines.push(`- \`${p}\` \u2014 no last_reviewed date`);
|
|
1510
|
+
found = true;
|
|
1511
|
+
continue;
|
|
1512
|
+
}
|
|
1513
|
+
const reviewed = parseISO2(fact.last_reviewed);
|
|
1514
|
+
const days = differenceInDays(todayDate, reviewed);
|
|
1515
|
+
if (days > this.staleDays) {
|
|
1516
|
+
lines.push(`- \`${p}\` \u2014 last reviewed ${fact.last_reviewed} (${days} days ago)`);
|
|
1517
|
+
found = true;
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
if (!found) lines.push("No stale facts found.\n");
|
|
1521
|
+
lines.push("");
|
|
1522
|
+
return lines.join("\n");
|
|
1523
|
+
}
|
|
1524
|
+
async renderGraph() {
|
|
1525
|
+
const lines = ["# Wikilink Graph\n"];
|
|
1526
|
+
const allFiles = await glob4(path6.join(this.vault.paths.memory, "**", "*.md"));
|
|
1527
|
+
const wikilinkRegex = /\[\[([^\]|#]+)(?:#[^\]|]*)?\|?[^\]]*\]\]/g;
|
|
1528
|
+
const edges = [];
|
|
1529
|
+
for (const file of allFiles.sort()) {
|
|
1530
|
+
if (file.startsWith(this.vault.paths.views)) continue;
|
|
1531
|
+
const relPath = this.vault.relativePath(file);
|
|
1532
|
+
try {
|
|
1533
|
+
const content = await fs9.readFile(file, "utf-8");
|
|
1534
|
+
let match;
|
|
1535
|
+
wikilinkRegex.lastIndex = 0;
|
|
1536
|
+
while ((match = wikilinkRegex.exec(content)) !== null) {
|
|
1537
|
+
edges.push({ from: relPath, to: match[1].trim() });
|
|
1538
|
+
}
|
|
1539
|
+
} catch {
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1542
|
+
if (edges.length === 0) {
|
|
1543
|
+
lines.push("No wikilinks found.\n");
|
|
1544
|
+
} else {
|
|
1545
|
+
for (const edge of edges) {
|
|
1546
|
+
lines.push(`- \`${edge.from}\` \u2192 [[${edge.to}]]`);
|
|
1547
|
+
}
|
|
1548
|
+
}
|
|
1549
|
+
lines.push("");
|
|
1550
|
+
return lines.join("\n");
|
|
1551
|
+
}
|
|
1552
|
+
renderInbox(operations) {
|
|
1553
|
+
const lines = ["# Inbox\n"];
|
|
1554
|
+
const proposed = operations.filter((o) => o.op.status === "proposed");
|
|
1555
|
+
if (proposed.length === 0) {
|
|
1556
|
+
lines.push("No pending operations.\n");
|
|
1557
|
+
} else {
|
|
1558
|
+
for (const { op, path: p } of proposed) {
|
|
1559
|
+
lines.push(`- **${op.op}** ${op.target_id ?? ""} (${op.agent_id}) \u2014 \`${p}\``);
|
|
1560
|
+
}
|
|
1561
|
+
}
|
|
1562
|
+
lines.push("");
|
|
1563
|
+
return lines.join("\n");
|
|
1564
|
+
}
|
|
1565
|
+
renderOperations(operations) {
|
|
1566
|
+
const lines = ["# Operations\n"];
|
|
1567
|
+
const sorted = [...operations].sort((a, b) => b.op.created_at.localeCompare(a.op.created_at));
|
|
1568
|
+
if (sorted.length === 0) {
|
|
1569
|
+
lines.push("No operations.\n");
|
|
1570
|
+
} else {
|
|
1571
|
+
for (const { op, path: p } of sorted) {
|
|
1572
|
+
lines.push(`- [${op.status}] **${op.op}** ${op.target_id ?? ""} (${op.agent_id}, ${op.created_at.slice(0, 10)}) \u2014 \`${p}\``);
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
lines.push("");
|
|
1576
|
+
return lines.join("\n");
|
|
1577
|
+
}
|
|
1578
|
+
renderClaims(claims) {
|
|
1579
|
+
const lines = ["# Claims\n"];
|
|
1580
|
+
const active = claims.filter((c) => c.claim.status === "active");
|
|
1581
|
+
if (active.length === 0) {
|
|
1582
|
+
lines.push("No active claims.\n");
|
|
1583
|
+
} else {
|
|
1584
|
+
for (const { claim, path: p } of active) {
|
|
1585
|
+
lines.push(`- **${claim.target_id}** by ${claim.agent_id} (expires ${claim.expires_at}) \u2014 \`${p}\``);
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
lines.push("");
|
|
1589
|
+
return lines.join("\n");
|
|
1590
|
+
}
|
|
1591
|
+
renderConflicts(operations) {
|
|
1592
|
+
const lines = ["# Conflicts\n"];
|
|
1593
|
+
const conflicts = operations.filter((o) => o.op.status === "conflict");
|
|
1594
|
+
if (conflicts.length === 0) {
|
|
1595
|
+
lines.push("No conflicts.\n");
|
|
1596
|
+
} else {
|
|
1597
|
+
for (const { op, path: p } of conflicts) {
|
|
1598
|
+
lines.push(`- **${op.op}** ${op.target_id ?? ""} \u2014 ${op.conflict_reason ?? "unknown reason"}`);
|
|
1599
|
+
lines.push(` \`${p}\``);
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1602
|
+
lines.push("");
|
|
1603
|
+
return lines.join("\n");
|
|
1604
|
+
}
|
|
1605
|
+
};
|
|
1606
|
+
|
|
1607
|
+
// src/core/scaffold.ts
|
|
1608
|
+
import * as fs10 from "fs/promises";
|
|
1609
|
+
import * as path7 from "path";
|
|
1610
|
+
import * as yaml5 from "js-yaml";
|
|
1611
|
+
var DEFAULT_PREDICATES = [
|
|
1612
|
+
"base",
|
|
1613
|
+
"collaborator",
|
|
1614
|
+
"employer",
|
|
1615
|
+
"language",
|
|
1616
|
+
"primary-project",
|
|
1617
|
+
"publication-channel",
|
|
1618
|
+
"role",
|
|
1619
|
+
"tool"
|
|
1620
|
+
];
|
|
1621
|
+
var FACT_SCHEMA = `type: object
|
|
1622
|
+
required: [type, entity, predicate, value, recorded_at]
|
|
1623
|
+
properties:
|
|
1624
|
+
type: { const: fact }
|
|
1625
|
+
id: { pattern: "^[a-z0-9][a-z0-9_-]*$" }
|
|
1626
|
+
entity: { pattern: "^[a-z0-9-]+$" }
|
|
1627
|
+
predicate: { pattern: "^[a-z0-9-]+$" }
|
|
1628
|
+
value: {}
|
|
1629
|
+
valid_from: { format: date, nullable: true }
|
|
1630
|
+
valid_to: { format: date, nullable: true }
|
|
1631
|
+
recorded_at: { format: date-time }
|
|
1632
|
+
confidence: { enum: [high, medium, low] }
|
|
1633
|
+
sources: { type: array }
|
|
1634
|
+
last_reviewed: { format: date }
|
|
1635
|
+
superseded_by: { type: string }
|
|
1636
|
+
tags: { type: array }
|
|
1637
|
+
decay: { type: object }
|
|
1638
|
+
`;
|
|
1639
|
+
var EVENT_SCHEMA = `type: object
|
|
1640
|
+
required: [type, occurred_at, summary]
|
|
1641
|
+
properties:
|
|
1642
|
+
type: { const: event }
|
|
1643
|
+
id: { pattern: "^[a-z0-9][a-z0-9_-]*$" }
|
|
1644
|
+
occurred_at: { format: date-time }
|
|
1645
|
+
summary: { type: string }
|
|
1646
|
+
entities: { type: array }
|
|
1647
|
+
kind: { enum: [conversation, decision, ingest, action, observation] }
|
|
1648
|
+
sources: { type: array }
|
|
1649
|
+
derived_facts: { type: array }
|
|
1650
|
+
`;
|
|
1651
|
+
var OPERATION_SCHEMA = `type: object
|
|
1652
|
+
required: [type, operation_id, op, agent_id, created_at, status, reason]
|
|
1653
|
+
properties:
|
|
1654
|
+
type: { const: operation }
|
|
1655
|
+
operation_id: { pattern: "^op-[a-z0-9][a-z0-9_-]*$" }
|
|
1656
|
+
op: { enum: [create_fact, update_fact, add_event, archive_fact, review_fact, rename_entity, add_entity, add_predicate] }
|
|
1657
|
+
agent_id: { pattern: "^agent-[a-z0-9-]+-[a-f0-9]{8}$" }
|
|
1658
|
+
created_at: { format: date-time }
|
|
1659
|
+
target_id: { type: string }
|
|
1660
|
+
target_path: { type: string }
|
|
1661
|
+
precondition_hash: { pattern: "^sha256:[0-9a-f]{64}$", nullable: true }
|
|
1662
|
+
status: { enum: [proposed, validated, applied, rejected, conflict, superseded] }
|
|
1663
|
+
reason: { type: string }
|
|
1664
|
+
sources: { type: array }
|
|
1665
|
+
payload: { type: object }
|
|
1666
|
+
`;
|
|
1667
|
+
var CLAIM_SCHEMA = `type: object
|
|
1668
|
+
required: [type, target_id, operation_id, agent_id, created_at, expires_at, heartbeat_at]
|
|
1669
|
+
properties:
|
|
1670
|
+
type: { const: claim }
|
|
1671
|
+
target_id: { pattern: "^[a-z0-9][a-z0-9_-]*$" }
|
|
1672
|
+
operation_id: { pattern: "^op-[a-z0-9][a-z0-9_-]*$" }
|
|
1673
|
+
agent_id: { pattern: "^agent-[a-z0-9-]+-[a-f0-9]{8}$" }
|
|
1674
|
+
created_at: { format: date-time }
|
|
1675
|
+
expires_at: { format: date-time }
|
|
1676
|
+
heartbeat_at: { format: date-time }
|
|
1677
|
+
status: { enum: [active, released, broken] }
|
|
1678
|
+
`;
|
|
1679
|
+
var ENTITY_SCHEMA = `type: object
|
|
1680
|
+
required: [type, id, kind]
|
|
1681
|
+
properties:
|
|
1682
|
+
type: { const: entity-index }
|
|
1683
|
+
id: { pattern: "^[a-z0-9-]+$" }
|
|
1684
|
+
kind: { enum: [person, project, org, place, concept, tool] }
|
|
1685
|
+
display: { type: string }
|
|
1686
|
+
aliases: { type: array }
|
|
1687
|
+
`;
|
|
1688
|
+
var DECISION_SCHEMA = `type: object
|
|
1689
|
+
required: [type, id, status, decided_at, title]
|
|
1690
|
+
properties:
|
|
1691
|
+
type: { const: decision }
|
|
1692
|
+
id: { pattern: "^DEC-[0-9]{3}$" }
|
|
1693
|
+
status: { enum: [accepted, pending, deprecated, superseded] }
|
|
1694
|
+
decided_at: { format: date }
|
|
1695
|
+
title: { type: string }
|
|
1696
|
+
entities: { type: array }
|
|
1697
|
+
sources: { type: array }
|
|
1698
|
+
`;
|
|
1699
|
+
var INSIGHT_SCHEMA = `type: object
|
|
1700
|
+
required: [type, title, recorded_at, summary]
|
|
1701
|
+
properties:
|
|
1702
|
+
type: { const: insight }
|
|
1703
|
+
id: { pattern: "^[a-z0-9][a-z0-9_-]*$" }
|
|
1704
|
+
title: { type: string }
|
|
1705
|
+
recorded_at: { format: date-time }
|
|
1706
|
+
summary: { type: string }
|
|
1707
|
+
entities: { type: array }
|
|
1708
|
+
sources: { type: array }
|
|
1709
|
+
confidence: { enum: [high, medium, low] }
|
|
1710
|
+
`;
|
|
1711
|
+
async function scaffoldVault(options) {
|
|
1712
|
+
const { vaultPath } = options;
|
|
1713
|
+
const predicates = options.predicates ?? DEFAULT_PREDICATES;
|
|
1714
|
+
const entities = options.entities ?? [];
|
|
1715
|
+
const created = [];
|
|
1716
|
+
const dirs = [
|
|
1717
|
+
"memory/schema",
|
|
1718
|
+
"memory/facts",
|
|
1719
|
+
"memory/events",
|
|
1720
|
+
"memory/decisions",
|
|
1721
|
+
"memory/insights",
|
|
1722
|
+
"memory/people",
|
|
1723
|
+
"memory/projects",
|
|
1724
|
+
"memory/context",
|
|
1725
|
+
"memory/_views/by-entity",
|
|
1726
|
+
"memory/_inbox",
|
|
1727
|
+
"memory/_claims",
|
|
1728
|
+
"memory/_ops/applied",
|
|
1729
|
+
"memory/_archive",
|
|
1730
|
+
"sources/articles",
|
|
1731
|
+
"sources/notes",
|
|
1732
|
+
"sources/assets"
|
|
1733
|
+
];
|
|
1734
|
+
for (const dir of dirs) {
|
|
1735
|
+
const fullPath = path7.join(vaultPath, dir);
|
|
1736
|
+
await fs10.mkdir(fullPath, { recursive: true });
|
|
1737
|
+
const gitkeep = path7.join(fullPath, ".gitkeep");
|
|
1738
|
+
try {
|
|
1739
|
+
await fs10.access(gitkeep);
|
|
1740
|
+
} catch {
|
|
1741
|
+
await fs10.writeFile(gitkeep, "", "utf-8");
|
|
1742
|
+
}
|
|
1743
|
+
created.push(dir);
|
|
1744
|
+
}
|
|
1745
|
+
const versionPath = path7.join(vaultPath, "memory/schema/version.yaml");
|
|
1746
|
+
await fs10.writeFile(
|
|
1747
|
+
versionPath,
|
|
1748
|
+
yaml5.dump({ spec_version: "3.0", schema_status: "stable", frozen_at: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10) }),
|
|
1749
|
+
"utf-8"
|
|
1750
|
+
);
|
|
1751
|
+
created.push("memory/schema/version.yaml");
|
|
1752
|
+
const predicatesPath = path7.join(vaultPath, "memory/schema/predicates.yaml");
|
|
1753
|
+
await fs10.writeFile(predicatesPath, yaml5.dump(predicates, { flowLevel: -1 }), "utf-8");
|
|
1754
|
+
created.push("memory/schema/predicates.yaml");
|
|
1755
|
+
const schemas = {
|
|
1756
|
+
"fact.schema.yaml": FACT_SCHEMA,
|
|
1757
|
+
"event.schema.yaml": EVENT_SCHEMA,
|
|
1758
|
+
"operation.schema.yaml": OPERATION_SCHEMA,
|
|
1759
|
+
"claim.schema.yaml": CLAIM_SCHEMA,
|
|
1760
|
+
"entity.schema.yaml": ENTITY_SCHEMA,
|
|
1761
|
+
"decision.schema.yaml": DECISION_SCHEMA,
|
|
1762
|
+
"insight.schema.yaml": INSIGHT_SCHEMA
|
|
1763
|
+
};
|
|
1764
|
+
for (const [filename, content] of Object.entries(schemas)) {
|
|
1765
|
+
const filePath = path7.join(vaultPath, "memory/schema", filename);
|
|
1766
|
+
await fs10.writeFile(filePath, content, "utf-8");
|
|
1767
|
+
created.push(`memory/schema/${filename}`);
|
|
1768
|
+
}
|
|
1769
|
+
const entitiesPath = path7.join(vaultPath, "memory/entities.md");
|
|
1770
|
+
const entitiesData = {
|
|
1771
|
+
type: "entity-index",
|
|
1772
|
+
entities
|
|
1773
|
+
};
|
|
1774
|
+
await writeMarkdownFile(
|
|
1775
|
+
entitiesPath,
|
|
1776
|
+
entitiesData,
|
|
1777
|
+
"\n# Entities\n\nCanonical entity list for this vault.\n"
|
|
1778
|
+
);
|
|
1779
|
+
created.push("memory/entities.md");
|
|
1780
|
+
const agentsPath = path7.join(vaultPath, "AGENTS.md");
|
|
1781
|
+
const agentsContent = `# Agent Instructions
|
|
1782
|
+
|
|
1783
|
+
## Memory Protocol (v3 Atomic Markdown Memory)
|
|
1784
|
+
|
|
1785
|
+
This vault uses the v3 Atomic Markdown Memory system.
|
|
1786
|
+
|
|
1787
|
+
### Key Rules
|
|
1788
|
+
- Facts live in \`memory/facts/{entity}/{predicate}.md\` (one fact per file)
|
|
1789
|
+
- Events are append-only in \`memory/events/YYYY-MM-DD/{slug}.md\`
|
|
1790
|
+
- Views in \`memory/_views/\` are generated \u2014 never edit them directly
|
|
1791
|
+
- Schemas in \`memory/schema/\` define allowed types and predicates
|
|
1792
|
+
- All entities must be registered in \`memory/entities.md\`
|
|
1793
|
+
- All predicates must be registered in \`memory/schema/predicates.yaml\`
|
|
1794
|
+
|
|
1795
|
+
### Update Protocol
|
|
1796
|
+
1. To add a fact: use the \`create_fact\` MCP tool (or write to \`memory/facts/\`)
|
|
1797
|
+
2. To record an event: use the \`add_event\` MCP tool
|
|
1798
|
+
3. After writes: run \`lint_vault\` to validate, then \`rebuild_views\` to update views
|
|
1799
|
+
4. For multi-agent: write to \`memory/_inbox/{agent-id}/ops/\` and use \`compact_inbox\`
|
|
1800
|
+
`;
|
|
1801
|
+
await fs10.writeFile(agentsPath, agentsContent, "utf-8");
|
|
1802
|
+
created.push("AGENTS.md");
|
|
1803
|
+
const sourcesReadme = path7.join(vaultPath, "sources/README.md");
|
|
1804
|
+
await fs10.writeFile(
|
|
1805
|
+
sourcesReadme,
|
|
1806
|
+
"# Sources\n\nImmutable inputs. Drop articles, notes, and assets here.\nAgents read but never modify source files.\n",
|
|
1807
|
+
"utf-8"
|
|
1808
|
+
);
|
|
1809
|
+
created.push("sources/README.md");
|
|
1810
|
+
return created;
|
|
1811
|
+
}
|
|
1812
|
+
|
|
1813
|
+
// src/server.ts
|
|
1814
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
1815
|
+
|
|
1816
|
+
// src/tools/read.ts
|
|
1817
|
+
import { z } from "zod";
|
|
1818
|
+
import * as fs11 from "fs/promises";
|
|
1819
|
+
function registerReadTools(server, vault, queryEngine) {
|
|
1820
|
+
server.tool(
|
|
1821
|
+
"read_fact",
|
|
1822
|
+
"Read a specific fact by entity+predicate or by ID. Optionally filter by temporal validity on a given date.",
|
|
1823
|
+
{
|
|
1824
|
+
entity: z.string().optional().describe("Entity ID (e.g. 'elena-voss')"),
|
|
1825
|
+
predicate: z.string().optional().describe("Predicate name (e.g. 'role')"),
|
|
1826
|
+
id: z.string().optional().describe("Stable fact ID (e.g. 'fact-elena-voss-role')"),
|
|
1827
|
+
on_date: z.string().optional().describe("Filter to facts valid on this date (YYYY-MM-DD)")
|
|
1828
|
+
},
|
|
1829
|
+
async (params) => {
|
|
1830
|
+
const facts = await queryEngine.findFacts({
|
|
1831
|
+
entity: params.entity,
|
|
1832
|
+
predicate: params.predicate,
|
|
1833
|
+
id: params.id,
|
|
1834
|
+
onDate: params.on_date
|
|
1835
|
+
});
|
|
1836
|
+
if (facts.length === 0) {
|
|
1837
|
+
return {
|
|
1838
|
+
content: [{ type: "text", text: JSON.stringify({ error: "No matching fact found", code: "NOT_FOUND" }) }],
|
|
1839
|
+
isError: true
|
|
1840
|
+
};
|
|
1841
|
+
}
|
|
1842
|
+
const results = facts.map((f) => ({
|
|
1843
|
+
...f.data,
|
|
1844
|
+
_path: vault.relativePath(f.path),
|
|
1845
|
+
_body: f.content.trim() || void 0
|
|
1846
|
+
}));
|
|
1847
|
+
return {
|
|
1848
|
+
content: [{ type: "text", text: JSON.stringify(results.length === 1 ? results[0] : results, null, 2) }]
|
|
1849
|
+
};
|
|
1850
|
+
}
|
|
1851
|
+
);
|
|
1852
|
+
server.tool(
|
|
1853
|
+
"read_entity",
|
|
1854
|
+
"Get all facts for a given entity. Returns a snapshot of everything known about the entity.",
|
|
1855
|
+
{
|
|
1856
|
+
entity: z.string().describe("Entity ID (e.g. 'elena-voss')"),
|
|
1857
|
+
on_date: z.string().optional().describe("Temporal snapshot date (YYYY-MM-DD)")
|
|
1858
|
+
},
|
|
1859
|
+
async (params) => {
|
|
1860
|
+
const facts = await queryEngine.findFacts({
|
|
1861
|
+
entity: params.entity,
|
|
1862
|
+
onDate: params.on_date
|
|
1863
|
+
});
|
|
1864
|
+
if (facts.length === 0) {
|
|
1865
|
+
return {
|
|
1866
|
+
content: [{ type: "text", text: JSON.stringify({ entity: params.entity, facts: [], message: "No facts found for this entity" }) }]
|
|
1867
|
+
};
|
|
1868
|
+
}
|
|
1869
|
+
const result = {
|
|
1870
|
+
entity: params.entity,
|
|
1871
|
+
on_date: params.on_date ?? "current",
|
|
1872
|
+
facts: facts.map((f) => ({
|
|
1873
|
+
predicate: f.data.predicate,
|
|
1874
|
+
value: f.data.value,
|
|
1875
|
+
valid_from: f.data.valid_from,
|
|
1876
|
+
valid_to: f.data.valid_to,
|
|
1877
|
+
confidence: f.data.confidence,
|
|
1878
|
+
path: vault.relativePath(f.path)
|
|
1879
|
+
}))
|
|
1880
|
+
};
|
|
1881
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
1882
|
+
}
|
|
1883
|
+
);
|
|
1884
|
+
server.tool(
|
|
1885
|
+
"read_event",
|
|
1886
|
+
"Read events filtered by entity, date range, or kind.",
|
|
1887
|
+
{
|
|
1888
|
+
entity: z.string().optional().describe("Filter events involving this entity"),
|
|
1889
|
+
since: z.string().optional().describe("Events after this date (YYYY-MM-DD or ISO 8601)"),
|
|
1890
|
+
until: z.string().optional().describe("Events before this date"),
|
|
1891
|
+
kind: z.string().optional().describe("Event kind: conversation, decision, ingest, action, observation"),
|
|
1892
|
+
limit: z.number().optional().describe("Maximum number of events to return")
|
|
1893
|
+
},
|
|
1894
|
+
async (params) => {
|
|
1895
|
+
const events = await queryEngine.findEvents({
|
|
1896
|
+
entity: params.entity,
|
|
1897
|
+
since: params.since,
|
|
1898
|
+
until: params.until,
|
|
1899
|
+
kind: params.kind,
|
|
1900
|
+
limit: params.limit
|
|
1901
|
+
});
|
|
1902
|
+
const results = events.map((e) => ({
|
|
1903
|
+
...e.data,
|
|
1904
|
+
_path: vault.relativePath(e.path),
|
|
1905
|
+
_body: e.content.trim() || void 0
|
|
1906
|
+
}));
|
|
1907
|
+
return { content: [{ type: "text", text: JSON.stringify(results, null, 2) }] };
|
|
1908
|
+
}
|
|
1909
|
+
);
|
|
1910
|
+
server.tool(
|
|
1911
|
+
"search_memory",
|
|
1912
|
+
"Search facts with flexible filters: text query, entity, predicate, date, confidence, tags.",
|
|
1913
|
+
{
|
|
1914
|
+
query: z.string().optional().describe("Text search in fact values, entity names, and predicates"),
|
|
1915
|
+
entity: z.string().optional().describe("Filter by entity"),
|
|
1916
|
+
predicate: z.string().optional().describe("Filter by predicate"),
|
|
1917
|
+
on_date: z.string().optional().describe("Filter to facts valid on this date (YYYY-MM-DD)"),
|
|
1918
|
+
confidence: z.string().optional().describe("Filter by confidence: high, medium, low"),
|
|
1919
|
+
tags: z.array(z.string()).optional().describe("Filter by tags (any match)"),
|
|
1920
|
+
limit: z.number().optional().describe("Maximum results to return")
|
|
1921
|
+
},
|
|
1922
|
+
async (params) => {
|
|
1923
|
+
const facts = await queryEngine.findFacts({
|
|
1924
|
+
query: params.query,
|
|
1925
|
+
entity: params.entity,
|
|
1926
|
+
predicate: params.predicate,
|
|
1927
|
+
onDate: params.on_date,
|
|
1928
|
+
confidence: params.confidence,
|
|
1929
|
+
tags: params.tags,
|
|
1930
|
+
limit: params.limit
|
|
1931
|
+
});
|
|
1932
|
+
const results = facts.map((f) => ({
|
|
1933
|
+
entity: f.data.entity,
|
|
1934
|
+
predicate: f.data.predicate,
|
|
1935
|
+
value: f.data.value,
|
|
1936
|
+
confidence: f.data.confidence,
|
|
1937
|
+
valid_from: f.data.valid_from,
|
|
1938
|
+
valid_to: f.data.valid_to,
|
|
1939
|
+
path: vault.relativePath(f.path)
|
|
1940
|
+
}));
|
|
1941
|
+
return {
|
|
1942
|
+
content: [{
|
|
1943
|
+
type: "text",
|
|
1944
|
+
text: JSON.stringify({ count: results.length, results }, null, 2)
|
|
1945
|
+
}]
|
|
1946
|
+
};
|
|
1947
|
+
}
|
|
1948
|
+
);
|
|
1949
|
+
server.tool(
|
|
1950
|
+
"get_view",
|
|
1951
|
+
"Read a materialized view. Views are pre-generated summaries of vault data.",
|
|
1952
|
+
{
|
|
1953
|
+
view: z.enum([
|
|
1954
|
+
"by-entity",
|
|
1955
|
+
"by-id",
|
|
1956
|
+
"by-predicate",
|
|
1957
|
+
"timeline",
|
|
1958
|
+
"contradictions",
|
|
1959
|
+
"stale",
|
|
1960
|
+
"graph",
|
|
1961
|
+
"inbox",
|
|
1962
|
+
"operations",
|
|
1963
|
+
"claims",
|
|
1964
|
+
"conflicts"
|
|
1965
|
+
]).describe("View name"),
|
|
1966
|
+
entity: z.string().optional().describe("For by-entity view, the entity ID")
|
|
1967
|
+
},
|
|
1968
|
+
async (params) => {
|
|
1969
|
+
let viewPath;
|
|
1970
|
+
if (params.view === "by-entity" || params.entity) {
|
|
1971
|
+
if (!params.entity) {
|
|
1972
|
+
return {
|
|
1973
|
+
content: [{ type: "text", text: JSON.stringify({ error: "entity parameter required for by-entity view" }) }],
|
|
1974
|
+
isError: true
|
|
1975
|
+
};
|
|
1976
|
+
}
|
|
1977
|
+
viewPath = vault.entityViewPath(params.entity);
|
|
1978
|
+
} else {
|
|
1979
|
+
viewPath = vault.viewPath(params.view);
|
|
1980
|
+
}
|
|
1981
|
+
try {
|
|
1982
|
+
const content = await fs11.readFile(viewPath, "utf-8");
|
|
1983
|
+
return { content: [{ type: "text", text: content }] };
|
|
1984
|
+
} catch {
|
|
1985
|
+
return {
|
|
1986
|
+
content: [{ type: "text", text: JSON.stringify({ error: `View not found: ${params.view}. Run rebuild_views to generate.`, code: "VIEW_NOT_FOUND" }) }],
|
|
1987
|
+
isError: true
|
|
1988
|
+
};
|
|
1989
|
+
}
|
|
1990
|
+
}
|
|
1991
|
+
);
|
|
1992
|
+
}
|
|
1993
|
+
|
|
1994
|
+
// src/tools/write.ts
|
|
1995
|
+
import { z as z2 } from "zod";
|
|
1996
|
+
function registerWriteTools(server, vault, operationManager, entities, predicates, config) {
|
|
1997
|
+
server.tool(
|
|
1998
|
+
"create_fact",
|
|
1999
|
+
"Create a new atomic fact about an entity. In direct mode, writes immediately. In inbox mode, creates an operation envelope for later application.",
|
|
2000
|
+
{
|
|
2001
|
+
entity: z2.string().describe("Entity ID (must be registered in entities.md)"),
|
|
2002
|
+
predicate: z2.string().describe("Predicate (must be in predicates.yaml)"),
|
|
2003
|
+
value: z2.string().describe("The fact value"),
|
|
2004
|
+
confidence: z2.enum(["high", "medium", "low"]).optional().describe("Confidence level"),
|
|
2005
|
+
valid_from: z2.string().nullable().optional().describe("Date when this fact became true (YYYY-MM-DD)"),
|
|
2006
|
+
valid_to: z2.string().nullable().optional().describe("Date when this fact stops being true (YYYY-MM-DD)"),
|
|
2007
|
+
sources: z2.array(z2.string()).optional().describe("Source file paths (relative to vault root)"),
|
|
2008
|
+
tags: z2.array(z2.string()).optional().describe("Tags for categorization"),
|
|
2009
|
+
reason: z2.string().describe("Why this fact is being recorded")
|
|
2010
|
+
},
|
|
2011
|
+
async (params) => {
|
|
2012
|
+
if (!await entities.exists(params.entity)) {
|
|
2013
|
+
return {
|
|
2014
|
+
content: [{ type: "text", text: JSON.stringify({ error: `Unknown entity '${params.entity}'. Use add_entity first.`, code: "ENTITY_NOT_FOUND" }) }],
|
|
2015
|
+
isError: true
|
|
2016
|
+
};
|
|
2017
|
+
}
|
|
2018
|
+
if (!await predicates.exists(params.predicate)) {
|
|
2019
|
+
return {
|
|
2020
|
+
content: [{ type: "text", text: JSON.stringify({ error: `Unknown predicate '${params.predicate}'. Use add_predicate first.`, code: "PREDICATE_NOT_FOUND" }) }],
|
|
2021
|
+
isError: true
|
|
2022
|
+
};
|
|
2023
|
+
}
|
|
2024
|
+
try {
|
|
2025
|
+
if (config.writeMode === "direct") {
|
|
2026
|
+
const result = await operationManager.createFactDirect({
|
|
2027
|
+
entity: params.entity,
|
|
2028
|
+
predicate: params.predicate,
|
|
2029
|
+
value: params.value,
|
|
2030
|
+
confidence: params.confidence,
|
|
2031
|
+
valid_from: params.valid_from,
|
|
2032
|
+
valid_to: params.valid_to,
|
|
2033
|
+
sources: params.sources,
|
|
2034
|
+
tags: params.tags,
|
|
2035
|
+
reason: params.reason
|
|
2036
|
+
});
|
|
2037
|
+
return {
|
|
2038
|
+
content: [{ type: "text", text: JSON.stringify({ success: true, mode: "direct", ...result }, null, 2) }]
|
|
2039
|
+
};
|
|
2040
|
+
} else {
|
|
2041
|
+
const result = await operationManager.createFactEnvelope({
|
|
2042
|
+
entity: params.entity,
|
|
2043
|
+
predicate: params.predicate,
|
|
2044
|
+
value: params.value,
|
|
2045
|
+
confidence: params.confidence,
|
|
2046
|
+
valid_from: params.valid_from,
|
|
2047
|
+
valid_to: params.valid_to,
|
|
2048
|
+
sources: params.sources,
|
|
2049
|
+
tags: params.tags,
|
|
2050
|
+
reason: params.reason
|
|
2051
|
+
});
|
|
2052
|
+
return {
|
|
2053
|
+
content: [{ type: "text", text: JSON.stringify({ success: true, mode: "inbox", ...result }, null, 2) }]
|
|
2054
|
+
};
|
|
2055
|
+
}
|
|
2056
|
+
} catch (err) {
|
|
2057
|
+
return {
|
|
2058
|
+
content: [{ type: "text", text: JSON.stringify({ error: err instanceof Error ? err.message : String(err), code: "OPERATION_FAILED" }) }],
|
|
2059
|
+
isError: true
|
|
2060
|
+
};
|
|
2061
|
+
}
|
|
2062
|
+
}
|
|
2063
|
+
);
|
|
2064
|
+
server.tool(
|
|
2065
|
+
"update_fact",
|
|
2066
|
+
"Update an existing fact. Computes precondition hash for safety. Specify the fact by ID or path.",
|
|
2067
|
+
{
|
|
2068
|
+
id: z2.string().optional().describe("Stable fact ID"),
|
|
2069
|
+
path: z2.string().optional().describe("Relative path to the fact file"),
|
|
2070
|
+
value: z2.string().optional().describe("New value"),
|
|
2071
|
+
valid_to: z2.string().nullable().optional().describe("Update valid_to date"),
|
|
2072
|
+
confidence: z2.enum(["high", "medium", "low"]).optional().describe("Update confidence"),
|
|
2073
|
+
tags: z2.array(z2.string()).optional().describe("Replace tags"),
|
|
2074
|
+
reason: z2.string().describe("Why this fact is being updated")
|
|
2075
|
+
},
|
|
2076
|
+
async (params) => {
|
|
2077
|
+
if (!params.id && !params.path) {
|
|
2078
|
+
return {
|
|
2079
|
+
content: [{ type: "text", text: JSON.stringify({ error: "Either 'id' or 'path' is required", code: "INVALID_PARAMS" }) }],
|
|
2080
|
+
isError: true
|
|
2081
|
+
};
|
|
2082
|
+
}
|
|
2083
|
+
try {
|
|
2084
|
+
if (config.writeMode === "direct") {
|
|
2085
|
+
const result = await operationManager.updateFactDirect({
|
|
2086
|
+
targetId: params.id,
|
|
2087
|
+
targetPath: params.path,
|
|
2088
|
+
value: params.value,
|
|
2089
|
+
valid_to: params.valid_to,
|
|
2090
|
+
confidence: params.confidence,
|
|
2091
|
+
tags: params.tags,
|
|
2092
|
+
reason: params.reason
|
|
2093
|
+
});
|
|
2094
|
+
return {
|
|
2095
|
+
content: [{ type: "text", text: JSON.stringify({ success: true, mode: "direct", ...result }, null, 2) }]
|
|
2096
|
+
};
|
|
2097
|
+
} else {
|
|
2098
|
+
const result = await operationManager.updateFactEnvelope({
|
|
2099
|
+
targetId: params.id,
|
|
2100
|
+
targetPath: params.path,
|
|
2101
|
+
value: params.value,
|
|
2102
|
+
valid_to: params.valid_to,
|
|
2103
|
+
confidence: params.confidence,
|
|
2104
|
+
tags: params.tags,
|
|
2105
|
+
reason: params.reason
|
|
2106
|
+
});
|
|
2107
|
+
return {
|
|
2108
|
+
content: [{ type: "text", text: JSON.stringify({ success: true, mode: "inbox", ...result }, null, 2) }]
|
|
2109
|
+
};
|
|
2110
|
+
}
|
|
2111
|
+
} catch (err) {
|
|
2112
|
+
return {
|
|
2113
|
+
content: [{ type: "text", text: JSON.stringify({ error: err instanceof Error ? err.message : String(err), code: "OPERATION_FAILED" }) }],
|
|
2114
|
+
isError: true
|
|
2115
|
+
};
|
|
2116
|
+
}
|
|
2117
|
+
}
|
|
2118
|
+
);
|
|
2119
|
+
server.tool(
|
|
2120
|
+
"add_event",
|
|
2121
|
+
"Record an episodic event. Events are append-only and never edited after creation.",
|
|
2122
|
+
{
|
|
2123
|
+
summary: z2.string().describe("Short summary of what happened"),
|
|
2124
|
+
entities: z2.array(z2.string()).optional().describe("Entity IDs involved in this event"),
|
|
2125
|
+
kind: z2.enum(["conversation", "decision", "ingest", "action", "observation"]).optional().describe("Event kind"),
|
|
2126
|
+
sources: z2.array(z2.string()).optional().describe("Source file paths"),
|
|
2127
|
+
body: z2.string().optional().describe("Extended markdown body for the event"),
|
|
2128
|
+
reason: z2.string().optional().describe("Why this event is being recorded (for inbox mode)")
|
|
2129
|
+
},
|
|
2130
|
+
async (params) => {
|
|
2131
|
+
try {
|
|
2132
|
+
if (config.writeMode === "direct") {
|
|
2133
|
+
const result = await operationManager.addEventDirect({
|
|
2134
|
+
summary: params.summary,
|
|
2135
|
+
entities: params.entities,
|
|
2136
|
+
kind: params.kind,
|
|
2137
|
+
sources: params.sources,
|
|
2138
|
+
body: params.body
|
|
2139
|
+
});
|
|
2140
|
+
return {
|
|
2141
|
+
content: [{ type: "text", text: JSON.stringify({ success: true, mode: "direct", ...result }, null, 2) }]
|
|
2142
|
+
};
|
|
2143
|
+
} else {
|
|
2144
|
+
const result = await operationManager.addEventEnvelope({
|
|
2145
|
+
summary: params.summary,
|
|
2146
|
+
entities: params.entities,
|
|
2147
|
+
kind: params.kind,
|
|
2148
|
+
sources: params.sources,
|
|
2149
|
+
body: params.body,
|
|
2150
|
+
reason: params.reason
|
|
2151
|
+
});
|
|
2152
|
+
return {
|
|
2153
|
+
content: [{ type: "text", text: JSON.stringify({ success: true, mode: "inbox", ...result }, null, 2) }]
|
|
2154
|
+
};
|
|
2155
|
+
}
|
|
2156
|
+
} catch (err) {
|
|
2157
|
+
return {
|
|
2158
|
+
content: [{ type: "text", text: JSON.stringify({ error: err instanceof Error ? err.message : String(err), code: "OPERATION_FAILED" }) }],
|
|
2159
|
+
isError: true
|
|
2160
|
+
};
|
|
2161
|
+
}
|
|
2162
|
+
}
|
|
2163
|
+
);
|
|
2164
|
+
server.tool(
|
|
2165
|
+
"archive_fact",
|
|
2166
|
+
"Archive a fact (move to _archive/). Use when a fact is no longer current.",
|
|
2167
|
+
{
|
|
2168
|
+
id: z2.string().optional().describe("Stable fact ID"),
|
|
2169
|
+
path: z2.string().optional().describe("Relative path to the fact file"),
|
|
2170
|
+
reason: z2.string().describe("Why this fact is being archived")
|
|
2171
|
+
},
|
|
2172
|
+
async (params) => {
|
|
2173
|
+
if (!params.id && !params.path) {
|
|
2174
|
+
return {
|
|
2175
|
+
content: [{ type: "text", text: JSON.stringify({ error: "Either 'id' or 'path' is required", code: "INVALID_PARAMS" }) }],
|
|
2176
|
+
isError: true
|
|
2177
|
+
};
|
|
2178
|
+
}
|
|
2179
|
+
try {
|
|
2180
|
+
const result = await operationManager.archiveFactDirect({
|
|
2181
|
+
targetId: params.id,
|
|
2182
|
+
targetPath: params.path,
|
|
2183
|
+
reason: params.reason
|
|
2184
|
+
});
|
|
2185
|
+
return {
|
|
2186
|
+
content: [{ type: "text", text: JSON.stringify({ success: true, ...result }, null, 2) }]
|
|
2187
|
+
};
|
|
2188
|
+
} catch (err) {
|
|
2189
|
+
return {
|
|
2190
|
+
content: [{ type: "text", text: JSON.stringify({ error: err instanceof Error ? err.message : String(err), code: "OPERATION_FAILED" }) }],
|
|
2191
|
+
isError: true
|
|
2192
|
+
};
|
|
2193
|
+
}
|
|
2194
|
+
}
|
|
2195
|
+
);
|
|
2196
|
+
}
|
|
2197
|
+
|
|
2198
|
+
// src/tools/maintenance.ts
|
|
2199
|
+
import { z as z3 } from "zod";
|
|
2200
|
+
import { format as format3 } from "date-fns";
|
|
2201
|
+
function registerMaintenanceTools(server, vault, validator, viewGenerator, operationManager, config) {
|
|
2202
|
+
server.tool(
|
|
2203
|
+
"lint_vault",
|
|
2204
|
+
"Run full schema validation on the vault. Checks version, schemas, entities, predicates, temporal validity, wikilinks, contradictions, and duplicate IDs.",
|
|
2205
|
+
{},
|
|
2206
|
+
async () => {
|
|
2207
|
+
try {
|
|
2208
|
+
const findings = await validator.lint();
|
|
2209
|
+
const errors = findings.filter((f) => f.level === "ERROR");
|
|
2210
|
+
const warnings = findings.filter((f) => f.level === "WARN");
|
|
2211
|
+
const passed = errors.length === 0;
|
|
2212
|
+
return {
|
|
2213
|
+
content: [{
|
|
2214
|
+
type: "text",
|
|
2215
|
+
text: JSON.stringify({
|
|
2216
|
+
passed,
|
|
2217
|
+
errors: errors.length,
|
|
2218
|
+
warnings: warnings.length,
|
|
2219
|
+
findings
|
|
2220
|
+
}, null, 2)
|
|
2221
|
+
}]
|
|
2222
|
+
};
|
|
2223
|
+
} catch (err) {
|
|
2224
|
+
return {
|
|
2225
|
+
content: [{ type: "text", text: JSON.stringify({ error: err instanceof Error ? err.message : String(err), code: "LINT_FAILED" }) }],
|
|
2226
|
+
isError: true
|
|
2227
|
+
};
|
|
2228
|
+
}
|
|
2229
|
+
}
|
|
2230
|
+
);
|
|
2231
|
+
server.tool(
|
|
2232
|
+
"rebuild_views",
|
|
2233
|
+
"Regenerate all materialized views in memory/_views/. Views are derived from facts, events, and operations.",
|
|
2234
|
+
{},
|
|
2235
|
+
async () => {
|
|
2236
|
+
try {
|
|
2237
|
+
const written = await viewGenerator.rebuildAll();
|
|
2238
|
+
return {
|
|
2239
|
+
content: [{
|
|
2240
|
+
type: "text",
|
|
2241
|
+
text: JSON.stringify({ success: true, files_written: written.length, files: written }, null, 2)
|
|
2242
|
+
}]
|
|
2243
|
+
};
|
|
2244
|
+
} catch (err) {
|
|
2245
|
+
return {
|
|
2246
|
+
content: [{ type: "text", text: JSON.stringify({ error: err instanceof Error ? err.message : String(err), code: "REBUILD_FAILED" }) }],
|
|
2247
|
+
isError: true
|
|
2248
|
+
};
|
|
2249
|
+
}
|
|
2250
|
+
}
|
|
2251
|
+
);
|
|
2252
|
+
server.tool(
|
|
2253
|
+
"compact_inbox",
|
|
2254
|
+
"Apply proposed operations from the inbox. Validates precondition hashes and claim conflicts before applying.",
|
|
2255
|
+
{
|
|
2256
|
+
auto_apply: z3.boolean().optional().describe("Automatically apply valid operations (default: true)")
|
|
2257
|
+
},
|
|
2258
|
+
async (params) => {
|
|
2259
|
+
try {
|
|
2260
|
+
const result = await operationManager.compact(params.auto_apply ?? true);
|
|
2261
|
+
if (result.applied > 0) {
|
|
2262
|
+
await viewGenerator.rebuildAll();
|
|
2263
|
+
}
|
|
2264
|
+
return {
|
|
2265
|
+
content: [{
|
|
2266
|
+
type: "text",
|
|
2267
|
+
text: JSON.stringify({
|
|
2268
|
+
success: true,
|
|
2269
|
+
applied: result.applied,
|
|
2270
|
+
conflicts: result.conflicts,
|
|
2271
|
+
archived: result.archived,
|
|
2272
|
+
errors: result.errors.length > 0 ? result.errors : void 0
|
|
2273
|
+
}, null, 2)
|
|
2274
|
+
}]
|
|
2275
|
+
};
|
|
2276
|
+
} catch (err) {
|
|
2277
|
+
return {
|
|
2278
|
+
content: [{ type: "text", text: JSON.stringify({ error: err instanceof Error ? err.message : String(err), code: "COMPACT_FAILED" }) }],
|
|
2279
|
+
isError: true
|
|
2280
|
+
};
|
|
2281
|
+
}
|
|
2282
|
+
}
|
|
2283
|
+
);
|
|
2284
|
+
server.tool(
|
|
2285
|
+
"reflect",
|
|
2286
|
+
"Create a reflection/observation event. Records a session summary or insight as an operation envelope.",
|
|
2287
|
+
{
|
|
2288
|
+
summary: z3.string().describe("Reflection summary text"),
|
|
2289
|
+
entities: z3.array(z3.string()).optional().describe("Entities mentioned in the reflection")
|
|
2290
|
+
},
|
|
2291
|
+
async (params) => {
|
|
2292
|
+
try {
|
|
2293
|
+
const operationId = generateOperationId();
|
|
2294
|
+
const now = /* @__PURE__ */ new Date();
|
|
2295
|
+
const dateStr = format3(now, "yyyy-MM-dd");
|
|
2296
|
+
const slug = slugify(params.summary);
|
|
2297
|
+
const data = {
|
|
2298
|
+
type: "operation",
|
|
2299
|
+
operation_id: operationId,
|
|
2300
|
+
op: "add_event",
|
|
2301
|
+
agent_id: config.agentId,
|
|
2302
|
+
created_at: now.toISOString(),
|
|
2303
|
+
target_id: generateEventId(dateStr, slug),
|
|
2304
|
+
target_path: `memory/events/${dateStr}/${slug}.md`,
|
|
2305
|
+
precondition_hash: null,
|
|
2306
|
+
status: "proposed",
|
|
2307
|
+
reason: "Session reflection",
|
|
2308
|
+
sources: [],
|
|
2309
|
+
payload: {
|
|
2310
|
+
type: "event",
|
|
2311
|
+
occurred_at: now.toISOString(),
|
|
2312
|
+
summary: params.summary,
|
|
2313
|
+
entities: params.entities ?? [],
|
|
2314
|
+
kind: "observation",
|
|
2315
|
+
sources: []
|
|
2316
|
+
}
|
|
2317
|
+
};
|
|
2318
|
+
const opPath = vault.inboxOpPath(config.agentId, operationId);
|
|
2319
|
+
await writeMarkdownFile(opPath, data, `
|
|
2320
|
+
# Reflection
|
|
2321
|
+
|
|
2322
|
+
${params.summary}
|
|
2323
|
+
`);
|
|
2324
|
+
return {
|
|
2325
|
+
content: [{
|
|
2326
|
+
type: "text",
|
|
2327
|
+
text: JSON.stringify({
|
|
2328
|
+
success: true,
|
|
2329
|
+
operationId,
|
|
2330
|
+
path: vault.relativePath(opPath),
|
|
2331
|
+
message: "Reflection created as operation envelope. Run compact_inbox to apply."
|
|
2332
|
+
}, null, 2)
|
|
2333
|
+
}]
|
|
2334
|
+
};
|
|
2335
|
+
} catch (err) {
|
|
2336
|
+
return {
|
|
2337
|
+
content: [{ type: "text", text: JSON.stringify({ error: err instanceof Error ? err.message : String(err), code: "REFLECT_FAILED" }) }],
|
|
2338
|
+
isError: true
|
|
2339
|
+
};
|
|
2340
|
+
}
|
|
2341
|
+
}
|
|
2342
|
+
);
|
|
2343
|
+
}
|
|
2344
|
+
|
|
2345
|
+
// src/tools/schema.ts
|
|
2346
|
+
import { z as z4 } from "zod";
|
|
2347
|
+
import * as fs12 from "fs/promises";
|
|
2348
|
+
function registerSchemaTools(server, vault, entities, predicates) {
|
|
2349
|
+
server.tool(
|
|
2350
|
+
"list_entities",
|
|
2351
|
+
"List all registered entities in the vault.",
|
|
2352
|
+
{
|
|
2353
|
+
kind: z4.enum(["person", "project", "org", "place", "concept", "tool"]).optional().describe("Filter by entity kind")
|
|
2354
|
+
},
|
|
2355
|
+
async (params) => {
|
|
2356
|
+
const all = params.kind ? await entities.getByKind(params.kind) : await entities.getAll();
|
|
2357
|
+
return {
|
|
2358
|
+
content: [{ type: "text", text: JSON.stringify({ count: all.length, entities: all }, null, 2) }]
|
|
2359
|
+
};
|
|
2360
|
+
}
|
|
2361
|
+
);
|
|
2362
|
+
server.tool(
|
|
2363
|
+
"list_predicates",
|
|
2364
|
+
"List all allowed predicates defined in the vault schema.",
|
|
2365
|
+
{},
|
|
2366
|
+
async () => {
|
|
2367
|
+
const all = await predicates.getAll();
|
|
2368
|
+
return {
|
|
2369
|
+
content: [{ type: "text", text: JSON.stringify({ count: all.length, predicates: all }, null, 2) }]
|
|
2370
|
+
};
|
|
2371
|
+
}
|
|
2372
|
+
);
|
|
2373
|
+
server.tool(
|
|
2374
|
+
"add_entity",
|
|
2375
|
+
"Register a new entity in the vault's entity index.",
|
|
2376
|
+
{
|
|
2377
|
+
id: z4.string().regex(/^[a-z0-9-]+$/).describe("Entity ID in kebab-case (e.g. 'john-doe')"),
|
|
2378
|
+
kind: z4.enum(["person", "project", "org", "place", "concept", "tool"]).describe("Entity kind"),
|
|
2379
|
+
display: z4.string().describe("Display name (e.g. 'John Doe')"),
|
|
2380
|
+
aliases: z4.array(z4.string()).optional().describe("Alternative names")
|
|
2381
|
+
},
|
|
2382
|
+
async (params) => {
|
|
2383
|
+
try {
|
|
2384
|
+
const updated = await entities.add({
|
|
2385
|
+
id: params.id,
|
|
2386
|
+
kind: params.kind,
|
|
2387
|
+
display: params.display,
|
|
2388
|
+
aliases: params.aliases
|
|
2389
|
+
});
|
|
2390
|
+
return {
|
|
2391
|
+
content: [{ type: "text", text: JSON.stringify({ success: true, entity: params.id, total: updated.length }, null, 2) }]
|
|
2392
|
+
};
|
|
2393
|
+
} catch (err) {
|
|
2394
|
+
return {
|
|
2395
|
+
content: [{ type: "text", text: JSON.stringify({ error: err instanceof Error ? err.message : String(err), code: "ADD_ENTITY_FAILED" }) }],
|
|
2396
|
+
isError: true
|
|
2397
|
+
};
|
|
2398
|
+
}
|
|
2399
|
+
}
|
|
2400
|
+
);
|
|
2401
|
+
server.tool(
|
|
2402
|
+
"add_predicate",
|
|
2403
|
+
"Register a new predicate in the vault schema.",
|
|
2404
|
+
{
|
|
2405
|
+
id: z4.string().regex(/^[a-z0-9-]+$/).describe("Predicate ID in kebab-case (e.g. 'favorite-color')")
|
|
2406
|
+
},
|
|
2407
|
+
async (params) => {
|
|
2408
|
+
try {
|
|
2409
|
+
const updated = await predicates.add(params.id);
|
|
2410
|
+
return {
|
|
2411
|
+
content: [{ type: "text", text: JSON.stringify({ success: true, predicate: params.id, total: updated.length }, null, 2) }]
|
|
2412
|
+
};
|
|
2413
|
+
} catch (err) {
|
|
2414
|
+
return {
|
|
2415
|
+
content: [{ type: "text", text: JSON.stringify({ error: err instanceof Error ? err.message : String(err), code: "ADD_PREDICATE_FAILED" }) }],
|
|
2416
|
+
isError: true
|
|
2417
|
+
};
|
|
2418
|
+
}
|
|
2419
|
+
}
|
|
2420
|
+
);
|
|
2421
|
+
server.tool(
|
|
2422
|
+
"get_schema",
|
|
2423
|
+
"Get the YAML schema definition for a given memory type.",
|
|
2424
|
+
{
|
|
2425
|
+
type: z4.enum(["fact", "event", "operation", "claim", "entity", "decision", "insight"]).describe("Schema type to retrieve")
|
|
2426
|
+
},
|
|
2427
|
+
async (params) => {
|
|
2428
|
+
const schemaFile = `${params.type}.schema.yaml`;
|
|
2429
|
+
const schemaPath = `${vault.paths.schema}/${schemaFile}`;
|
|
2430
|
+
try {
|
|
2431
|
+
const content = await fs12.readFile(schemaPath, "utf-8");
|
|
2432
|
+
return {
|
|
2433
|
+
content: [{ type: "text", text: content }]
|
|
2434
|
+
};
|
|
2435
|
+
} catch {
|
|
2436
|
+
return {
|
|
2437
|
+
content: [{ type: "text", text: JSON.stringify({ error: `Schema not found: ${schemaFile}`, code: "SCHEMA_NOT_FOUND" }) }],
|
|
2438
|
+
isError: true
|
|
2439
|
+
};
|
|
2440
|
+
}
|
|
2441
|
+
}
|
|
2442
|
+
);
|
|
2443
|
+
}
|
|
2444
|
+
|
|
2445
|
+
// src/tools/vault.ts
|
|
2446
|
+
import { z as z5 } from "zod";
|
|
2447
|
+
import { glob as glob5 } from "glob";
|
|
2448
|
+
import * as path8 from "path";
|
|
2449
|
+
function registerVaultTools(server, vault) {
|
|
2450
|
+
server.tool(
|
|
2451
|
+
"vault_init",
|
|
2452
|
+
"Scaffold a new v3 Atomic Markdown Memory vault with all required directories, schemas, and configuration.",
|
|
2453
|
+
{
|
|
2454
|
+
path: z5.string().optional().describe("Vault path (defaults to configured vault path)"),
|
|
2455
|
+
entities: z5.array(
|
|
2456
|
+
z5.object({
|
|
2457
|
+
id: z5.string(),
|
|
2458
|
+
kind: z5.enum(["person", "project", "org", "place", "concept", "tool"]),
|
|
2459
|
+
display: z5.string(),
|
|
2460
|
+
aliases: z5.array(z5.string()).optional()
|
|
2461
|
+
})
|
|
2462
|
+
).optional().describe("Initial entities to register"),
|
|
2463
|
+
predicates: z5.array(z5.string()).optional().describe("Initial predicates (defaults to standard set)")
|
|
2464
|
+
},
|
|
2465
|
+
async (params) => {
|
|
2466
|
+
const vaultPath = params.path ?? vault.paths.root;
|
|
2467
|
+
try {
|
|
2468
|
+
const created = await scaffoldVault({
|
|
2469
|
+
vaultPath,
|
|
2470
|
+
entities: params.entities?.map((e) => ({
|
|
2471
|
+
id: e.id,
|
|
2472
|
+
kind: e.kind,
|
|
2473
|
+
display: e.display,
|
|
2474
|
+
aliases: e.aliases
|
|
2475
|
+
})),
|
|
2476
|
+
predicates: params.predicates
|
|
2477
|
+
});
|
|
2478
|
+
return {
|
|
2479
|
+
content: [{
|
|
2480
|
+
type: "text",
|
|
2481
|
+
text: JSON.stringify({ success: true, vault_path: vaultPath, created_count: created.length, created }, null, 2)
|
|
2482
|
+
}]
|
|
2483
|
+
};
|
|
2484
|
+
} catch (err) {
|
|
2485
|
+
return {
|
|
2486
|
+
content: [{ type: "text", text: JSON.stringify({ error: err instanceof Error ? err.message : String(err), code: "INIT_FAILED" }) }],
|
|
2487
|
+
isError: true
|
|
2488
|
+
};
|
|
2489
|
+
}
|
|
2490
|
+
}
|
|
2491
|
+
);
|
|
2492
|
+
server.tool(
|
|
2493
|
+
"vault_status",
|
|
2494
|
+
"Get vault health overview: counts, version, pending operations, and last modification info.",
|
|
2495
|
+
{},
|
|
2496
|
+
async () => {
|
|
2497
|
+
try {
|
|
2498
|
+
await vault.assertValid();
|
|
2499
|
+
const factFiles = await glob5(path8.join(vault.paths.facts, "**", "*.md"));
|
|
2500
|
+
const eventFiles = await glob5(path8.join(vault.paths.events, "**", "*.md"));
|
|
2501
|
+
const inboxFiles = await glob5(path8.join(vault.paths.inbox, "**", "ops", "*.md"));
|
|
2502
|
+
const appliedFiles = await glob5(path8.join(vault.paths.opsApplied, "*.md"));
|
|
2503
|
+
const archiveFiles = await glob5(path8.join(vault.paths.archive, "**", "*.md"));
|
|
2504
|
+
let pending = 0;
|
|
2505
|
+
for (const file of inboxFiles) {
|
|
2506
|
+
try {
|
|
2507
|
+
const parsed = await parseMarkdownFile(file);
|
|
2508
|
+
if (parsed.data.status === "proposed") pending++;
|
|
2509
|
+
} catch {
|
|
2510
|
+
}
|
|
2511
|
+
}
|
|
2512
|
+
return {
|
|
2513
|
+
content: [{
|
|
2514
|
+
type: "text",
|
|
2515
|
+
text: JSON.stringify({
|
|
2516
|
+
valid: true,
|
|
2517
|
+
vault_path: vault.paths.root,
|
|
2518
|
+
counts: {
|
|
2519
|
+
facts: factFiles.length,
|
|
2520
|
+
events: eventFiles.length,
|
|
2521
|
+
pending_operations: pending,
|
|
2522
|
+
applied_operations: appliedFiles.length,
|
|
2523
|
+
archived_facts: archiveFiles.length
|
|
2524
|
+
}
|
|
2525
|
+
}, null, 2)
|
|
2526
|
+
}]
|
|
2527
|
+
};
|
|
2528
|
+
} catch (err) {
|
|
2529
|
+
return {
|
|
2530
|
+
content: [{
|
|
2531
|
+
type: "text",
|
|
2532
|
+
text: JSON.stringify({
|
|
2533
|
+
valid: false,
|
|
2534
|
+
error: err instanceof Error ? err.message : String(err)
|
|
2535
|
+
}, null, 2)
|
|
2536
|
+
}],
|
|
2537
|
+
isError: true
|
|
2538
|
+
};
|
|
2539
|
+
}
|
|
2540
|
+
}
|
|
2541
|
+
);
|
|
2542
|
+
}
|
|
2543
|
+
|
|
2544
|
+
// src/tools/index.ts
|
|
2545
|
+
function registerAllTools(server, services) {
|
|
2546
|
+
registerReadTools(server, services.vault, services.queryEngine);
|
|
2547
|
+
registerWriteTools(
|
|
2548
|
+
server,
|
|
2549
|
+
services.vault,
|
|
2550
|
+
services.operationManager,
|
|
2551
|
+
services.entities,
|
|
2552
|
+
services.predicates,
|
|
2553
|
+
services.config
|
|
2554
|
+
);
|
|
2555
|
+
registerMaintenanceTools(
|
|
2556
|
+
server,
|
|
2557
|
+
services.vault,
|
|
2558
|
+
services.validator,
|
|
2559
|
+
services.viewGenerator,
|
|
2560
|
+
services.operationManager,
|
|
2561
|
+
services.config
|
|
2562
|
+
);
|
|
2563
|
+
registerSchemaTools(server, services.vault, services.entities, services.predicates);
|
|
2564
|
+
registerVaultTools(server, services.vault);
|
|
2565
|
+
}
|
|
2566
|
+
|
|
2567
|
+
// src/server.ts
|
|
2568
|
+
function createServer(config) {
|
|
2569
|
+
const server = new McpServer({
|
|
2570
|
+
name: "obsidian-memory",
|
|
2571
|
+
version: "0.1.0"
|
|
2572
|
+
});
|
|
2573
|
+
const vault = new Vault(config);
|
|
2574
|
+
const entities = new EntityRegistry(vault);
|
|
2575
|
+
const predicates = new PredicateRegistry(vault);
|
|
2576
|
+
const queryEngine = new QueryEngine(vault);
|
|
2577
|
+
const operationManager = new OperationManager(vault, config.agentId);
|
|
2578
|
+
const validator = new SchemaValidator(vault, entities, predicates);
|
|
2579
|
+
const viewGenerator = new ViewGenerator(vault, config.staleDays);
|
|
2580
|
+
registerAllTools(server, {
|
|
2581
|
+
vault,
|
|
2582
|
+
queryEngine,
|
|
2583
|
+
operationManager,
|
|
2584
|
+
validator,
|
|
2585
|
+
viewGenerator,
|
|
2586
|
+
entities,
|
|
2587
|
+
predicates,
|
|
2588
|
+
config
|
|
2589
|
+
});
|
|
2590
|
+
return server;
|
|
2591
|
+
}
|
|
2592
|
+
|
|
2593
|
+
// src/config.ts
|
|
2594
|
+
import * as os from "os";
|
|
2595
|
+
|
|
2596
|
+
// src/types/config.ts
|
|
2597
|
+
var DEFAULT_CONFIG = {
|
|
2598
|
+
transport: "stdio",
|
|
2599
|
+
writeMode: "direct",
|
|
2600
|
+
port: 3100,
|
|
2601
|
+
staleDays: 180
|
|
2602
|
+
};
|
|
2603
|
+
|
|
2604
|
+
// src/config.ts
|
|
2605
|
+
function resolveConfig(input) {
|
|
2606
|
+
const vaultPath = input.vault ?? process.env.VAULT_PATH ?? process.env.OBSIDIAN_VAULT_PATH;
|
|
2607
|
+
if (!vaultPath) {
|
|
2608
|
+
throw new Error(
|
|
2609
|
+
"Vault path is required. Provide --vault flag or set VAULT_PATH environment variable."
|
|
2610
|
+
);
|
|
2611
|
+
}
|
|
2612
|
+
const transport = input.transport ?? process.env.TRANSPORT ?? DEFAULT_CONFIG.transport;
|
|
2613
|
+
const agentId = input.agentId ?? process.env.AGENT_ID ?? generateAgentId(os.hostname().split(".")[0]);
|
|
2614
|
+
const writeMode = input.mode ?? process.env.WRITE_MODE ?? DEFAULT_CONFIG.writeMode;
|
|
2615
|
+
const port = input.port ?? (process.env.PORT ? parseInt(process.env.PORT, 10) : DEFAULT_CONFIG.port);
|
|
2616
|
+
const staleDays = input.staleDays ?? (process.env.STALE_DAYS ? parseInt(process.env.STALE_DAYS, 10) : DEFAULT_CONFIG.staleDays);
|
|
2617
|
+
return {
|
|
2618
|
+
vaultPath,
|
|
2619
|
+
transport,
|
|
2620
|
+
agentId,
|
|
2621
|
+
writeMode,
|
|
2622
|
+
port,
|
|
2623
|
+
apiKey: input.apiKey ?? process.env.API_KEY,
|
|
2624
|
+
staleDays
|
|
2625
|
+
};
|
|
2626
|
+
}
|
|
2627
|
+
|
|
2628
|
+
export {
|
|
2629
|
+
Vault,
|
|
2630
|
+
VaultError,
|
|
2631
|
+
parseMarkdownFile,
|
|
2632
|
+
serializeMarkdown,
|
|
2633
|
+
writeMarkdownFile,
|
|
2634
|
+
readFrontmatter,
|
|
2635
|
+
fileExists,
|
|
2636
|
+
EntityRegistry,
|
|
2637
|
+
PredicateRegistry,
|
|
2638
|
+
QueryEngine,
|
|
2639
|
+
computeFileHash,
|
|
2640
|
+
computeStringHash,
|
|
2641
|
+
isValidHash,
|
|
2642
|
+
generateOperationId,
|
|
2643
|
+
generateAgentId,
|
|
2644
|
+
generateFactId,
|
|
2645
|
+
generateEventId,
|
|
2646
|
+
slugify,
|
|
2647
|
+
OperationManager,
|
|
2648
|
+
OperationError,
|
|
2649
|
+
SchemaValidator,
|
|
2650
|
+
ViewGenerator,
|
|
2651
|
+
scaffoldVault,
|
|
2652
|
+
createServer,
|
|
2653
|
+
resolveConfig
|
|
2654
|
+
};
|
|
2655
|
+
//# sourceMappingURL=chunk-5U2LXK3W.js.map
|