@ghl-ai/aw 0.1.37-beta.35 → 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.
- package/commands/init.mjs +3 -0
- package/commands/memory.mjs +137 -6
- package/ecc.mjs +1 -1
- package/memory-sync.mjs +127 -0
- package/package.json +3 -2
package/commands/init.mjs
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
import {
|
|
8
8
|
existsSync,
|
|
9
|
+
mkdirSync,
|
|
9
10
|
writeFileSync,
|
|
10
11
|
symlinkSync,
|
|
11
12
|
lstatSync,
|
|
@@ -246,6 +247,7 @@ export async function initCommand(args) {
|
|
|
246
247
|
}
|
|
247
248
|
|
|
248
249
|
ensureAwGitignore(AW_HOME);
|
|
250
|
+
mkdirSync(join(GLOBAL_AW_DIR, 'memory'), { recursive: true });
|
|
249
251
|
const freshCfg = config.load(GLOBAL_AW_DIR);
|
|
250
252
|
if (existsSync(GLOBAL_AW_DIR)) {
|
|
251
253
|
syncFileTree(join(AW_HOME, RULES_SOURCE_DIR), join(GLOBAL_AW_DIR, RULES_SOURCE_DIR));
|
|
@@ -351,6 +353,7 @@ export async function initCommand(args) {
|
|
|
351
353
|
try {
|
|
352
354
|
await initPersistentClone(repoUrl, AW_HOME, sparsePaths);
|
|
353
355
|
ensureAwGitignore(AW_HOME);
|
|
356
|
+
mkdirSync(join(GLOBAL_AW_DIR, 'memory'), { recursive: true });
|
|
354
357
|
s.stop('Registry cloned');
|
|
355
358
|
} catch (e) {
|
|
356
359
|
s.stop(chalk.red('Clone failed'));
|
package/commands/memory.mjs
CHANGED
|
@@ -1,17 +1,24 @@
|
|
|
1
|
-
// commands/memory.mjs — `aw memory [store|search|pack|stats]`
|
|
1
|
+
// commands/memory.mjs — `aw memory [store|search|pack|stats|validate|invalidate|sync]`
|
|
2
2
|
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { homedir } from 'node:os';
|
|
3
5
|
import * as fmt from '../fmt.mjs';
|
|
4
6
|
import { chalk } from '../fmt.mjs';
|
|
5
7
|
import { callMemoryTool } from '../memory-bridge.mjs';
|
|
8
|
+
import { syncMemories } from '../memory-sync.mjs';
|
|
9
|
+
import { REGISTRY_DIR } from '../constants.mjs';
|
|
6
10
|
|
|
7
11
|
export async function memoryCommand(args) {
|
|
8
12
|
const sub = args._positional?.[0];
|
|
9
13
|
switch (sub) {
|
|
10
|
-
case 'store':
|
|
11
|
-
case 'search':
|
|
12
|
-
case 'pack':
|
|
13
|
-
case 'stats':
|
|
14
|
-
|
|
14
|
+
case 'store': return memoryStore(args);
|
|
15
|
+
case 'search': return memorySearch(args);
|
|
16
|
+
case 'pack': return memoryPack(args);
|
|
17
|
+
case 'stats': return memoryStats(args);
|
|
18
|
+
case 'validate': return memoryValidate(args);
|
|
19
|
+
case 'invalidate': return memoryInvalidate(args);
|
|
20
|
+
case 'sync': return memorySync(args);
|
|
21
|
+
default: return memoryHelp();
|
|
15
22
|
}
|
|
16
23
|
}
|
|
17
24
|
|
|
@@ -223,6 +230,121 @@ async function memoryStats(args) {
|
|
|
223
230
|
}
|
|
224
231
|
}
|
|
225
232
|
|
|
233
|
+
// ── validate ──────────────────────────────────────────────────────────
|
|
234
|
+
|
|
235
|
+
async function memoryValidate(args) {
|
|
236
|
+
const memoryId = args._positional?.[1];
|
|
237
|
+
if (!memoryId) {
|
|
238
|
+
fmt.cancel('Usage: aw memory validate <memory-id> [--reason "text"]');
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
fmt.intro('aw memory validate');
|
|
243
|
+
|
|
244
|
+
const params = {
|
|
245
|
+
memory_id: memoryId,
|
|
246
|
+
feedback_type: 'validate',
|
|
247
|
+
actor_type: 'user',
|
|
248
|
+
};
|
|
249
|
+
if (args['--reason']) params.reason = args['--reason'];
|
|
250
|
+
|
|
251
|
+
const s = fmt.spinner();
|
|
252
|
+
s.start('Validating memory...');
|
|
253
|
+
try {
|
|
254
|
+
const result = await callMemoryTool('memory_feedback', params);
|
|
255
|
+
s.stop('Memory validated');
|
|
256
|
+
|
|
257
|
+
const data = result?.result ?? result;
|
|
258
|
+
const lines = [
|
|
259
|
+
`${chalk.dim('id:')} ${memoryId}`,
|
|
260
|
+
`${chalk.dim('action:')} ${chalk.green('validated')}`,
|
|
261
|
+
data.reason ? `${chalk.dim('reason:')} ${data.reason}` : null,
|
|
262
|
+
].filter(Boolean).join('\n');
|
|
263
|
+
|
|
264
|
+
fmt.note(lines, 'Result');
|
|
265
|
+
fmt.outro('Memory validated');
|
|
266
|
+
} catch (err) {
|
|
267
|
+
s.stop(chalk.red('Failed'));
|
|
268
|
+
fmt.cancel(`Validate failed: ${err.message}`);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// ── invalidate ────────────────────────────────────────────────────────
|
|
273
|
+
|
|
274
|
+
async function memoryInvalidate(args) {
|
|
275
|
+
const memoryId = args._positional?.[1];
|
|
276
|
+
if (!memoryId) {
|
|
277
|
+
fmt.cancel('Usage: aw memory invalidate <memory-id> [--reason "text"]');
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
fmt.intro('aw memory invalidate');
|
|
282
|
+
|
|
283
|
+
const params = {
|
|
284
|
+
memory_id: memoryId,
|
|
285
|
+
feedback_type: 'invalidate',
|
|
286
|
+
actor_type: 'user',
|
|
287
|
+
};
|
|
288
|
+
if (args['--reason']) params.reason = args['--reason'];
|
|
289
|
+
|
|
290
|
+
const s = fmt.spinner();
|
|
291
|
+
s.start('Invalidating memory...');
|
|
292
|
+
try {
|
|
293
|
+
const result = await callMemoryTool('memory_feedback', params);
|
|
294
|
+
s.stop('Memory invalidated');
|
|
295
|
+
|
|
296
|
+
const data = result?.result ?? result;
|
|
297
|
+
const lines = [
|
|
298
|
+
`${chalk.dim('id:')} ${memoryId}`,
|
|
299
|
+
`${chalk.dim('action:')} ${chalk.red('invalidated')}`,
|
|
300
|
+
data.reason ? `${chalk.dim('reason:')} ${data.reason}` : null,
|
|
301
|
+
].filter(Boolean).join('\n');
|
|
302
|
+
|
|
303
|
+
fmt.note(lines, 'Result');
|
|
304
|
+
fmt.outro('Memory invalidated');
|
|
305
|
+
} catch (err) {
|
|
306
|
+
s.stop(chalk.red('Failed'));
|
|
307
|
+
fmt.cancel(`Invalidate failed: ${err.message}`);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// ── sync ──────────────────────────────────────────────────────────────
|
|
312
|
+
|
|
313
|
+
async function memorySync(args) {
|
|
314
|
+
fmt.intro('aw memory sync');
|
|
315
|
+
|
|
316
|
+
const registryDir = join(homedir(), REGISTRY_DIR, 'memory');
|
|
317
|
+
const force = args['--force'] === true;
|
|
318
|
+
|
|
319
|
+
const s = fmt.spinner();
|
|
320
|
+
s.start('Syncing memories...');
|
|
321
|
+
try {
|
|
322
|
+
const result = await syncMemories(registryDir, { force });
|
|
323
|
+
if (result.skipped) {
|
|
324
|
+
s.stop('Cache is fresh');
|
|
325
|
+
fmt.logInfo('Local memory cache is less than 24h old. Use --force to re-sync.');
|
|
326
|
+
fmt.outro('Sync skipped');
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
s.stop('Memories synced');
|
|
331
|
+
|
|
332
|
+
const fileLines = Object.entries(result.files)
|
|
333
|
+
.map(([file, count]) => ` ${chalk.cyan(file.padEnd(20))} ${count} memories`)
|
|
334
|
+
.join('\n');
|
|
335
|
+
|
|
336
|
+
if (fileLines) {
|
|
337
|
+
fmt.note(fileLines, 'Files written');
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
fmt.logStep(`Total: ${chalk.bold(result.total)} memories synced to ${chalk.dim(registryDir)}`);
|
|
341
|
+
fmt.outro('Sync complete');
|
|
342
|
+
} catch (err) {
|
|
343
|
+
s.stop(chalk.red('Failed'));
|
|
344
|
+
fmt.cancel(`Sync failed: ${err.message}`);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
226
348
|
// ── help ─────────────────────────────────────────────────────────────
|
|
227
349
|
|
|
228
350
|
function memoryHelp() {
|
|
@@ -257,6 +379,15 @@ function memoryHelp() {
|
|
|
257
379
|
cmd('aw memory stats', 'Show memory statistics'),
|
|
258
380
|
cmd(' --namespace <ns>', 'Namespace scope'),
|
|
259
381
|
'',
|
|
382
|
+
cmd('aw memory validate <memory-id>', 'Mark a memory as validated'),
|
|
383
|
+
cmd(' --reason "text"', 'Reason for validation'),
|
|
384
|
+
'',
|
|
385
|
+
cmd('aw memory invalidate <memory-id>', 'Mark a memory as invalidated'),
|
|
386
|
+
cmd(' --reason "text"', 'Reason for invalidation'),
|
|
387
|
+
'',
|
|
388
|
+
cmd('aw memory sync', 'Sync memories to local files'),
|
|
389
|
+
cmd(' --force', 'Force re-sync even if cache is fresh'),
|
|
390
|
+
'',
|
|
260
391
|
].join('\n');
|
|
261
392
|
|
|
262
393
|
console.log(help);
|
package/ecc.mjs
CHANGED
|
@@ -9,7 +9,7 @@ import * as fmt from "./fmt.mjs";
|
|
|
9
9
|
|
|
10
10
|
const AW_ECC_REPO_SSH = "git@github.com:shreyansh-ghl/aw-ecc.git";
|
|
11
11
|
const AW_ECC_REPO_HTTPS = "https://github.com/shreyansh-ghl/aw-ecc.git";
|
|
12
|
-
const AW_ECC_TAG = "v1.
|
|
12
|
+
const AW_ECC_TAG = "v1.4.0";
|
|
13
13
|
|
|
14
14
|
const MARKETPLACE_NAME = "aw-marketplace";
|
|
15
15
|
const PLUGIN_KEY = `aw@${MARKETPLACE_NAME}`;
|
package/memory-sync.mjs
ADDED
|
@@ -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.
|
|
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"
|