@ghl-ai/aw 0.1.37-beta.44 → 0.1.37-beta.46

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 CHANGED
@@ -353,7 +353,6 @@ export async function initCommand(args) {
353
353
  try {
354
354
  await initPersistentClone(repoUrl, AW_HOME, sparsePaths);
355
355
  ensureAwGitignore(AW_HOME);
356
- mkdirSync(join(GLOBAL_AW_DIR, 'memory'), { recursive: true });
357
356
  s.stop('Registry cloned');
358
357
  } catch (e) {
359
358
  s.stop(chalk.red('Clone failed'));
@@ -382,6 +381,9 @@ export async function initCommand(args) {
382
381
  }
383
382
  }
384
383
 
384
+ // Create memory dir after symlink so GLOBAL_AW_DIR resolves correctly
385
+ mkdirSync(join(GLOBAL_AW_DIR, 'memory'), { recursive: true });
386
+
385
387
  // Create sync config — default to 'platform' when no namespace specified
386
388
  const cfg = config.create(GLOBAL_AW_DIR, { namespace: team || 'platform', user });
387
389
  if (folderName) {
@@ -6,10 +6,32 @@ import { homedir } from 'node:os';
6
6
  import { confirm, isCancel, text } from '@clack/prompts';
7
7
  import * as fmt from '../fmt.mjs';
8
8
  import { chalk } from '../fmt.mjs';
9
- import { callMemoryTool } from '../memory-bridge.mjs';
9
+ import { callMemoryTool, computeAncestry, resolveStoreNamespace, resolveReadPaths } from '../memory-bridge.mjs';
10
10
  import { syncMemories } from '../memory-sync.mjs';
11
- import { REGISTRY_DIR } from '../constants.mjs';
11
+ import { REGISTRY_DIR, ORG_SCOPE_NAMESPACE } from '../constants.mjs';
12
12
  import { getQueueStats } from '../memory-queue.mjs';
13
+ import * as config from '../config.mjs';
14
+
15
+ /** Load .sync-config.json from the registry directory. */
16
+ function loadCfg() {
17
+ const registryDir = join(homedir(), '.aw', REGISTRY_DIR);
18
+ return config.load(registryDir) || {};
19
+ }
20
+
21
+ /**
22
+ * Validate that an explicit namespace is within the user's ancestry chain.
23
+ * Returns true if valid, false if not.
24
+ */
25
+ function validateNamespaceAccess(cfg, explicitNs) {
26
+ const includes = cfg.include || [];
27
+ // Build full ancestry from the user's include paths
28
+ const validPaths = includes.length > 0
29
+ ? computeAncestry(includes)
30
+ : [cfg.namespace || ORG_SCOPE_NAMESPACE];
31
+ // Always allow the org root
32
+ if (!validPaths.includes(ORG_SCOPE_NAMESPACE)) validPaths.push(ORG_SCOPE_NAMESPACE);
33
+ return validPaths.includes(explicitNs);
34
+ }
13
35
 
14
36
  // ── legacy import ─────────────────────────────────────────────────────
15
37
 
@@ -155,18 +177,34 @@ async function memoryStore(args) {
155
177
 
156
178
  fmt.intro('aw memory store');
157
179
 
180
+ const cfg = loadCfg();
181
+ const explicitNs = args['--namespace'] || null;
182
+
183
+ // Validate explicit namespace is within ancestry chain
184
+ if (explicitNs && !validateNamespaceAccess(cfg, explicitNs)) {
185
+ fmt.cancel(`Namespace "${explicitNs}" is not in your ancestry chain. Your include paths: ${(cfg.include || []).join(', ') || '(none)'}`);
186
+ return;
187
+ }
188
+
189
+ // Resolve store namespace via layered defaults
190
+ const storeNs = resolveStoreNamespace(cfg, explicitNs);
191
+
158
192
  const params = { content };
159
193
  if (args['--type']) params.type = args['--type'];
160
- if (args['--namespace']) params.namespace = args['--namespace'];
194
+ if (storeNs) params.namespace = storeNs;
161
195
  if (args['--layer']) params.layer = args['--layer'];
162
196
  if (args['--overlay']) params.overlay = args['--overlay'].split(',').map(s => s.trim());
163
197
  if (args['--angle']) params.angle = args['--angle'].split(',').map(s => s.trim());
164
198
  if (args['--tags']) params.tags = args['--tags'].split(',').map(s => s.trim());
165
199
 
200
+ // Build call opts
201
+ const callOpts = {};
202
+ if (storeNs) callOpts.storeNamespace = storeNs;
203
+
166
204
  const s = fmt.spinner();
167
205
  s.start('Storing memory...');
168
206
  try {
169
- const result = await callMemoryTool('memory_curated_store', params);
207
+ const result = await callMemoryTool('memory_curated_store', params, callOpts);
170
208
  s.stop('Memory stored');
171
209
 
172
210
  const data = result?.result ?? result;
@@ -203,17 +241,29 @@ async function memorySearch(args) {
203
241
 
204
242
  fmt.intro('aw memory search');
205
243
 
244
+ const cfg = loadCfg();
245
+ const explicitNs = args['--namespace'] || null;
246
+
247
+ // Resolve read paths via layered defaults
248
+ const readPaths = resolveReadPaths(cfg, explicitNs);
249
+
206
250
  const params = { query };
207
- if (args['--namespace']) params.namespace = args['--namespace'];
251
+ if (explicitNs) params.namespace = explicitNs;
208
252
  if (args['--limit']) params.limit = parseInt(args['--limit'], 10);
209
253
  if (args['--layer']) params.layer = args['--layer'];
210
254
  if (args['--overlay']) params.overlay = args['--overlay'];
211
255
  if (args['--angle']) params.angle = args['--angle'];
212
256
 
257
+ // Override namespace paths header when explicit ns is provided
258
+ const callOpts = {};
259
+ if (explicitNs) {
260
+ callOpts.namespacePaths = readPaths.join(',');
261
+ }
262
+
213
263
  const s = fmt.spinner();
214
264
  s.start('Searching memories...');
215
265
  try {
216
- const result = await callMemoryTool('memory_search', params);
266
+ const result = await callMemoryTool('memory_search', params, callOpts);
217
267
  s.stop('Search complete');
218
268
 
219
269
  const memories = Array.isArray(result) ? result : (result?.memories ?? result?.results ?? []);
@@ -259,18 +309,30 @@ async function memoryPack(args) {
259
309
 
260
310
  fmt.intro('aw memory pack');
261
311
 
312
+ const cfg = loadCfg();
313
+ const explicitNs = args['--namespace'] || null;
314
+
315
+ // Resolve read paths via layered defaults (same as search)
316
+ const readPaths = resolveReadPaths(cfg, explicitNs);
317
+
262
318
  const params = { query };
263
- if (args['--namespace']) params.namespace = args['--namespace'];
319
+ if (explicitNs) params.namespace = explicitNs;
264
320
  if (args['--budget']) params.token_budget = parseInt(args['--budget'], 10);
265
321
  else params.token_budget = 3500;
266
322
  if (args['--layer']) params.layer = args['--layer'];
267
323
  if (args['--overlay']) params.overlay = args['--overlay'];
268
324
  if (args['--angle']) params.angle = args['--angle'];
269
325
 
326
+ // Override namespace paths header when explicit ns is provided
327
+ const callOpts = {};
328
+ if (explicitNs) {
329
+ callOpts.namespacePaths = readPaths.join(',');
330
+ }
331
+
270
332
  const s = fmt.spinner();
271
333
  s.start('Building memory pack...');
272
334
  try {
273
- const result = await callMemoryTool('memory_pack', params);
335
+ const result = await callMemoryTool('memory_pack', params, callOpts);
274
336
  s.stop('Pack ready');
275
337
 
276
338
  const pack = result?.pack ?? result?.content ?? result;
@@ -1261,3 +1323,6 @@ function memoryHelp() {
1261
1323
 
1262
1324
  console.log(help);
1263
1325
  }
1326
+
1327
+ // ── test-only exports ────────────────────────────────────────────────
1328
+ export const _test = { rateMemoryQuality, getQualityIssue, buildPromoteContent, normalizeForGrouping, formatTimeAgo };
package/constants.mjs CHANGED
@@ -37,3 +37,6 @@ export const AW_CO_AUTHOR = `Co-Authored-By: ${AW_BOT_NAME} <${AW_BOT_EMAIL}>`;
37
37
 
38
38
  /** MCP base URL for memory and other tool calls — override with AW_MCP_URL env var */
39
39
  export const MCP_BASE_URL = process.env.AW_MCP_URL || 'http://localhost:3100/agentic-workspace/mcp';
40
+
41
+ /** Default org-level namespace — always included in ancestry */
42
+ export const ORG_SCOPE_NAMESPACE = 'platform';
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.4.1";
12
+ const AW_ECC_TAG = "v1.4.3";
13
13
 
14
14
  const MARKETPLACE_NAME = "aw-marketplace";
15
15
  const PLUGIN_KEY = `aw@${MARKETPLACE_NAME}`;
package/memory-bridge.mjs CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  import { execSync } from 'node:child_process';
4
4
  import { join } from 'node:path';
5
- import { MCP_BASE_URL, AW_HOME, REGISTRY_DIR } from './constants.mjs';
5
+ import { MCP_BASE_URL, AW_HOME, REGISTRY_DIR, ORG_SCOPE_NAMESPACE } from './constants.mjs';
6
6
  import * as config from './config.mjs';
7
7
  import { enqueue, flushQueue } from './memory-queue.mjs';
8
8
 
@@ -24,9 +24,21 @@ function resolveHeaders() {
24
24
  'Accept': 'application/json, text/event-stream',
25
25
  };
26
26
 
27
- // Resolve namespace from .sync-config.json → X-Namespace header
27
+ // Resolve namespace paths from .sync-config.json
28
28
  const registryDir = join(AW_HOME, REGISTRY_DIR);
29
29
  const cfg = config.load(registryDir);
30
+
31
+ // Send ALL initialized namespace paths (from include[]) + platform
32
+ const paths = [...(cfg?.include || [])];
33
+ if (!paths.some(p => p === ORG_SCOPE_NAMESPACE || p.startsWith(ORG_SCOPE_NAMESPACE + '/'))) {
34
+ paths.push(ORG_SCOPE_NAMESPACE);
35
+ }
36
+ headers['X-Namespace-Paths'] = paths.join(',');
37
+
38
+ // Send GitHub username
39
+ headers['X-Github-User'] = cfg?.user || '';
40
+
41
+ // Keep X-Namespace for backwards compat
30
42
  if (cfg?.namespace) {
31
43
  headers['X-Namespace'] = cfg.namespace;
32
44
  }
@@ -56,6 +68,57 @@ function resolveGhToken() {
56
68
  return null;
57
69
  }
58
70
 
71
+ /**
72
+ * Compute the full ancestry chain for a set of namespace paths.
73
+ * E.g. ['platform/scheduling/calendars'] → ['platform/scheduling/calendars', 'platform/scheduling', 'platform']
74
+ */
75
+ export function computeAncestry(paths) {
76
+ const result = new Set();
77
+ for (const path of paths) {
78
+ const segments = path.split('/');
79
+ for (let i = segments.length; i >= 1; i--) {
80
+ result.add(segments.slice(0, i).join('/'));
81
+ }
82
+ }
83
+ result.add(ORG_SCOPE_NAMESPACE);
84
+ return [...result];
85
+ }
86
+
87
+ /**
88
+ * Resolve the store namespace using layered defaults.
89
+ * Layer 1: Explicit (from --namespace flag)
90
+ * Layer 2: Single include path
91
+ * Layer 3: Multi include — return null (curation picks)
92
+ * Layer 4: Fallback to cfg.namespace or 'platform'
93
+ */
94
+ export function resolveStoreNamespace(cfg, explicitNs) {
95
+ // Layer 1: Explicit
96
+ if (explicitNs) return explicitNs;
97
+ // Layer 2: Single include
98
+ const includes = cfg?.include || [];
99
+ if (includes.length === 1) return includes[0];
100
+ // Layer 3: Multi include — return null (curation picks)
101
+ if (includes.length > 1) return null;
102
+ // Layer 4: Fallback
103
+ return cfg?.namespace || ORG_SCOPE_NAMESPACE;
104
+ }
105
+
106
+ /**
107
+ * Resolve read namespace paths using layered defaults.
108
+ * Returns a full ancestry chain for the resolved paths.
109
+ */
110
+ export function resolveReadPaths(cfg, explicitNs) {
111
+ // Layer 1: Explicit
112
+ if (explicitNs) return computeAncestry([explicitNs]);
113
+ // Layer 2: Single include
114
+ const includes = cfg?.include || [];
115
+ if (includes.length === 1) return computeAncestry(includes);
116
+ // Layer 3: Multi include
117
+ if (includes.length > 1) return computeAncestry(includes);
118
+ // Layer 4: Fallback
119
+ return [ORG_SCOPE_NAMESPACE];
120
+ }
121
+
59
122
  /** Write operations that can be queued when MCP is unreachable. */
60
123
  const WRITE_OPS = new Set([
61
124
  'memory_store',
@@ -112,11 +175,14 @@ async function tryFlushQueue() {
112
175
  * - On success, flushes any queued operations
113
176
  * @param {string} toolName — MCP tool name (e.g. 'memory_curated_store', 'memory_search')
114
177
  * @param {object} params — Tool arguments
178
+ * @param {object} [opts] — Optional overrides
179
+ * @param {string} [opts.storeNamespace] — If set, adds X-Store-Namespace header
180
+ * @param {string} [opts.namespacePaths] — If set, overrides X-Namespace-Paths header
115
181
  * @returns {Promise<object>} Parsed tool result (or synthetic {queued:true})
116
182
  */
117
- export async function callMemoryTool(toolName, params) {
183
+ export async function callMemoryTool(toolName, params, opts = {}) {
118
184
  try {
119
- const result = await _callMemoryToolRaw(toolName, params);
185
+ const result = await _callMemoryToolRaw(toolName, params, opts);
120
186
  // MCP is reachable — flush any queued items in the background
121
187
  tryFlushQueue();
122
188
  return result;
@@ -133,12 +199,20 @@ export async function callMemoryTool(toolName, params) {
133
199
  * Raw MCP transport — JSON-RPC 2.0 over Streamable HTTP.
134
200
  * @param {string} toolName
135
201
  * @param {object} params
202
+ * @param {object} [opts]
136
203
  * @returns {Promise<object>}
137
204
  */
138
- async function _callMemoryToolRaw(toolName, params) {
205
+ async function _callMemoryToolRaw(toolName, params, opts = {}) {
206
+ const headers = { ...resolveHeaders() };
207
+ if (opts.storeNamespace) {
208
+ headers['X-Store-Namespace'] = opts.storeNamespace;
209
+ }
210
+ if (opts.namespacePaths) {
211
+ headers['X-Namespace-Paths'] = opts.namespacePaths;
212
+ }
139
213
  const response = await fetch(MCP_BASE_URL, {
140
214
  method: 'POST',
141
- headers: resolveHeaders(),
215
+ headers,
142
216
  body: JSON.stringify({
143
217
  jsonrpc: '2.0',
144
218
  id: _rpcId++,
package/memory-queue.mjs CHANGED
@@ -4,8 +4,8 @@ import { readFileSync, writeFileSync, appendFileSync, existsSync, mkdirSync } fr
4
4
  import { join, dirname } from 'node:path';
5
5
  import { AW_HOME } from './constants.mjs';
6
6
 
7
- /** Queue file path: ~/.aw/memory-queue.jsonl */
8
- const QUEUE_PATH = join(AW_HOME, 'memory-queue.jsonl');
7
+ /** Queue file path: ~/.aw/memory-queue.jsonl (resolved lazily for testability) */
8
+ function getQueuePath() { return join(AW_HOME, 'memory-queue.jsonl'); }
9
9
 
10
10
  /** Max age for queued items before they are discarded (7 days in ms) */
11
11
  const MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
@@ -16,7 +16,8 @@ const MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
16
16
  * @param {object} payload — Tool arguments
17
17
  */
18
18
  export function enqueue(op, payload) {
19
- const dir = dirname(QUEUE_PATH);
19
+ const queuePath = getQueuePath();
20
+ const dir = dirname(queuePath);
20
21
  if (!existsSync(dir)) {
21
22
  mkdirSync(dir, { recursive: true });
22
23
  }
@@ -28,7 +29,7 @@ export function enqueue(op, payload) {
28
29
  attempt: 0,
29
30
  };
30
31
 
31
- appendFileSync(QUEUE_PATH, JSON.stringify(entry) + '\n', 'utf8');
32
+ appendFileSync(queuePath, JSON.stringify(entry) + '\n', 'utf8');
32
33
  }
33
34
 
34
35
  /**
@@ -39,11 +40,12 @@ export function enqueue(op, payload) {
39
40
  * @returns {Promise<{processed: number, failed: number, discarded: number}>}
40
41
  */
41
42
  export async function flushQueue(sender) {
42
- if (!existsSync(QUEUE_PATH)) {
43
+ const queuePath = getQueuePath();
44
+ if (!existsSync(queuePath)) {
43
45
  return { processed: 0, failed: 0, discarded: 0 };
44
46
  }
45
47
 
46
- const raw = readFileSync(QUEUE_PATH, 'utf8').trim();
48
+ const raw = readFileSync(queuePath, 'utf8').trim();
47
49
  if (!raw) {
48
50
  return { processed: 0, failed: 0, discarded: 0 };
49
51
  }
@@ -85,9 +87,9 @@ export async function flushQueue(sender) {
85
87
  // Rewrite queue with only the remaining (failed) items
86
88
  if (remaining.length > 0) {
87
89
  const data = remaining.map(e => JSON.stringify(e)).join('\n') + '\n';
88
- writeFileSync(QUEUE_PATH, data, 'utf8');
90
+ writeFileSync(queuePath, data, 'utf8');
89
91
  } else {
90
- writeFileSync(QUEUE_PATH, '', 'utf8');
92
+ writeFileSync(queuePath, '', 'utf8');
91
93
  }
92
94
 
93
95
  return { processed, failed, discarded };
@@ -98,11 +100,12 @@ export async function flushQueue(sender) {
98
100
  * @returns {{pending: number, oldest: string|null, newest: string|null}}
99
101
  */
100
102
  export function getQueueStats() {
101
- if (!existsSync(QUEUE_PATH)) {
103
+ const queuePath = getQueuePath();
104
+ if (!existsSync(queuePath)) {
102
105
  return { pending: 0, oldest: null, newest: null };
103
106
  }
104
107
 
105
- const raw = readFileSync(QUEUE_PATH, 'utf8').trim();
108
+ const raw = readFileSync(queuePath, 'utf8').trim();
106
109
  if (!raw) {
107
110
  return { pending: 0, oldest: null, newest: null };
108
111
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ghl-ai/aw",
3
- "version": "0.1.37-beta.44",
3
+ "version": "0.1.37-beta.46",
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",
@@ -45,7 +45,7 @@
45
45
  "license": "MIT",
46
46
  "scripts": {
47
47
  "test": "yarn test:vitest && yarn test:node",
48
- "test:vitest": "vitest run --reporter=verbose tests/commands tests/mcp.test.mjs tests/telemetry.test.mjs",
48
+ "test:vitest": "vitest run --reporter=verbose tests/commands tests/mcp.test.mjs tests/telemetry.test.mjs tests/memory-queue.test.mjs tests/memory-sync.test.mjs tests/memory-bridge.test.mjs",
49
49
  "test:node": "node tests/run-node-tests.mjs",
50
50
  "test:watch": "vitest --reporter=verbose tests/commands tests/mcp.test.mjs tests/telemetry.test.mjs",
51
51
  "preuninstall": "node bin.js nuke 2>/dev/null || true"