@gethmy/mcp 2.3.0 → 2.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/dist/cli.js +80 -23
  2. package/dist/index.js +80 -23
  3. package/dist/lib/active-learning.js +939 -787
  4. package/dist/lib/api-client.js +2527 -638
  5. package/dist/lib/auto-session.js +177 -196
  6. package/dist/lib/cli.js +34954 -128
  7. package/dist/lib/config.js +235 -201
  8. package/dist/lib/consolidation.js +374 -289
  9. package/dist/lib/context-assembly.js +1265 -838
  10. package/dist/lib/graph-expansion.js +139 -155
  11. package/dist/lib/http.js +1917 -130
  12. package/dist/lib/index.js +29525 -5
  13. package/dist/lib/lifecycle-maintenance.js +663 -79
  14. package/dist/lib/memory-cleanup.js +1316 -381
  15. package/dist/lib/onboard.js +2588 -32
  16. package/dist/lib/prompt-builder.js +438 -445
  17. package/dist/lib/remote.js +31733 -143
  18. package/dist/lib/server.js +29389 -3216
  19. package/dist/lib/skills.js +315 -132
  20. package/dist/lib/tui/agents.js +128 -107
  21. package/dist/lib/tui/docs.js +1590 -687
  22. package/dist/lib/tui/setup.js +5698 -804
  23. package/dist/lib/tui/theme.js +183 -86
  24. package/dist/lib/tui/writer.js +1149 -176
  25. package/package.json +2 -2
  26. package/src/api-client.ts +37 -1
  27. package/src/memory-cleanup.ts +92 -52
  28. package/src/server.ts +16 -1
  29. package/dist/lib/__tests__/active-learning.test.js +0 -386
  30. package/dist/lib/__tests__/agent-performance-profiles.test.js +0 -325
  31. package/dist/lib/__tests__/auto-session.test.js +0 -661
  32. package/dist/lib/__tests__/context-assembly.test.js +0 -362
  33. package/dist/lib/__tests__/graph-expansion.test.js +0 -150
  34. package/dist/lib/__tests__/integration-memory-crud.test.js +0 -797
  35. package/dist/lib/__tests__/integration-memory-system.test.js +0 -281
  36. package/dist/lib/__tests__/lifecycle-maintenance.test.js +0 -207
  37. package/dist/lib/__tests__/pattern-detection.test.js +0 -295
  38. package/dist/lib/__tests__/prompt-builder.test.js +0 -418
