@ghl-ai/aw 0.1.25-beta.8 → 0.1.25

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
220
+ // cfg.include has the renamed namespace (e.g. 'revex/courses'), but the repo
183
221
  // 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 => {
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
@@ -17,7 +17,14 @@ import { resolveInput } from '../paths.mjs';
17
17
  import { linkWorkspace } from '../link.mjs';
18
18
  import { generateCommands, copyInstructions } from '../integrate.mjs';
19
19
 
20
- export function pullCommand(args) {
20
+ // Filter out top-level platform CLI meta-commands (drop, pull, push, etc.)
21
+ // but keep domain-specific commands (platform/design/commands/, platform/infra/commands/).
22
+ function filterActions(actions, pattern) {
23
+ if (pattern !== 'platform') return actions;
24
+ return actions.filter(a => !(a.type === 'commands' && a.namespacePath === 'platform'));
25
+ }
26
+
27
+ export async function pullCommand(args) {
21
28
  const input = args._positional?.[0] || '';
22
29
  const cwd = process.cwd();
23
30
  const GLOBAL_AW_DIR = join(homedir(), '.aw_registry');
@@ -27,6 +34,7 @@ export function pullCommand(args) {
27
34
  const verbose = args['-v'] === true || args['--verbose'] === true;
28
35
  const silent = args['--silent'] === true || args._silent === true;
29
36
  const renameNamespace = args._renameNamespace || null;
37
+ const teamNSOverride = args._teamNS || null;
30
38
 
31
39
  // Silent mode: wrap fmt to suppress all output and exit cleanly on errors
32
40
  const log = {
@@ -41,7 +49,7 @@ export function pullCommand(args) {
41
49
  spinner: silent ? () => ({ start: () => {}, stop: () => {} }) : fmt.spinner,
42
50
  };
43
51
 
44
- // No args = re-pull everything in sync config
52
+ // No args = re-pull everything in sync config (parallel)
45
53
  if (!input) {
46
54
  const cfg = config.load(workspaceDir);
47
55
  if (!cfg) log.cancel('No .sync-config.json found. Run: aw init');
@@ -49,9 +57,19 @@ export function pullCommand(args) {
49
57
  log.cancel('Nothing to pull. Add paths first:\n\n aw pull <path>');
50
58
  }
51
59
  log.logInfo(`Pulling ${chalk.cyan(cfg.include.length)} synced path${cfg.include.length > 1 ? 's' : ''}...`);
52
- for (const p of cfg.include) {
53
- pullCommand({ ...args, _positional: [p], _skipIntegrate: true });
54
- }
60
+ const pullJobs = cfg.include.map(p => {
61
+ const isTeamNs = p !== 'platform';
62
+ const derivedTeamNS = isTeamNs ? p.replace(/\//g, '-') : undefined;
63
+ return pullAsync({
64
+ ...args,
65
+ _positional: [isTeamNs ? '[template]' : p],
66
+ _workspaceDir: workspaceDir,
67
+ _skipIntegrate: true,
68
+ _renameNamespace: isTeamNs ? p : undefined,
69
+ _teamNS: derivedTeamNS,
70
+ });
71
+ });
72
+ await Promise.all(pullJobs);
55
73
  // Post-pull IDE integration (once after all paths pulled)
56
74
  linkWorkspace(cwd);
57
75
  generateCommands(cwd);
@@ -145,9 +163,10 @@ export function pullCommand(args) {
145
163
  }
146
164
 
147
165
  // Compute plan
148
- const { actions } = computePlan(registryDirs, workspaceDir, [pattern], {
166
+ const { actions: rawActions } = computePlan(registryDirs, workspaceDir, [pattern], {
149
167
  skipOrphans: true,
150
168
  });
169
+ const actions = filterActions(rawActions, pattern);
151
170
 
152
171
  if (dryRun) {
153
172
  printDryRun(actions, verbose);
@@ -157,7 +176,7 @@ export function pullCommand(args) {
157
176
  // Apply
158
177
  const s2 = log.spinner();
159
178
  s2.start('Applying changes...');
160
- const conflictCount = applyActions(actions, { teamNS: renameNamespace || undefined });
179
+ const conflictCount = applyActions(actions, { teamNS: teamNSOverride || renameNamespace || undefined });
161
180
  updateManifest(workspaceDir, actions, cfg.namespace);
162
181
  s2.stop('Changes applied');
163
182
 
@@ -200,6 +219,7 @@ export async function pullAsync(args) {
200
219
  const input = args._positional?.[0] || '';
201
220
  const workspaceDir = args._workspaceDir;
202
221
  const renameNamespace = args._renameNamespace || null;
222
+ const teamNSOverride = args._teamNS || null;
203
223
 
204
224
  const resolved = resolveInput(input, workspaceDir);
205
225
  let pattern = resolved.registryPath;
@@ -251,8 +271,9 @@ export async function pullAsync(args) {
251
271
  config.addPattern(workspaceDir, pattern);
252
272
  }
253
273
 
254
- const { actions } = computePlan(registryDirs, workspaceDir, [pattern], { skipOrphans: true });
255
- const conflictCount = applyActions(actions, { teamNS: renameNamespace || undefined });
274
+ const { actions: rawActions } = computePlan(registryDirs, workspaceDir, [pattern], { skipOrphans: true });
275
+ const actions = filterActions(rawActions, pattern);
276
+ const conflictCount = applyActions(actions, { teamNS: teamNSOverride || renameNamespace || undefined });
256
277
  updateManifest(workspaceDir, actions, cfg.namespace);
257
278
 
258
279
  const ROOT_REGISTRY_FILES = ['AW-PROTOCOL.md'];
package/constants.mjs CHANGED
@@ -1,7 +1,7 @@
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 = 'master';
5
5
 
6
6
  /** Default registry repository */
7
7
  export const REGISTRY_REPO = 'GoHighLevel/ghl-agentic-workspace';
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
  */
@@ -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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ghl-ai/aw",
3
- "version": "0.1.25-beta.8",
3
+ "version": "0.1.25",
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,