@ghl-ai/aw 0.1.37-beta.36 → 0.1.37-beta.37

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 (2) hide show
  1. package/memory-sync.mjs +127 -0
  2. package/package.json +3 -2
@@ -0,0 +1,127 @@
1
+ // memory-sync.mjs — Bidirectional memory sync: pull from MCP, write to local files.
2
+
3
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs';
4
+ import { join } from 'node:path';
5
+ import { callMemoryTool } from './memory-bridge.mjs';
6
+
7
+ const MANIFEST_FILE = 'manifest.json';
8
+ const STALE_THRESHOLD_MS = 24 * 60 * 60 * 1000; // 24 hours
9
+
10
+ /**
11
+ * Check if local memory cache is stale (>24h since last sync).
12
+ * @param {string} registryDir — path to ~/.aw_registry/memory/
13
+ * @returns {boolean} true if stale or manifest missing
14
+ */
15
+ export function checkStaleness(registryDir) {
16
+ const manifestPath = join(registryDir, MANIFEST_FILE);
17
+ if (!existsSync(manifestPath)) return true;
18
+ try {
19
+ const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
20
+ if (!manifest.lastSync) return true;
21
+ const lastSync = new Date(manifest.lastSync).getTime();
22
+ return Date.now() - lastSync > STALE_THRESHOLD_MS;
23
+ } catch {
24
+ return true;
25
+ }
26
+ }
27
+
28
+ /**
29
+ * Group a memory into a target file based on overlay/angle.
30
+ * @param {object} mem — memory object with overlay, angle fields
31
+ * @returns {string} filename (e.g. 'incidents.md', 'patterns.md', 'product.md', 'core.md')
32
+ */
33
+ function classifyMemory(mem) {
34
+ const overlays = Array.isArray(mem.overlay) ? mem.overlay : (Array.isArray(mem.overlays) ? mem.overlays : []);
35
+ const angles = Array.isArray(mem.angle) ? mem.angle : (Array.isArray(mem.angles) ? mem.angles : []);
36
+
37
+ const overlayStr = overlays.join(' ').toLowerCase();
38
+ const angleStr = angles.join(' ').toLowerCase();
39
+
40
+ if (overlayStr.includes('incident')) return 'incidents.md';
41
+ if (overlayStr.includes('service') || angleStr.includes('technical')) return 'patterns.md';
42
+ if (overlayStr.includes('product')) return 'product.md';
43
+ return 'core.md';
44
+ }
45
+
46
+ /**
47
+ * Pull top memories from MCP and write grouped files.
48
+ * @param {string} registryDir — path to ~/.aw_registry/memory/
49
+ * @returns {Promise<{ total: number, files: Record<string, number> }>}
50
+ */
51
+ export async function pullAndWriteMemories(registryDir) {
52
+ const result = await callMemoryTool('memory_search', { query: '*', limit: 50 });
53
+ const memories = Array.isArray(result) ? result : (result?.memories ?? result?.results ?? []);
54
+
55
+ // Group by target file
56
+ const groups = {};
57
+ for (const mem of memories) {
58
+ const file = classifyMemory(mem);
59
+ if (!groups[file]) groups[file] = [];
60
+ groups[file].push(mem);
61
+ }
62
+
63
+ const now = new Date().toISOString();
64
+ const fileCounts = {};
65
+
66
+ for (const [filename, mems] of Object.entries(groups)) {
67
+ const sectionName = filename.replace('.md', '').replace(/^\w/, c => c.toUpperCase());
68
+ const lines = [
69
+ '<!-- aw:memory:auto-synced -->',
70
+ `<!-- last-sync: ${now} -->`,
71
+ '<!-- DO NOT EDIT — regenerated by `aw memory sync` -->',
72
+ '',
73
+ `## ${sectionName}`,
74
+ ];
75
+
76
+ for (const mem of mems) {
77
+ const content = mem.content ?? mem.text ?? JSON.stringify(mem);
78
+ const confidence = mem.confidence ?? mem.classification_confidence ?? '';
79
+ const id = mem.id ?? '';
80
+ const meta = [
81
+ confidence ? `confidence: ${confidence}` : null,
82
+ id ? `id: ${id}` : null,
83
+ ].filter(Boolean).join(' | ');
84
+
85
+ lines.push(`- ${content}${meta ? ` <!-- ${meta} -->` : ''}`);
86
+ }
87
+
88
+ lines.push('');
89
+ writeFileSync(join(registryDir, filename), lines.join('\n'));
90
+ fileCounts[filename] = mems.length;
91
+ }
92
+
93
+ return { total: memories.length, files: fileCounts };
94
+ }
95
+
96
+ /**
97
+ * Write manifest.json with sync metadata.
98
+ * @param {string} registryDir — path to ~/.aw_registry/memory/
99
+ * @param {number} count — number of memories synced
100
+ */
101
+ export function writeManifest(registryDir, count) {
102
+ const manifest = {
103
+ lastSync: new Date().toISOString(),
104
+ memoriesSynced: count,
105
+ version: 1,
106
+ };
107
+ writeFileSync(join(registryDir, MANIFEST_FILE), JSON.stringify(manifest, null, 2) + '\n');
108
+ }
109
+
110
+ /**
111
+ * Main sync function — check staleness, pull, write, update manifest.
112
+ * @param {string} registryDir — path to ~/.aw_registry/memory/
113
+ * @param {object} [opts]
114
+ * @param {boolean} [opts.force] — skip staleness check
115
+ * @returns {Promise<{ skipped: boolean, total?: number, files?: Record<string, number> }>}
116
+ */
117
+ export async function syncMemories(registryDir, opts = {}) {
118
+ mkdirSync(registryDir, { recursive: true });
119
+
120
+ if (!opts.force && !checkStaleness(registryDir)) {
121
+ return { skipped: true };
122
+ }
123
+
124
+ const { total, files } = await pullAndWriteMemories(registryDir);
125
+ writeManifest(registryDir, total);
126
+ return { skipped: false, total, files };
127
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ghl-ai/aw",
3
- "version": "0.1.37-beta.36",
3
+ "version": "0.1.37-beta.37",
4
4
  "description": "Agentic Workspace CLI — pull, push & manage agents, skills and commands from the registry",
5
5
  "type": "module",
6
6
  "bin": "bin.js",
@@ -27,7 +27,8 @@
27
27
  "ecc.mjs",
28
28
  "render-rules.mjs",
29
29
  "telemetry.mjs",
30
- "memory-bridge.mjs"
30
+ "memory-bridge.mjs",
31
+ "memory-sync.mjs"
31
32
  ],
32
33
  "engines": {
33
34
  "node": ">=18.0.0"