@ghl-ai/aw 0.1.25-beta.9 → 0.1.26-beta.1

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/cli.mjs CHANGED
@@ -71,8 +71,9 @@ function printHelp() {
71
71
  const sec = (title) => `\n ${chalk.bold.underline(title)}`;
72
72
  const help = [
73
73
  sec('Setup'),
74
- cmd('aw init --namespace <team>', 'Initialize workspace (required)'),
74
+ cmd('aw init --namespace <team/sub-team>', 'Initialize workspace (required)'),
75
75
  ` ${chalk.dim('Teams: revex, mobile, commerce, leadgen, crm, marketplace, ai')}`,
76
+ ` ${chalk.dim('Example: aw init --namespace revex/courses')}`,
76
77
 
77
78
  sec('Download'),
78
79
  cmd('aw pull', 'Re-pull all synced paths (like git pull)'),
package/commands/init.mjs CHANGED
@@ -146,26 +146,47 @@ export async function initCommand(args) {
146
146
  fmt.cancel([
147
147
  `Missing required ${chalk.bold('--namespace')} flag`,
148
148
  '',
149
- ` ${chalk.dim('Usage:')} aw init --namespace <team>`,
149
+ ` ${chalk.dim('Usage:')} aw init --namespace <team/sub-team>`,
150
150
  ` ${chalk.dim('Teams:')} ${list}`,
151
151
  '',
152
- ` ${chalk.dim('Example:')} ${chalk.bold('aw init --namespace commerce')}`,
152
+ ` ${chalk.dim('Example:')} ${chalk.bold('aw init --namespace commerce/payments')}`,
153
153
  ].join('\n'));
154
154
  }
155
155
 
156
- if (namespace && !ALLOWED_NAMESPACES.includes(namespace)) {
156
+ // Parse team/sub-team
157
+ const nsParts = namespace ? namespace.split('/') : [];
158
+ const team = nsParts[0] || null;
159
+ const subTeam = nsParts[1] || null;
160
+ const teamNS = subTeam ? `${team}-${subTeam}` : team; // for $TEAM_NS replacement
161
+ const folderName = subTeam ? `${team}/${subTeam}` : team; // for .aw_registry/ path
162
+
163
+ if (team && !ALLOWED_NAMESPACES.includes(team)) {
157
164
  const list = ALLOWED_NAMESPACES.map(n => chalk.cyan(n)).join(', ');
158
165
  fmt.cancel([
159
- `Unknown namespace ${chalk.red(namespace)}`,
166
+ `Unknown team ${chalk.red(team)}`,
160
167
  '',
161
168
  ` ${chalk.dim('Allowed:')} ${list}`,
162
169
  '',
163
- ` ${chalk.dim('Example:')} ${chalk.bold('aw init --namespace commerce')}`,
170
+ ` ${chalk.dim('Example:')} ${chalk.bold('aw init --namespace commerce/payments')}`,
171
+ ].join('\n'));
172
+ }
173
+
174
+ if (team && !subTeam) {
175
+ fmt.cancel([
176
+ `Missing sub-team in ${chalk.red(namespace)}`,
177
+ '',
178
+ ` ${chalk.dim('Format:')} --namespace <team>/<sub-team>`,
179
+ '',
180
+ ` ${chalk.dim('Example:')} ${chalk.bold(`aw init --namespace ${team}/courses`)}`,
164
181
  ].join('\n'));
165
182
  }
166
183
 
167
- if (namespace && !/^[a-z][a-z0-9-]{1,38}[a-z0-9]$/.test(namespace)) {
168
- fmt.cancel(`Invalid namespace '${namespace}' — must match: ^[a-z][a-z0-9-]{1,38}[a-z0-9]$`);
184
+ const SLUG_RE = /^[a-z][a-z0-9-]{1,38}[a-z0-9]$/;
185
+ if (team && !SLUG_RE.test(team)) {
186
+ fmt.cancel(`Invalid team '${team}' — must match: ${SLUG_RE}`);
187
+ }
188
+ if (subTeam && !SLUG_RE.test(subTeam)) {
189
+ fmt.cancel(`Invalid sub-team '${subTeam}' — must match: ${SLUG_RE}`);
169
190
  }
170
191
 
171
192
  const hasConfig = config.exists(GLOBAL_AW_DIR);
@@ -176,21 +197,40 @@ export async function initCommand(args) {
176
197
  // ── Fast path: already initialized → just pull + link ─────────────────
177
198
 
178
199
  if (isExisting) {
179
- if (!silent) fmt.logStep('Already initialized — syncing...');
200
+ const cfg = config.load(GLOBAL_AW_DIR);
201
+
202
+ // Add new sub-team if not already tracked
203
+ const isNewSubTeam = folderName && cfg && !cfg.include.includes(folderName);
204
+ if (isNewSubTeam) {
205
+ if (!silent) fmt.logStep(`Adding sub-team ${chalk.cyan(folderName)}...`);
206
+ await pullAsync({
207
+ ...args,
208
+ _positional: ['[template]'],
209
+ _renameNamespace: folderName,
210
+ _teamNS: teamNS,
211
+ _workspaceDir: GLOBAL_AW_DIR,
212
+ _skipIntegrate: true,
213
+ });
214
+ config.addPattern(GLOBAL_AW_DIR, folderName);
215
+ } else {
216
+ if (!silent) fmt.logStep('Already initialized — syncing...');
217
+ }
180
218
 
181
219
  // Pull latest (parallel)
182
- // cfg.include has the renamed namespace (e.g. 'revex'), but the repo
183
- // only has 'registry/[template]/' — remap non-platform entries back.
184
- const cfg = config.load(GLOBAL_AW_DIR);
185
- if (cfg && cfg.include.length > 0) {
186
- const pullJobs = cfg.include.map(p => {
220
+ // cfg.include has the renamed namespace (e.g. 'revex/courses'), but the repo
221
+ // only has '.aw_registry/[template]/' — remap non-platform entries back.
222
+ const freshCfg = config.load(GLOBAL_AW_DIR);
223
+ if (freshCfg && freshCfg.include.length > 0) {
224
+ const pullJobs = freshCfg.include.map(p => {
187
225
  const isTeamNs = p !== 'platform';
226
+ const derivedTeamNS = isTeamNs ? p.replace(/\//g, '-') : undefined;
188
227
  return pullAsync({
189
228
  ...args,
190
229
  _positional: [isTeamNs ? '[template]' : p],
191
230
  _workspaceDir: GLOBAL_AW_DIR,
192
231
  _skipIntegrate: true,
193
232
  _renameNamespace: isTeamNs ? p : undefined,
233
+ _teamNS: derivedTeamNS,
194
234
  });
195
235
  });
196
236
  await Promise.all(pullJobs);
@@ -199,9 +239,9 @@ export async function initCommand(args) {
199
239
  // Re-link IDE dirs (idempotent)
200
240
  linkWorkspace(HOME);
201
241
  generateCommands(HOME);
202
- copyInstructions(HOME, null, namespace) || [];
242
+ copyInstructions(HOME, null, freshCfg?.namespace || team) || [];
203
243
  initAwDocs(HOME);
204
- setupMcp(HOME, namespace) || [];
244
+ setupMcp(HOME, freshCfg?.namespace || team) || [];
205
245
 
206
246
  // Link current project if needed
207
247
  if (cwd !== HOME && !existsSync(join(cwd, '.aw_registry'))) {
@@ -213,7 +253,7 @@ export async function initCommand(args) {
213
253
 
214
254
  if (!silent) {
215
255
  fmt.outro([
216
- 'Sync complete',
256
+ isNewSubTeam ? `Sub-team ${chalk.cyan(folderName)} added` : 'Sync complete',
217
257
  '',
218
258
  ` ${chalk.green('✓')} Registry updated`,
219
259
  ` ${chalk.green('✓')} IDE integration refreshed`,
@@ -235,26 +275,26 @@ export async function initCommand(args) {
235
275
  // Step 1: Create global source of truth
236
276
  mkdirSync(GLOBAL_AW_DIR, { recursive: true });
237
277
 
238
- const cfg = config.create(GLOBAL_AW_DIR, { namespace, user });
278
+ const cfg = config.create(GLOBAL_AW_DIR, { namespace: team, user });
239
279
 
240
280
  fmt.note([
241
281
  `${chalk.dim('source:')} ~/.aw_registry/`,
242
- namespace ? `${chalk.dim('namespace:')} ${cfg.namespace}` : `${chalk.dim('namespace:')} ${chalk.dim('none')}`,
282
+ folderName ? `${chalk.dim('namespace:')} ${folderName}` : `${chalk.dim('namespace:')} ${chalk.dim('none')}`,
243
283
  user ? `${chalk.dim('user:')} ${cfg.user}` : null,
244
284
  `${chalk.dim('version:')} v${VERSION}`,
245
285
  ].filter(Boolean).join('\n'), 'Config created');
246
286
 
247
287
  // Step 2: Pull registry content (parallel)
248
288
  const s = fmt.spinner();
249
- const pullTargets = namespace ? `platform + ${namespace}` : 'platform';
289
+ const pullTargets = folderName ? `platform + ${folderName}` : 'platform';
250
290
  s.start(`Pulling ${pullTargets}...`);
251
291
 
252
292
  const pullJobs = [
253
293
  pullAsync({ ...args, _positional: ['platform'], _workspaceDir: GLOBAL_AW_DIR, _skipIntegrate: true }),
254
294
  ];
255
- if (namespace) {
295
+ if (folderName) {
256
296
  pullJobs.push(
257
- pullAsync({ ...args, _positional: ['[template]'], _renameNamespace: namespace, _workspaceDir: GLOBAL_AW_DIR, _skipIntegrate: true }),
297
+ pullAsync({ ...args, _positional: ['[template]'], _renameNamespace: folderName, _teamNS: teamNS, _workspaceDir: GLOBAL_AW_DIR, _skipIntegrate: true }),
258
298
  );
259
299
  }
260
300
 
@@ -275,9 +315,9 @@ export async function initCommand(args) {
275
315
  fmt.logStep('Linking IDE symlinks...');
276
316
  linkWorkspace(HOME);
277
317
  generateCommands(HOME);
278
- const instructionFiles = copyInstructions(HOME, null, namespace) || [];
318
+ const instructionFiles = copyInstructions(HOME, null, team) || [];
279
319
  initAwDocs(HOME);
280
- const mcpFiles = setupMcp(HOME, namespace) || [];
320
+ const mcpFiles = setupMcp(HOME, team) || [];
281
321
  const gitTemplateInstalled = installGitTemplate();
282
322
  installIdeTasks();
283
323
 
package/commands/pull.mjs CHANGED
@@ -8,6 +8,7 @@ import * as config from '../config.mjs';
8
8
  import * as fmt from '../fmt.mjs';
9
9
  import { chalk } from '../fmt.mjs';
10
10
  import { sparseCheckout, sparseCheckoutAsync, cleanup, includeToSparsePaths } from '../git.mjs';
11
+ import { REGISTRY_DIR } from '../constants.mjs';
11
12
  import { walkRegistryTree } from '../registry.mjs';
12
13
  import { matchesAny } from '../glob.mjs';
13
14
  import { computePlan } from '../plan.mjs';
@@ -17,6 +18,13 @@ import { resolveInput } from '../paths.mjs';
17
18
  import { linkWorkspace } from '../link.mjs';
18
19
  import { generateCommands, copyInstructions } from '../integrate.mjs';
19
20
 
21
+ // Filter out top-level platform CLI meta-commands (drop, pull, push, etc.)
22
+ // but keep domain-specific commands (platform/design/commands/, platform/infra/commands/).
23
+ function filterActions(actions, pattern) {
24
+ if (pattern !== 'platform') return actions;
25
+ return actions.filter(a => !(a.type === 'commands' && a.namespacePath === 'platform'));
26
+ }
27
+
20
28
  export async function pullCommand(args) {
21
29
  const input = args._positional?.[0] || '';
22
30
  const cwd = process.cwd();
@@ -27,6 +35,7 @@ export async function pullCommand(args) {
27
35
  const verbose = args['-v'] === true || args['--verbose'] === true;
28
36
  const silent = args['--silent'] === true || args._silent === true;
29
37
  const renameNamespace = args._renameNamespace || null;
38
+ const teamNSOverride = args._teamNS || null;
30
39
 
31
40
  // Silent mode: wrap fmt to suppress all output and exit cleanly on errors
32
41
  const log = {
@@ -51,12 +60,14 @@ export async function pullCommand(args) {
51
60
  log.logInfo(`Pulling ${chalk.cyan(cfg.include.length)} synced path${cfg.include.length > 1 ? 's' : ''}...`);
52
61
  const pullJobs = cfg.include.map(p => {
53
62
  const isTeamNs = p !== 'platform';
63
+ const derivedTeamNS = isTeamNs ? p.replace(/\//g, '-') : undefined;
54
64
  return pullAsync({
55
65
  ...args,
56
66
  _positional: [isTeamNs ? '[template]' : p],
57
67
  _workspaceDir: workspaceDir,
58
68
  _skipIntegrate: true,
59
69
  _renameNamespace: isTeamNs ? p : undefined,
70
+ _teamNS: derivedTeamNS,
60
71
  });
61
72
  });
62
73
  await Promise.all(pullJobs);
@@ -101,7 +112,7 @@ export async function pullCommand(args) {
101
112
 
102
113
  try {
103
114
  const registryDirs = [];
104
- const regBase = join(tempDir, 'registry');
115
+ const regBase = join(tempDir, REGISTRY_DIR);
105
116
 
106
117
  if (existsSync(regBase)) {
107
118
  for (const name of listDirs(regBase)) {
@@ -153,9 +164,10 @@ export async function pullCommand(args) {
153
164
  }
154
165
 
155
166
  // Compute plan
156
- const { actions } = computePlan(registryDirs, workspaceDir, [pattern], {
167
+ const { actions: rawActions } = computePlan(registryDirs, workspaceDir, [pattern], {
157
168
  skipOrphans: true,
158
169
  });
170
+ const actions = filterActions(rawActions, pattern);
159
171
 
160
172
  if (dryRun) {
161
173
  printDryRun(actions, verbose);
@@ -165,7 +177,7 @@ export async function pullCommand(args) {
165
177
  // Apply
166
178
  const s2 = log.spinner();
167
179
  s2.start('Applying changes...');
168
- const conflictCount = applyActions(actions, { teamNS: renameNamespace || undefined });
180
+ const conflictCount = applyActions(actions, { teamNS: teamNSOverride || renameNamespace || undefined });
169
181
  updateManifest(workspaceDir, actions, cfg.namespace);
170
182
  s2.stop('Changes applied');
171
183
 
@@ -208,6 +220,7 @@ export async function pullAsync(args) {
208
220
  const input = args._positional?.[0] || '';
209
221
  const workspaceDir = args._workspaceDir;
210
222
  const renameNamespace = args._renameNamespace || null;
223
+ const teamNSOverride = args._teamNS || null;
211
224
 
212
225
  const resolved = resolveInput(input, workspaceDir);
213
226
  let pattern = resolved.registryPath;
@@ -223,7 +236,7 @@ export async function pullAsync(args) {
223
236
 
224
237
  try {
225
238
  const registryDirs = [];
226
- const regBase = join(tempDir, 'registry');
239
+ const regBase = join(tempDir, REGISTRY_DIR);
227
240
 
228
241
  if (existsSync(regBase)) {
229
242
  for (const name of listDirs(regBase)) {
@@ -259,8 +272,9 @@ export async function pullAsync(args) {
259
272
  config.addPattern(workspaceDir, pattern);
260
273
  }
261
274
 
262
- const { actions } = computePlan(registryDirs, workspaceDir, [pattern], { skipOrphans: true });
263
- const conflictCount = applyActions(actions, { teamNS: renameNamespace || undefined });
275
+ const { actions: rawActions } = computePlan(registryDirs, workspaceDir, [pattern], { skipOrphans: true });
276
+ const actions = filterActions(rawActions, pattern);
277
+ const conflictCount = applyActions(actions, { teamNS: teamNSOverride || renameNamespace || undefined });
264
278
  updateManifest(workspaceDir, actions, cfg.namespace);
265
279
 
266
280
  const ROOT_REGISTRY_FILES = ['AW-PROTOCOL.md'];
package/commands/push.mjs CHANGED
@@ -6,7 +6,7 @@ import { execSync, execFileSync } from 'node:child_process';
6
6
  import { tmpdir } from 'node:os';
7
7
  import * as fmt from '../fmt.mjs';
8
8
  import { chalk } from '../fmt.mjs';
9
- import { REGISTRY_REPO, REGISTRY_BASE_BRANCH } from '../constants.mjs';
9
+ import { REGISTRY_REPO, REGISTRY_BASE_BRANCH, REGISTRY_DIR } from '../constants.mjs';
10
10
  import { resolveInput } from '../paths.mjs';
11
11
  import { load as loadManifest } from '../manifest.mjs';
12
12
  import { hashFile } from '../registry.mjs';
@@ -98,8 +98,8 @@ export function pushCommand(args) {
98
98
 
99
99
  const isDir = statSync(absPath).isDirectory();
100
100
  const registryTarget = isDir
101
- ? `registry/${namespacePath}/${parentDir}/${slug}`
102
- : `registry/${namespacePath}/${parentDir}/${slug}.md`;
101
+ ? `${REGISTRY_DIR}/${namespacePath}/${parentDir}/${slug}`
102
+ : `${REGISTRY_DIR}/${namespacePath}/${parentDir}/${slug}.md`;
103
103
 
104
104
  fmt.note(
105
105
  [
@@ -148,13 +148,13 @@ export function pushCommand(args) {
148
148
 
149
149
  // Check if this is a new namespace — auto-add CODEOWNERS entry
150
150
  let newNamespace = false;
151
- const nsDir = join(tempDir, 'registry', topNamespace);
151
+ const nsDir = join(tempDir, REGISTRY_DIR, topNamespace);
152
152
  const codeownersPath = join(tempDir, 'CODEOWNERS');
153
153
  if (!existsSync(nsDir) || isNewNamespaceInCodeowners(codeownersPath, topNamespace)) {
154
154
  newNamespace = true;
155
155
  const ghUser = getGitHubUser();
156
156
  if (ghUser && existsSync(codeownersPath)) {
157
- const line = `/registry/${topNamespace}/ @${ghUser}\n`;
157
+ const line = `/${REGISTRY_DIR}/${topNamespace}/ @${ghUser}\n`;
158
158
  appendFileSync(codeownersPath, line);
159
159
  execSync('git add CODEOWNERS', { cwd: tempDir, stdio: 'pipe' });
160
160
  }
@@ -236,5 +236,5 @@ function getGitHubUser() {
236
236
  function isNewNamespaceInCodeowners(codeownersPath, namespace) {
237
237
  if (!existsSync(codeownersPath)) return true;
238
238
  const content = readFileSync(codeownersPath, 'utf8');
239
- return !content.includes(`/registry/${namespace}/`);
239
+ return !content.includes(`/${REGISTRY_DIR}/${namespace}/`);
240
240
  }
@@ -7,7 +7,7 @@ import { execSync } from 'node:child_process';
7
7
  import * as config from '../config.mjs';
8
8
  import * as fmt from '../fmt.mjs';
9
9
  import { chalk } from '../fmt.mjs';
10
- import { REGISTRY_BASE_BRANCH, REGISTRY_REPO } from '../constants.mjs';
10
+ import { REGISTRY_BASE_BRANCH, REGISTRY_REPO, REGISTRY_DIR } from '../constants.mjs';
11
11
 
12
12
  export function searchCommand(args) {
13
13
  const query = (args._positional || []).join(' ').toLowerCase();
@@ -150,14 +150,14 @@ function searchRemote(repo, query) {
150
150
  try {
151
151
  // git archive fetches a single file from remote — no clone needed
152
152
  content = execSync(
153
- `git archive --remote="${repoUrl}" ${REGISTRY_BASE_BRANCH} registry/registry.json | tar -xO registry/registry.json`,
153
+ `git archive --remote="${repoUrl}" ${REGISTRY_BASE_BRANCH} ${REGISTRY_DIR}/registry.json | tar -xO ${REGISTRY_DIR}/registry.json`,
154
154
  { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }
155
155
  );
156
156
  } catch {
157
157
  // GitHub doesn't support git archive — fallback to gh API
158
158
  try {
159
159
  const raw = execSync(
160
- `gh api "repos/${repo}/contents/registry/registry.json?ref=${REGISTRY_BASE_BRANCH}" --jq .content -H "Accept: application/vnd.github.v3+json"`,
160
+ `gh api "repos/${repo}/contents/${REGISTRY_DIR}/registry.json?ref=${REGISTRY_BASE_BRANCH}" --jq .content -H "Accept: application/vnd.github.v3+json"`,
161
161
  { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }
162
162
  );
163
163
  content = Buffer.from(raw.trim(), 'base64').toString('utf8');
@@ -167,9 +167,9 @@ function searchRemote(repo, query) {
167
167
  try {
168
168
  execSync(`git clone --filter=blob:none --no-checkout "${repoUrl}" "${tempDir}"`, { stdio: 'pipe' });
169
169
  execSync('git sparse-checkout init --cone', { cwd: tempDir, stdio: 'pipe' });
170
- execSync('git sparse-checkout set --skip-checks "registry/registry.json"', { cwd: tempDir, stdio: 'pipe' });
170
+ execSync(`git sparse-checkout set --skip-checks "${REGISTRY_DIR}/registry.json"`, { cwd: tempDir, stdio: 'pipe' });
171
171
  execSync(`git checkout ${REGISTRY_BASE_BRANCH}`, { cwd: tempDir, stdio: 'pipe' });
172
- content = readFileSync(join(tempDir, 'registry', 'registry.json'), 'utf8');
172
+ content = readFileSync(join(tempDir, REGISTRY_DIR, 'registry.json'), 'utf8');
173
173
  } finally {
174
174
  try { execSync(`rm -rf "${tempDir}"`, { stdio: 'pipe' }); } catch {}
175
175
  }
package/constants.mjs CHANGED
@@ -1,7 +1,10 @@
1
1
  // constants.mjs — Single source of truth for registry settings.
2
2
 
3
3
  /** Base branch for PRs and sync checkout */
4
- export const REGISTRY_BASE_BRANCH = 'chore/restructure';
4
+ export const REGISTRY_BASE_BRANCH = 'feat/aw-platform-docs';
5
5
 
6
6
  /** Default registry repository */
7
- export const REGISTRY_REPO = 'GoHighLevel/ghl-agentic-workspace';
7
+ export const REGISTRY_REPO = 'GoHighLevel/platform-docs';
8
+
9
+ /** Directory inside the registry repo that holds platform/ and [template]/ */
10
+ export const REGISTRY_DIR = '.aw_registry';
package/git.mjs CHANGED
@@ -5,7 +5,7 @@ import { mkdtempSync, existsSync } from 'node:fs';
5
5
  import { join } from 'node:path';
6
6
  import { tmpdir } from 'node:os';
7
7
  import { promisify } from 'node:util';
8
- import { REGISTRY_BASE_BRANCH } from './constants.mjs';
8
+ import { REGISTRY_BASE_BRANCH, REGISTRY_DIR } from './constants.mjs';
9
9
 
10
10
  const exec = promisify(execCb);
11
11
 
@@ -76,16 +76,13 @@ export function cleanup(tempDir) {
76
76
 
77
77
  /**
78
78
  * Compute sparse checkout paths from include paths.
79
- * e.g., ["platform", "dev/agents/debugger"] -> ["registry/platform", "registry/dev/agents/debugger"]
79
+ * e.g., ["platform", "dev/agents/debugger"] -> [".aw_registry/platform", ".aw_registry/dev/agents/debugger"]
80
80
  */
81
81
  export function includeToSparsePaths(paths) {
82
82
  const result = new Set();
83
83
  for (const p of paths) {
84
- result.add(`registry/${p}`);
84
+ result.add(`${REGISTRY_DIR}/${p}`);
85
85
  }
86
- // Also fetch root instruction files
87
- result.add('registry/CLAUDE.md');
88
- result.add('registry/AGENTS.md');
89
- result.add('registry/AW-PROTOCOL.md');
86
+ result.add(`${REGISTRY_DIR}/AW-PROTOCOL.md`);
90
87
  return [...result];
91
88
  }
package/integrate.mjs CHANGED
@@ -3,24 +3,11 @@
3
3
  import { existsSync, mkdirSync, writeFileSync, readFileSync, readdirSync, rmSync } from 'node:fs';
4
4
  import { join } from 'node:path';
5
5
  import * as fmt from './fmt.mjs';
6
-
7
- // AW CLI commands to generate
8
- const AW_COMMANDS = [
9
- { name: 'pull', description: 'Pull agents & skills from registry', hint: '<path>' },
10
- { name: 'push', description: 'Push local changes to registry', hint: '<path>' },
11
- { name: 'status', description: 'Show workspace sync status', hint: '' },
12
- { name: 'drop', description: 'Stop syncing a path', hint: '<path>' },
13
- { name: 'search', description: 'Search local and remote registry', hint: '<query>' },
14
- { name: 'nuke', description: 'Remove entire .aw_registry/', hint: '' },
15
- ];
6
+ import * as config from './config.mjs';
16
7
 
17
8
  /**
18
- * Generate /aw:* commands directly into namespace commands/ dirs.
19
- * Writes CLI commands + agent invoke commands alongside hand-written commands.
20
- * link.mjs symlinks everything from commands/ → .claude/commands/aw/ for discovery.
21
- *
22
- * A .generated-manifest.json tracks which files were generated so they can be
23
- * cleaned on rebuild without needing filename prefixes.
9
+ * Count hand-written commands already present in the registry.
10
+ * No CLI stub generation all commands come from the registry itself.
24
11
  */
25
12
  export function generateCommands(cwd) {
26
13
  const awDir = join(cwd, '.aw_registry');
@@ -29,56 +16,15 @@ export function generateCommands(cwd) {
29
16
  const oldGenDir = join(awDir, '.generated-commands');
30
17
  if (existsSync(oldGenDir)) rmSync(oldGenDir, { recursive: true, force: true });
31
18
 
19
+ // Count hand-written commands across all namespaces for reporting
32
20
  let count = 0;
33
- const namespaces = listNamespaceDirs(awDir);
34
-
21
+ const namespaces = getTeamNamespaces(awDir);
35
22
  for (const ns of namespaces) {
36
- const commandsDir = join(awDir, ns, 'commands');
37
- mkdirSync(commandsDir, { recursive: true });
38
-
39
- // 1. CLI commands
40
- for (const cmd of AW_COMMANDS) {
41
- const fileName = `${cmd.name}.md`;
42
- // Skip if a hand-written command with same name exists
43
- if (existsSync(join(commandsDir, fileName))) continue;
44
-
45
- const content = [
46
- '---',
47
- `name: aw:${cmd.name}`,
48
- `description: ${cmd.description}`,
49
- cmd.hint ? `argument-hint: "${cmd.hint}"` : null,
50
- '---',
51
- '',
52
- 'Run the following command:',
53
- '',
54
- '```',
55
- `node bin/aw ${cmd.name} $ARGUMENTS`,
56
- '```',
57
- '',
58
- ].filter(v => v !== null).join('\n');
59
-
60
- writeFileSync(join(commandsDir, fileName), content);
61
- count++;
23
+ const nsDir = join(awDir, ns);
24
+ if (existsSync(nsDir)) {
25
+ const cmdFiles = findFiles(nsDir, 'commands');
26
+ count += cmdFiles.length;
62
27
  }
63
-
64
- }
65
-
66
- // Codex skills — .agents/skills/<name>/SKILL.md
67
- const agentsSkillsDir = join(cwd, '.agents/skills');
68
- mkdirSync(agentsSkillsDir, { recursive: true });
69
- for (const cmd of AW_COMMANDS) {
70
- const skillDir = join(agentsSkillsDir, cmd.name);
71
- mkdirSync(skillDir, { recursive: true });
72
- const content = [
73
- '---',
74
- `name: ${cmd.name}`,
75
- `description: ${cmd.description}`,
76
- '---',
77
- '',
78
- `Run: \`node bin/aw ${cmd.name}\` followed by the user's arguments.`,
79
- '',
80
- ].join('\n');
81
- writeFileSync(join(skillDir, 'SKILL.md'), content);
82
28
  }
83
29
 
84
30
  if (count > 0) {
@@ -88,6 +34,27 @@ export function generateCommands(cwd) {
88
34
  return count;
89
35
  }
90
36
 
37
+ function findFiles(dir, typeName) {
38
+ const results = [];
39
+ function walk(d) {
40
+ for (const entry of readdirSync(d, { withFileTypes: true })) {
41
+ if (entry.name.startsWith('.')) continue;
42
+ const full = join(d, entry.name);
43
+ if (entry.isDirectory()) {
44
+ if (entry.name === typeName) {
45
+ for (const f of readdirSync(full)) {
46
+ if (f.endsWith('.md')) results.push(join(full, f));
47
+ }
48
+ } else {
49
+ walk(full);
50
+ }
51
+ }
52
+ }
53
+ }
54
+ walk(dir);
55
+ return results;
56
+ }
57
+
91
58
  /**
92
59
  * Copy CLAUDE.md and AGENTS.md to project root.
93
60
  */
@@ -98,7 +65,7 @@ export function copyInstructions(cwd, tempDir, namespace) {
98
65
  if (existsSync(dest)) continue;
99
66
 
100
67
  if (tempDir) {
101
- const src = join(tempDir, 'registry', file);
68
+ const src = join(tempDir, '.aw_registry', file);
102
69
  if (existsSync(src)) {
103
70
  let content = readFileSync(src, 'utf8');
104
71
  if (namespace) {
@@ -460,10 +427,13 @@ No active tasks. Tasks are created during workflow execution.
460
427
  fmt.logSuccess('Created .aw_docs/ (local orchestration state)');
461
428
  }
462
429
 
463
- function listNamespaceDirs(awDir) {
464
- if (!existsSync(awDir)) return [];
465
- return readdirSync(awDir, { withFileTypes: true })
466
- .filter(d => d.isDirectory() && !d.name.startsWith('.'))
467
- .map(d => d.name);
430
+ /**
431
+ * Return team namespace paths from config (excludes 'platform').
432
+ * E.g. ['revex/courses'] generated CLI commands only go into team namespaces.
433
+ */
434
+ function getTeamNamespaces(awDir) {
435
+ const cfg = config.load(awDir);
436
+ if (!cfg || !cfg.include) return [];
437
+ return cfg.include.filter(p => p !== 'platform');
468
438
  }
469
439
 
package/mcp.mjs CHANGED
@@ -35,12 +35,12 @@ export function setupMcp(cwd, namespace) {
35
35
 
36
36
  const mcpUrl = paths.ghlMcpUrl;
37
37
 
38
- const ghlAiServer = { type: 'url', url: mcpUrl };
38
+ const ghlAiServer = { type: 'http', url: mcpUrl };
39
39
  const gitJenkinsServer = paths.gitJenkinsPath
40
40
  ? { command: 'node', args: [paths.gitJenkinsPath] }
41
41
  : null;
42
42
 
43
- // ── Claude Code: ~/.claude/settings.json ──
43
+ // ── Claude Code: ~/.claude/settings.json (global) ──
44
44
  const claudeSettingsPath = join(HOME, '.claude', 'settings.json');
45
45
  if (mergeJsonMcpServer(claudeSettingsPath, 'ghl-ai', ghlAiServer)) {
46
46
  updatedFiles.push(claudeSettingsPath);
@@ -49,7 +49,37 @@ export function setupMcp(cwd, namespace) {
49
49
  updatedFiles.push(claudeSettingsPath);
50
50
  }
51
51
 
52
- // ── Cursor: ~/.cursor/mcp.json ──
52
+ // ── Claude Code: .mcp.json (project root — highest priority) ──
53
+ // Claude Code resolves project .mcp.json before global settings.json,
54
+ // so we must write here to ensure the HTTP URL takes precedence over
55
+ // any stale command-based configs in parent directories.
56
+ const projectMcpPath = join(cwd, '.mcp.json');
57
+ if (mergeJsonMcpServer(projectMcpPath, 'ghl-ai', ghlAiServer)) {
58
+ updatedFiles.push(projectMcpPath);
59
+ }
60
+
61
+ // ── Claude Code: .claude/mcp.json (workspace-level) ──
62
+ // Claude Code also reads .claude/mcp.json for workspace-scoped MCP config.
63
+ // Write here so the MCP server is available regardless of which resolution
64
+ // path Claude Code uses (root .mcp.json or .claude/mcp.json).
65
+ const claudeWorkspaceMcpPath = join(cwd, '.claude', 'mcp.json');
66
+ if (mergeJsonMcpServer(claudeWorkspaceMcpPath, 'ghl-ai', ghlAiServer)) {
67
+ updatedFiles.push(claudeWorkspaceMcpPath);
68
+ }
69
+ if (gitJenkinsServer && mergeJsonMcpServer(claudeWorkspaceMcpPath, 'git-jenkins', gitJenkinsServer)) {
70
+ updatedFiles.push(claudeWorkspaceMcpPath);
71
+ }
72
+
73
+ // ── Cursor: project .cursor/mcp.json (workspace-level) ──
74
+ const cursorProjectMcpPath = join(cwd, '.cursor', 'mcp.json');
75
+ if (mergeJsonMcpServer(cursorProjectMcpPath, 'ghl-ai', ghlAiServer)) {
76
+ updatedFiles.push(cursorProjectMcpPath);
77
+ }
78
+ if (gitJenkinsServer && mergeJsonMcpServer(cursorProjectMcpPath, 'git-jenkins', gitJenkinsServer)) {
79
+ updatedFiles.push(cursorProjectMcpPath);
80
+ }
81
+
82
+ // ── Cursor: ~/.cursor/mcp.json (global) ──
53
83
  const cursorMcpPath = join(HOME, '.cursor', 'mcp.json');
54
84
  if (mergeJsonMcpServer(cursorMcpPath, 'ghl-ai', ghlAiServer)) {
55
85
  updatedFiles.push(cursorMcpPath);
@@ -94,12 +124,18 @@ function mergeJsonMcpServer(filePath, serverName, serverConfig) {
94
124
  config.mcpServers = {};
95
125
  }
96
126
 
97
- // Check if already configured with same URL
127
+ // Check if already configured with same config
98
128
  const existing = config.mcpServers[serverName];
99
129
  if (existing && JSON.stringify(existing) === JSON.stringify(serverConfig)) {
100
130
  return false;
101
131
  }
102
132
 
133
+ // If we're writing an HTTP URL server, remove any stale command-based config
134
+ // to prevent old local MCP servers from shadowing the remote one.
135
+ if (serverConfig.type === 'http' && existing && existing.command) {
136
+ fmt.logStep(`Replacing stale command-based '${serverName}' with HTTP URL`);
137
+ }
138
+
103
139
  config.mcpServers[serverName] = serverConfig;
104
140
 
105
141
  mkdirSync(join(filePath, '..'), { recursive: true });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ghl-ai/aw",
3
- "version": "0.1.25-beta.9",
3
+ "version": "0.1.26-beta.1",
4
4
  "description": "Agentic Workspace CLI — pull, push & manage agents, skills and commands from the registry",
5
5
  "type": "module",
6
6
  "bin": {
package/plan.mjs CHANGED
@@ -24,11 +24,12 @@ export function computePlan(registryDirs, workspaceDir, includePatterns = [], {
24
24
  continue;
25
25
  }
26
26
 
27
- // For skill files: key includes relative path to avoid collisions
28
- // For other files: key is type/slug
27
+ // Key must include namespacePath to avoid collisions when the same
28
+ // slug exists under different domains (e.g. backend/agents/developer
29
+ // vs frontend/agents/developer).
29
30
  const key = entry.skillRelPath
30
- ? `${entry.type}/${entry.slug}/${entry.skillRelPath}`
31
- : `${entry.type}/${entry.slug}`;
31
+ ? `${entry.namespacePath}/${entry.type}/${entry.slug}/${entry.skillRelPath}`
32
+ : `${entry.namespacePath}/${entry.type}/${entry.slug}`;
32
33
  plan.set(key, { ...entry, source: entry.namespacePath || name });
33
34
  }
34
35
  }
@@ -108,9 +109,10 @@ export function computePlan(registryDirs, workspaceDir, includePatterns = [], {
108
109
  }
109
110
  }
110
111
  if (typeIdx === -1) continue;
112
+ const namespace = parts.slice(0, typeIdx).join('/');
111
113
  const type = parts[typeIdx];
112
114
  const slug = parts[typeIdx + 1]?.replace(/\.md$/, '');
113
- const key = `${type}/${slug}`;
115
+ const key = `${namespace}/${type}/${slug}`;
114
116
  if (!plan.has(key)) {
115
117
  actions.push({
116
118
  slug,