@@ -1,88 +1,672 @@
1
- /**
2
- * Automatic Memory Lifecycle Maintenance
3
- *
4
- * Runs at session end to enforce decay/archival rules:
5
- * - Archive entities with confidence < 0.3
6
- * - Delete stale drafts (>30 days old with low decay score)
7
- * - Auto-promote eligible entities (draft→episode, episode→reference)
8
- *
9
- * All operations are non-fatal failures are logged but never block session end.
10
- */
11
- import { evaluateLifecycle } from "@harmony/memory";
12
- /**
13
- * Run lifecycle maintenance for a workspace.
14
- * Called automatically at session end alongside consolidation.
15
- */
16
- export async function runLifecycleMaintenance(client, workspaceId, projectId) {
17
- const result = {
18
- archived: 0,
19
- pruned: 0,
20
- promoted: 0,
21
- reviewed: 0,
22
- errors: 0,
23
- };
24
- let entities;
1
+ import { createRequire } from "node:module";
2
+ var __create = Object.create;
3
+ var __getProtoOf = Object.getPrototypeOf;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __toESM = (mod, isNodeMode, target) => {
8
+ target = mod != null ? __create(__getProtoOf(mod)) : {};
9
+ const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
10
+ for (let key of __getOwnPropNames(mod))
11
+ if (!__hasOwnProp.call(to, key))
12
+ __defProp(to, key, {
13
+ get: () => mod[key],
14
+ enumerable: true
15
+ });
16
+ return to;
17
+ };
18
+ var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
19
+ var __export = (target, all) => {
20
+ for (var name in all)
21
+ __defProp(target, name, {
22
+ get: all[name],
23
+ enumerable: true,
24
+ configurable: true,
25
+ set: (newValue) => all[name] = () => newValue
26
+ });
27
+ };
28
+ var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
29
+ var __require = /* @__PURE__ */ createRequire(import.meta.url);
30
+
31
+ // ../memory/dist/schema.js
32
+ var init_schema = () => {};
33
+
34
+ // ../memory/dist/constraints.js
35
+ var init_constraints = __esm(() => {
36
+ init_schema();
37
+ });
38
+ // ../memory/dist/client.js
39
+ var init_client = __esm(() => {
40
+ init_constraints();
41
+ });
42
+
43
+ // ../memory/dist/graph-walk.js
44
+ async function discoverRelatedContext(client, startIds, maxDepth = 2, maxEntities = 20, minConfidence = 0.5) {
45
+ const visited = new Set;
46
+ const collectedEntities = [];
47
+ const collectedRelations = [];
48
+ let truncated = false;
49
+ const queue = startIds.map((id) => [id, 0]);
50
+ for (const id of startIds) {
51
+ visited.add(id);
52
+ }
53
+ while (queue.length > 0) {
54
+ const [entityId, depth] = queue.shift();
55
+ if (collectedEntities.length >= maxEntities) {
56
+ truncated = true;
57
+ break;
58
+ }
59
+ if (depth > maxDepth)
60
+ continue;
25
61
  try {
26
- const listResult = await client.listMemoryEntities({
27
- workspace_id: workspaceId,
28
- project_id: projectId,
29
- limit: 200,
62
+ const entityResult = await client.getMemoryEntity(entityId);
63
+ const entity = entityResult.entity;
64
+ if (entity) {
65
+ collectedEntities.push({
66
+ id: entity.id,
67
+ type: entity.type,
68
+ title: entity.title,
69
+ confidence: entity.confidence ?? 1,
70
+ memory_tier: entity.memory_tier || "reference"
30
71
  });
31
- entities = (listResult.entities || []);
72
+ }
73
+ if (depth >= maxDepth)
74
+ continue;
75
+ const related = await client.getRelatedEntities(entityId);
76
+ for (const raw of related.outgoing || []) {
77
+ const rel = raw;
78
+ const relConfidence = rel.confidence ?? 1;
79
+ if (relConfidence < minConfidence)
80
+ continue;
81
+ const target = rel.target;
82
+ const targetId = target?.id ?? rel.target_id;
83
+ if (targetId && !visited.has(targetId)) {
84
+ visited.add(targetId);
85
+ queue.push([targetId, depth + 1]);
86
+ collectedRelations.push({
87
+ id: rel.id,
88
+ source_id: entityId,
89
+ target_id: targetId,
90
+ relation_type: rel.relation_type,
91
+ confidence: relConfidence
92
+ });
93
+ }
94
+ }
95
+ for (const raw of related.incoming || []) {
96
+ const rel = raw;
97
+ const relConfidence = rel.confidence ?? 1;
98
+ if (relConfidence < minConfidence)
99
+ continue;
100
+ const source = rel.source;
101
+ const sourceId = source?.id ?? rel.source_id;
102
+ if (sourceId && !visited.has(sourceId)) {
103
+ visited.add(sourceId);
104
+ queue.push([sourceId, depth + 1]);
105
+ collectedRelations.push({
106
+ id: rel.id,
107
+ source_id: sourceId,
108
+ target_id: entityId,
109
+ relation_type: rel.relation_type,
110
+ confidence: relConfidence
111
+ });
112
+ }
113
+ }
114
+ } catch {}
115
+ }
116
+ return {
117
+ entities: collectedEntities,
118
+ relations: collectedRelations,
119
+ depth: maxDepth,
120
+ truncated
121
+ };
122
+ }
123
+
124
+ // ../memory/dist/lifecycle.js
125
+ function computeDecayScore(tier, lastAccessedAt, accessCount) {
126
+ const halfLife = DECAY_HALF_LIVES[tier];
127
+ const now = Date.now();
128
+ let daysSinceAccess = 0;
129
+ if (lastAccessedAt) {
130
+ daysSinceAccess = (now - new Date(lastAccessedAt).getTime()) / (1000 * 60 * 60 * 24);
131
+ }
132
+ const timeDecay = 0.5 ** (daysSinceAccess / halfLife);
133
+ const accessBonus = Math.log10(accessCount + 1) * 0.1;
134
+ const score = Math.min(timeDecay + accessBonus, 1);
135
+ return { score, daysSinceAccess, halfLife, accessBonus };
136
+ }
137
+ function checkPromotion(currentTier, accessCount, confidence, createdAt) {
138
+ const ageDays = (Date.now() - new Date(createdAt).getTime()) / (1000 * 60 * 60 * 24);
139
+ const base = {
140
+ eligible: false,
141
+ targetTier: null,
142
+ reason: null,
143
+ currentTier,
144
+ accessCount,
145
+ confidence,
146
+ ageDays
147
+ };
148
+ if (currentTier === "draft") {
149
+ const rules = PROMOTION_RULES.draftToEpisode;
150
+ if (accessCount >= rules.minAccessCount && confidence >= rules.minConfidence && ageDays >= rules.minAgeDays) {
151
+ return {
152
+ ...base,
153
+ eligible: true,
154
+ targetTier: "episode",
155
+ reason: `Accessed ${accessCount} times (≥${rules.minAccessCount}), confidence ${confidence} (≥${rules.minConfidence}), age ${Math.round(ageDays)}d (≥${rules.minAgeDays}d)`
156
+ };
32
157
  }
33
- catch {
34
- return result;
158
+ }
159
+ if (currentTier === "episode") {
160
+ const rules = PROMOTION_RULES.episodeToReference;
161
+ if (accessCount >= rules.minAccessCount && confidence >= rules.minConfidence && ageDays >= rules.minAgeDays) {
162
+ return {
163
+ ...base,
164
+ eligible: true,
165
+ targetTier: "reference",
166
+ reason: `Accessed ${accessCount} times (≥${rules.minAccessCount}), confidence ${confidence} (≥${rules.minConfidence}), age ${Math.round(ageDays)}d (≥${rules.minAgeDays}d)`
167
+ };
168
+ }
169
+ }
170
+ return base;
171
+ }
172
+ function evaluateLifecycle(entity) {
173
+ const decay = computeDecayScore(entity.memory_tier, entity.last_accessed_at, entity.access_count);
174
+ const promotion = checkPromotion(entity.memory_tier, entity.access_count, entity.confidence, entity.created_at);
175
+ const shouldArchive = entity.confidence < ARCHIVE_THRESHOLD;
176
+ const archiveReason = shouldArchive ? `Confidence ${entity.confidence} below threshold ${ARCHIVE_THRESHOLD}` : undefined;
177
+ const shouldFlagForReview = decay.daysSinceAccess >= STALE_DAYS && entity.access_count < STALE_MIN_ACCESS;
178
+ const reviewReason = shouldFlagForReview ? `Not accessed in ${Math.round(decay.daysSinceAccess)} days with only ${entity.access_count} accesses` : undefined;
179
+ return {
180
+ decay,
181
+ promotion,
182
+ shouldArchive,
183
+ shouldFlagForReview,
184
+ archiveReason,
185
+ reviewReason
186
+ };
187
+ }
188
+ var DECAY_HALF_LIVES, PROMOTION_RULES, ARCHIVE_THRESHOLD = 0.3, STALE_DAYS = 90, STALE_MIN_ACCESS = 3;
189
+ var init_lifecycle = __esm(() => {
190
+ DECAY_HALF_LIVES = {
191
+ draft: 7,
192
+ episode: 30,
193
+ reference: 180
194
+ };
195
+ PROMOTION_RULES = {
196
+ draftToEpisode: {
197
+ minAccessCount: 5,
198
+ minConfidence: 0.8,
199
+ minAgeDays: 1
200
+ },
201
+ episodeToReference: {
202
+ minAccessCount: 10,
203
+ minConfidence: 0.9,
204
+ minAgeDays: 7
205
+ }
206
+ };
207
+ });
208
+
209
+ // ../memory/dist/sync-storage.js
210
+ function parseSyncMarkdown(markdown) {
211
+ const trimmed = markdown.trim();
212
+ let frontmatter = {
213
+ type: "context",
214
+ scope: "project",
215
+ tier: "reference",
216
+ confidence: 1,
217
+ tags: []
218
+ };
219
+ let body = trimmed;
220
+ const fmMatch = trimmed.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
221
+ if (fmMatch) {
222
+ frontmatter = parseSyncYamlFrontmatter(fmMatch[1]);
223
+ body = fmMatch[2].trim();
224
+ }
225
+ let title = "";
226
+ const titleMatch = body.match(/^#\s+(.+)/m);
227
+ if (titleMatch) {
228
+ title = titleMatch[1].trim();
229
+ body = body.replace(/^#\s+.+\n?/, "").trim();
230
+ }
231
+ return { frontmatter, title, content: body };
232
+ }
233
+ function serializeSyncMarkdown(entity) {
234
+ const lines = ["---"];
235
+ lines.push(`id: ${entity.id}`);
236
+ lines.push(`workspace_id: ${entity.workspace_id}`);
237
+ if (entity.project_id) {
238
+ lines.push(`project_id: ${entity.project_id}`);
239
+ }
240
+ lines.push(`type: ${entity.type}`);
241
+ lines.push(`scope: ${entity.scope}`);
242
+ lines.push(`tier: ${entity.memory_tier || "reference"}`);
243
+ lines.push(`confidence: ${entity.confidence}`);
244
+ if (entity.tags.length > 0) {
245
+ lines.push(`tags: [${entity.tags.join(", ")}]`);
246
+ } else {
247
+ lines.push("tags: []");
248
+ }
249
+ if (entity.agent_identifier) {
250
+ lines.push(`agent: ${entity.agent_identifier}`);
251
+ }
252
+ lines.push(`created_at: ${entity.created_at}`);
253
+ lines.push(`updated_at: ${entity.updated_at}`);
254
+ lines.push("---");
255
+ lines.push("");
256
+ lines.push(`# ${entity.title}`);
257
+ lines.push("");
258
+ lines.push(entity.content);
259
+ return lines.join(`
260
+ `);
261
+ }
262
+ function parseSyncYamlFrontmatter(yaml) {
263
+ const result = {
264
+ type: "context",
265
+ scope: "project",
266
+ tier: "reference",
267
+ confidence: 1,
268
+ tags: []
269
+ };
270
+ for (const line of yaml.split(`
271
+ `)) {
272
+ const colonIndex = line.indexOf(":");
273
+ if (colonIndex === -1)
274
+ continue;
275
+ const key = line.slice(0, colonIndex).trim();
276
+ const value = line.slice(colonIndex + 1).trim();
277
+ switch (key) {
278
+ case "id":
279
+ result.id = value;
280
+ break;
281
+ case "workspace_id":
282
+ result.workspace_id = value;
283
+ break;
284
+ case "project_id":
285
+ result.project_id = value;
286
+ break;
287
+ case "type":
288
+ result.type = value;
289
+ break;
290
+ case "scope":
291
+ result.scope = value;
292
+ break;
293
+ case "tier":
294
+ result.tier = value;
295
+ break;
296
+ case "confidence":
297
+ result.confidence = parseFloat(value) || 1;
298
+ break;
299
+ case "tags":
300
+ result.tags = parseYamlArray(value);
301
+ break;
302
+ case "related":
303
+ result.related = parseYamlArray(value);
304
+ break;
305
+ case "agent":
306
+ result.agent = value;
307
+ break;
308
+ case "created_at":
309
+ result.created_at = value;
310
+ break;
311
+ case "updated_at":
312
+ result.updated_at = value;
313
+ break;
35
314
  }
36
- if (entities.length === 0)
37
- return result;
38
- const now = Date.now();
39
- const STALE_DRAFT_MAX_AGE_DAYS = 30;
40
- for (const entity of entities) {
41
- try {
42
- const lifecycle = evaluateLifecycle(entity);
43
- // 1. Archive low-confidence entities (confidence < 0.3)
44
- if (lifecycle.shouldArchive) {
45
- await client.deleteMemoryEntity(entity.id);
46
- result.archived++;
47
- continue;
48
- }
49
- // 2. Prune stale drafts (>30 days old with low decay score)
50
- if (entity.memory_tier === "draft") {
51
- const ageDays = (now - new Date(entity.created_at).getTime()) / (1000 * 60 * 60 * 24);
52
- if (ageDays > STALE_DRAFT_MAX_AGE_DAYS && lifecycle.decay.score < 0.3) {
53
- await client.deleteMemoryEntity(entity.id);
54
- result.pruned++;
55
- continue;
56
- }
57
- }
58
- // 3. Auto-promote eligible entities
59
- if (lifecycle.promotion.eligible && lifecycle.promotion.targetTier) {
60
- await client.updateMemoryEntity(entity.id, {
61
- memory_tier: lifecycle.promotion.targetTier,
62
- metadata: {
63
- promoted_at: new Date().toISOString(),
64
- promotion_reason: lifecycle.promotion.reason,
65
- promoted_from: entity.memory_tier,
66
- },
67
- });
68
- result.promoted++;
69
- continue;
70
- }
71
- // 4. Flag stale entities for review (>90 days, <3 accesses)
72
- if (lifecycle.shouldFlagForReview) {
73
- await client.updateMemoryEntity(entity.id, {
74
- metadata: {
75
- needs_review: true,
76
- review_reason: lifecycle.reviewReason,
77
- flagged_at: new Date().toISOString(),
78
- },
79
- });
80
- result.reviewed++;
81
- }
315
+ }
316
+ return result;
317
+ }
318
+ function parseYamlArray(value) {
319
+ const match = value.match(/^\[(.*)]\s*$/);
320
+ if (!match)
321
+ return [];
322
+ return match[1].split(",").map((s) => s.trim()).filter(Boolean);
323
+ }
324
+
325
+ // ../memory/dist/sync.js
326
+ import { createHash } from "node:crypto";
327
+ import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
328
+ import { join, relative, sep } from "node:path";
329
+ function computeFileHash(content) {
330
+ return `sha256:${createHash("sha256").update(content).digest("hex")}`;
331
+ }
332
+ function slugifyTitle(title) {
333
+ return title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80);
334
+ }
335
+ function entityToFilename(entity) {
336
+ const slug = slugifyTitle(entity.title);
337
+ const shortId = entity.id.slice(0, 8);
338
+ return `${entity.type}--${slug}--${shortId}.md`;
339
+ }
340
+ function entityToDirectoryPath(entity, memoryDir) {
341
+ const wsDir = join(memoryDir, entity.workspace_id);
342
+ if (entity.scope === "private") {
343
+ return join(wsDir, "_private");
344
+ }
345
+ if (entity.scope === "workspace" || !entity.project_id) {
346
+ return join(wsDir, "_workspace");
347
+ }
348
+ return join(wsDir, entity.project_id);
349
+ }
350
+ function emptySyncState() {
351
+ return { version: 1, lastPullAt: null, entities: {} };
352
+ }
353
+ function loadSyncState(memoryDir) {
354
+ const statePath = join(memoryDir, ".sync-state.json");
355
+ if (!existsSync(statePath))
356
+ return emptySyncState();
357
+ try {
358
+ return JSON.parse(readFileSync(statePath, "utf-8"));
359
+ } catch {
360
+ return emptySyncState();
361
+ }
362
+ }
363
+ function saveSyncState(memoryDir, state) {
364
+ if (!existsSync(memoryDir)) {
365
+ mkdirSync(memoryDir, { recursive: true });
366
+ }
367
+ writeFileSync(join(memoryDir, ".sync-state.json"), JSON.stringify(state, null, 2));
368
+ }
369
+ function writeEntityFile(entity, memoryDir) {
370
+ const dir = entityToDirectoryPath(entity, memoryDir);
371
+ if (!existsSync(dir))
372
+ mkdirSync(dir, { recursive: true });
373
+ const filename = entityToFilename(entity);
374
+ const filePath = join(dir, filename);
375
+ const markdown = serializeSyncMarkdown(entity);
376
+ writeFileSync(filePath, markdown);
377
+ return relative(memoryDir, filePath);
378
+ }
379
+ function deleteEntityFile(relPath, memoryDir) {
380
+ const absPath = join(memoryDir, relPath);
381
+ if (existsSync(absPath)) {
382
+ rmSync(absPath);
383
+ }
384
+ }
385
+ function findMarkdownFiles(dir) {
386
+ const results = [];
387
+ if (!existsSync(dir))
388
+ return results;
389
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
390
+ const fullPath = join(dir, entry.name);
391
+ if (entry.isDirectory()) {
392
+ results.push(...findMarkdownFiles(fullPath));
393
+ } else if (entry.isFile() && entry.name.endsWith(".md")) {
394
+ results.push(fullPath);
395
+ }
396
+ }
397
+ return results;
398
+ }
399
+ async function syncPull(client, config, workspaceId, projectId) {
400
+ const { memoryDir } = config;
401
+ const state = loadSyncState(memoryDir);
402
+ const result = {
403
+ pulled: 0,
404
+ pushed: 0,
405
+ deleted: 0,
406
+ conflicts: 0,
407
+ errors: []
408
+ };
409
+ const allEntities = [];
410
+ let offset = 0;
411
+ const batchSize = 100;
412
+ while (true) {
413
+ try {
414
+ const resp = await client.listMemoryEntities({
415
+ workspace_id: workspaceId,
416
+ project_id: projectId,
417
+ limit: batchSize,
418
+ offset
419
+ });
420
+ const batch = resp.entities;
421
+ allEntities.push(...batch);
422
+ if (batch.length < batchSize)
423
+ break;
424
+ offset += batchSize;
425
+ } catch (err) {
426
+ result.errors.push(`Failed to fetch entities: ${err}`);
427
+ return result;
428
+ }
429
+ }
430
+ const remoteIds = new Set(allEntities.map((e) => e.id));
431
+ for (const entity of allEntities) {
432
+ const existing = state.entities[entity.id];
433
+ const markdown = serializeSyncMarkdown(entity);
434
+ const hash = computeFileHash(markdown);
435
+ if (!existing) {
436
+ const relPath = writeEntityFile(entity, memoryDir);
437
+ state.entities[entity.id] = {
438
+ filePath: relPath,
439
+ remoteUpdatedAt: entity.updated_at,
440
+ lastSyncedHash: hash
441
+ };
442
+ result.pulled++;
443
+ } else {
444
+ const remoteChanged = entity.updated_at > existing.remoteUpdatedAt;
445
+ if (!remoteChanged)
446
+ continue;
447
+ const absPath = join(memoryDir, existing.filePath);
448
+ let localChanged = false;
449
+ if (existsSync(absPath)) {
450
+ const localContent = readFileSync(absPath, "utf-8");
451
+ const localHash = computeFileHash(localContent);
452
+ localChanged = localHash !== existing.lastSyncedHash;
453
+ }
454
+ if (localChanged) {
455
+ result.conflicts++;
456
+ }
457
+ const newRelPath = writeEntityFile(entity, memoryDir);
458
+ if (existing.filePath !== newRelPath) {
459
+ deleteEntityFile(existing.filePath, memoryDir);
460
+ }
461
+ state.entities[entity.id] = {
462
+ filePath: newRelPath,
463
+ remoteUpdatedAt: entity.updated_at,
464
+ lastSyncedHash: hash
465
+ };
466
+ result.pulled++;
467
+ }
468
+ }
469
+ for (const [entityId, entry] of Object.entries(state.entities)) {
470
+ if (!remoteIds.has(entityId)) {
471
+ deleteEntityFile(entry.filePath, memoryDir);
472
+ delete state.entities[entityId];
473
+ result.deleted++;
474
+ }
475
+ }
476
+ state.lastPullAt = new Date().toISOString();
477
+ saveSyncState(memoryDir, state);
478
+ return result;
479
+ }
480
+ async function syncPush(client, config, workspaceId) {
481
+ const { memoryDir } = config;
482
+ const state = loadSyncState(memoryDir);
483
+ const result = {
484
+ pulled: 0,
485
+ pushed: 0,
486
+ deleted: 0,
487
+ conflicts: 0,
488
+ errors: []
489
+ };
490
+ const mdFiles = findMarkdownFiles(memoryDir);
491
+ for (const absPath of mdFiles) {
492
+ const content = readFileSync(absPath, "utf-8");
493
+ const hash = computeFileHash(content);
494
+ const parsed = parseSyncMarkdown(content);
495
+ if (parsed.frontmatter.id) {
496
+ const entityId = parsed.frontmatter.id;
497
+ const existing = state.entities[entityId];
498
+ if (existing && hash === existing.lastSyncedHash)
499
+ continue;
500
+ try {
501
+ const resp = await client.updateMemoryEntity(entityId, {
502
+ title: parsed.title || "Untitled",
503
+ content: parsed.content,
504
+ type: parsed.frontmatter.type,
505
+ scope: parsed.frontmatter.scope,
506
+ confidence: parsed.frontmatter.confidence,
507
+ tags: parsed.frontmatter.tags
508
+ });
509
+ const updated = resp.entity;
510
+ const newMarkdown = serializeSyncMarkdown(updated);
511
+ writeFileSync(absPath, newMarkdown);
512
+ const relPath = relative(memoryDir, absPath);
513
+ state.entities[entityId] = {
514
+ filePath: relPath,
515
+ remoteUpdatedAt: updated.updated_at,
516
+ lastSyncedHash: computeFileHash(newMarkdown)
517
+ };
518
+ result.pushed++;
519
+ } catch (err) {
520
+ result.errors.push(`Failed to update ${entityId}: ${err}`);
521
+ }
522
+ } else {
523
+ const relPath = relative(memoryDir, absPath);
524
+ const parts = relPath.split(sep);
525
+ const fileWorkspaceId = parts.length >= 2 ? parts[0] : workspaceId;
526
+ let fileProjectId;
527
+ if (parts.length >= 3) {
528
+ const scopeDir = parts[1];
529
+ if (scopeDir !== "_workspace" && scopeDir !== "_private") {
530
+ fileProjectId = scopeDir;
82
531
  }
83
- catch {
84
- result.errors++;
532
+ }
533
+ let scope = parsed.frontmatter.scope || "project";
534
+ if (parts.length >= 3) {
535
+ const scopeDir = parts[1];
536
+ if (scopeDir === "_private")
537
+ scope = "private";
538
+ else if (scopeDir === "_workspace")
539
+ scope = "workspace";
540
+ }
541
+ try {
542
+ const resp = await client.createMemoryEntity({
543
+ workspace_id: fileWorkspaceId,
544
+ project_id: fileProjectId,
545
+ type: parsed.frontmatter.type,
546
+ scope,
547
+ title: parsed.title || "Untitled",
548
+ content: parsed.content,
549
+ confidence: parsed.frontmatter.confidence,
550
+ tags: parsed.frontmatter.tags,
551
+ agent_identifier: parsed.frontmatter.agent
552
+ });
553
+ const created = resp.entity;
554
+ const dir = entityToDirectoryPath(created, memoryDir);
555
+ if (!existsSync(dir))
556
+ mkdirSync(dir, { recursive: true });
557
+ const newFilename = entityToFilename(created);
558
+ const newAbsPath = join(dir, newFilename);
559
+ const newMarkdown = serializeSyncMarkdown(created);
560
+ writeFileSync(newAbsPath, newMarkdown);
561
+ if (absPath !== newAbsPath && existsSync(absPath)) {
562
+ rmSync(absPath);
85
563
  }
564
+ const newRelPath = relative(memoryDir, newAbsPath);
565
+ state.entities[created.id] = {
566
+ filePath: newRelPath,
567
+ remoteUpdatedAt: created.updated_at,
568
+ lastSyncedHash: computeFileHash(newMarkdown)
569
+ };
570
+ result.pushed++;
571
+ } catch (err) {
572
+ result.errors.push(`Failed to create entity from ${relPath}: ${err}`);
573
+ }
86
574
  }
575
+ }
576
+ saveSyncState(memoryDir, state);
577
+ return result;
578
+ }
579
+ async function syncFull(client, config, workspaceId, projectId) {
580
+ const pullResult = await syncPull(client, config, workspaceId, projectId);
581
+ const pushResult = await syncPush(client, config, workspaceId);
582
+ return {
583
+ pulled: pullResult.pulled,
584
+ pushed: pushResult.pushed,
585
+ deleted: pullResult.deleted,
586
+ conflicts: pullResult.conflicts,
587
+ errors: [...pullResult.errors, ...pushResult.errors]
588
+ };
589
+ }
590
+ var init_sync = () => {};
591
+
592
+ // ../memory/dist/index.js
593
+ var init_dist = __esm(() => {
594
+ init_client();
595
+ init_constraints();
596
+ init_lifecycle();
597
+ init_schema();
598
+ init_sync();
599
+ });
600
+
601
+ // src/lifecycle-maintenance.ts
602
+ init_dist();
603
+ async function runLifecycleMaintenance(client2, workspaceId, projectId) {
604
+ const result = {
605
+ archived: 0,
606
+ pruned: 0,
607
+ promoted: 0,
608
+ reviewed: 0,
609
+ errors: 0
610
+ };
611
+ let entities;
612
+ try {
613
+ const listResult = await client2.listMemoryEntities({
614
+ workspace_id: workspaceId,
615
+ project_id: projectId,
616
+ limit: 200
617
+ });
618
+ entities = listResult.entities || [];
619
+ } catch {
87
620
  return result;
621
+ }
622
+ if (entities.length === 0)
623
+ return result;
624
+ const now = Date.now();
625
+ const STALE_DRAFT_MAX_AGE_DAYS = 30;
626
+ for (const entity of entities) {
627
+ try {
628
+ const lifecycle2 = evaluateLifecycle(entity);
629
+ if (lifecycle2.shouldArchive) {
630
+ await client2.deleteMemoryEntity(entity.id);
631
+ result.archived++;
632
+ continue;
633
+ }
634
+ if (entity.memory_tier === "draft") {
635
+ const ageDays = (now - new Date(entity.created_at).getTime()) / (1000 * 60 * 60 * 24);
636
+ if (ageDays > STALE_DRAFT_MAX_AGE_DAYS && lifecycle2.decay.score < 0.3) {
637
+ await client2.deleteMemoryEntity(entity.id);
638
+ result.pruned++;
639
+ continue;
640
+ }
641
+ }
642
+ if (lifecycle2.promotion.eligible && lifecycle2.promotion.targetTier) {
643
+ await client2.updateMemoryEntity(entity.id, {
644
+ memory_tier: lifecycle2.promotion.targetTier,
645
+ metadata: {
646
+ promoted_at: new Date().toISOString(),
647
+ promotion_reason: lifecycle2.promotion.reason,
648
+ promoted_from: entity.memory_tier
649
+ }
650
+ });
651
+ result.promoted++;
652
+ continue;
653
+ }
654
+ if (lifecycle2.shouldFlagForReview) {
655
+ await client2.updateMemoryEntity(entity.id, {
656
+ metadata: {
657
+ needs_review: true,
658
+ review_reason: lifecycle2.reviewReason,
659
+ flagged_at: new Date().toISOString()
660
+ }
661
+ });
662
+ result.reviewed++;
663
+ }
664
+ } catch {
665
+ result.errors++;
666
+ }
667
+ }
668
+ return result;
88
669
  }
670
+ export {
671
+ runLifecycleMaintenance
672
+ };