@ghl-ai/aw 0.1.25 → 0.1.26-beta.2

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
@@ -218,7 +218,7 @@ export async function initCommand(args) {
218
218
 
219
219
  // Pull latest (parallel)
220
220
  // cfg.include has the renamed namespace (e.g. 'revex/courses'), but the repo
221
- // only has 'registry/[template]/' — remap non-platform entries back.
221
+ // only has '.aw_registry/[template]/' — remap non-platform entries back.
222
222
  const freshCfg = config.load(GLOBAL_AW_DIR);
223
223
  if (freshCfg && freshCfg.include.length > 0) {
224
224
  const pullJobs = freshCfg.include.map(p => {
package/commands/pull.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  // commands/pull.mjs — Pull content from registry
2
2
 
3
- import { mkdirSync, existsSync, readdirSync, copyFileSync } from 'node:fs';
3
+ import { mkdirSync, existsSync, readdirSync, copyFileSync, cpSync } from 'node:fs';
4
4
  import { join } from 'node:path';
5
5
  import { homedir } from 'node:os';
6
6
  import { execSync } from 'node:child_process';
@@ -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';
@@ -111,7 +112,7 @@ export async function pullCommand(args) {
111
112
 
112
113
  try {
113
114
  const registryDirs = [];
114
- const regBase = join(tempDir, 'registry');
115
+ const regBase = join(tempDir, REGISTRY_DIR);
115
116
 
116
117
  if (existsSync(regBase)) {
117
118
  for (const name of listDirs(regBase)) {
@@ -190,6 +191,16 @@ export async function pullCommand(args) {
190
191
  }
191
192
  }
192
193
 
194
+ // Bulk-copy docs/ directories (not a TYPE_DIR — raw recursive copy)
195
+ for (const rd of registryDirs) {
196
+ const docsDir = join(rd.path, 'docs');
197
+ if (existsSync(docsDir)) {
198
+ const dest = join(workspaceDir, rd.name, 'docs');
199
+ mkdirSync(dest, { recursive: true });
200
+ cpSync(docsDir, dest, { recursive: true });
201
+ }
202
+ }
203
+
193
204
  // MCP registration (second-class — skip if not available)
194
205
  if (cfg.namespace) {
195
206
  registerMcp(cfg.namespace);
@@ -235,7 +246,7 @@ export async function pullAsync(args) {
235
246
 
236
247
  try {
237
248
  const registryDirs = [];
238
- const regBase = join(tempDir, 'registry');
249
+ const regBase = join(tempDir, REGISTRY_DIR);
239
250
 
240
251
  if (existsSync(regBase)) {
241
252
  for (const name of listDirs(regBase)) {
@@ -282,6 +293,16 @@ export async function pullAsync(args) {
282
293
  if (existsSync(src)) copyFileSync(src, join(workspaceDir, fname));
283
294
  }
284
295
 
296
+ // Bulk-copy docs/ directories (not a TYPE_DIR — raw recursive copy)
297
+ for (const rd of registryDirs) {
298
+ const docsDir = join(rd.path, 'docs');
299
+ if (existsSync(docsDir)) {
300
+ const dest = join(workspaceDir, rd.name, 'docs');
301
+ mkdirSync(dest, { recursive: true });
302
+ cpSync(docsDir, dest, { recursive: true });
303
+ }
304
+ }
305
+
285
306
  return { pattern, actions, conflictCount };
286
307
  } finally {
287
308
  cleanup(tempDir);
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 = 'master';
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
@@ -65,7 +65,7 @@ export function copyInstructions(cwd, tempDir, namespace) {
65
65
  if (existsSync(dest)) continue;
66
66
 
67
67
  if (tempDir) {
68
- const src = join(tempDir, 'registry', file);
68
+ const src = join(tempDir, '.aw_registry', file);
69
69
  if (existsSync(src)) {
70
70
  let content = readFileSync(src, 'utf8');
71
71
  if (namespace) {
package/link.mjs CHANGED
@@ -1,15 +1,19 @@
1
1
  // link.mjs — Create symlinks from IDE dirs → .aw_registry/
2
2
 
3
- import { existsSync, lstatSync, mkdirSync, readdirSync, unlinkSync } from 'node:fs';
3
+ import { existsSync, lstatSync, mkdirSync, readdirSync, unlinkSync, symlinkSync } from 'node:fs';
4
4
  import { join, relative } from 'node:path';
5
- import { execSync } from 'node:child_process';
6
5
  import { homedir } from 'node:os';
7
6
  import * as fmt from './fmt.mjs';
8
7
 
8
+ function forceSymlink(target, linkPath) {
9
+ try { unlinkSync(linkPath); } catch { /* not there yet */ }
10
+ symlinkSync(target, linkPath);
11
+ }
12
+
9
13
  const IDE_DIRS = ['.claude', '.cursor', '.codex'];
10
14
  // Per-file symlink types
11
15
  const FILE_TYPES = ['agents'];
12
- const ALL_KNOWN_TYPES = new Set([...FILE_TYPES, 'skills', 'commands', 'evals']);
16
+ const ALL_KNOWN_TYPES = new Set([...FILE_TYPES, 'skills', 'commands', 'evals', 'docs']);
13
17
 
14
18
  /**
15
19
  * List namespace directories inside .aw_registry/ (skip dotfiles).
@@ -135,10 +139,7 @@ export function linkWorkspace(cwd) {
135
139
  const linkPath = join(linkDir, flat);
136
140
  const targetPath = join(typeDirPath, file);
137
141
  const relTarget = relative(linkDir, targetPath);
138
- try {
139
- execSync(`ln -sfn "${relTarget}" "${linkPath}"`, { stdio: 'pipe' });
140
- created++;
141
- } catch { /* best effort */ }
142
+ try { forceSymlink(relTarget, linkPath); created++; } catch { /* best effort */ }
142
143
  }
143
144
  }
144
145
  }
@@ -155,10 +156,7 @@ export function linkWorkspace(cwd) {
155
156
  const linkPath = join(linkDir, flat);
156
157
  const targetPath = join(skillsDir, skill);
157
158
  const relTarget = relative(linkDir, targetPath);
158
- try {
159
- execSync(`ln -sfn "${relTarget}" "${linkPath}"`, { stdio: 'pipe' });
160
- created++;
161
- } catch { /* best effort */ }
159
+ try { forceSymlink(relTarget, linkPath); created++; } catch { /* best effort */ }
162
160
  }
163
161
  }
164
162
  }
@@ -178,10 +176,7 @@ export function linkWorkspace(cwd) {
178
176
  const linkPath = join(linkDir, flat);
179
177
  const targetPath = join(subDir, evalName);
180
178
  const relTarget = relative(linkDir, targetPath);
181
- try {
182
- execSync(`ln -sfn "${relTarget}" "${linkPath}"`, { stdio: 'pipe' });
183
- created++;
184
- } catch { /* best effort */ }
179
+ try { forceSymlink(relTarget, linkPath); created++; } catch { /* best effort */ }
185
180
  }
186
181
  }
187
182
  }
@@ -199,10 +194,7 @@ export function linkWorkspace(cwd) {
199
194
  const linkPath = join(agentsSkillsDir, flat);
200
195
  const targetPath = join(skillsDir, skill);
201
196
  const relTarget = relative(agentsSkillsDir, targetPath);
202
- try {
203
- execSync(`ln -sfn "${relTarget}" "${linkPath}"`, { stdio: 'pipe' });
204
- created++;
205
- } catch { /* best effort */ }
197
+ try { forceSymlink(relTarget, linkPath); created++; } catch { /* best effort */ }
206
198
  }
207
199
  }
208
200
  }
@@ -220,10 +212,7 @@ export function linkWorkspace(cwd) {
220
212
  const linkPath = join(linkDir, cmdFileName);
221
213
  const targetPath = join(commandsDir, file);
222
214
  const relTarget = relative(linkDir, targetPath);
223
- try {
224
- execSync(`ln -sfn "${relTarget}" "${linkPath}"`, { stdio: 'pipe' });
225
- created++;
226
- } catch { /* best effort */ }
215
+ try { forceSymlink(relTarget, linkPath); created++; } catch { /* best effort */ }
227
216
  }
228
217
  }
229
218
  }
@@ -235,13 +224,19 @@ export function linkWorkspace(cwd) {
235
224
  for (const ide of IDE_DIRS) {
236
225
  const linkPath = join(cwd, ide, 'AW-PROTOCOL.md');
237
226
  const relTarget = relative(join(cwd, ide), protocolSrc);
238
- try {
239
- if (existsSync(linkPath) || lstatSync(linkPath).isSymbolicLink()) unlinkSync(linkPath);
240
- } catch { /* not there yet */ }
241
- try {
242
- execSync(`ln -sfn "${relTarget}" "${linkPath}"`, { stdio: 'pipe' });
243
- created++;
244
- } catch { /* best effort */ }
227
+ try { forceSymlink(relTarget, linkPath); created++; } catch { /* best effort */ }
228
+ }
229
+ }
230
+
231
+ // Docs: single directory symlink per IDE → .aw_registry/platform/docs
232
+ const platformDocsDir = join(awDir, 'platform', 'docs');
233
+ if (existsSync(platformDocsDir)) {
234
+ for (const ide of IDE_DIRS) {
235
+ const linkDir = join(cwd, ide);
236
+ mkdirSync(linkDir, { recursive: true });
237
+ const linkPath = join(linkDir, 'docs');
238
+ const relTarget = relative(linkDir, platformDocsDir);
239
+ try { forceSymlink(relTarget, linkPath); created++; } catch { /* best effort */ }
245
240
  }
246
241
  }
247
242
 
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",
3
+ "version": "0.1.26-beta.2",
4
4
  "description": "Agentic Workspace CLI — pull, push & manage agents, skills and commands from the registry",
5
5
  "type": "module",
6
6
  "bin": {
package/registry.mjs CHANGED
@@ -5,6 +5,7 @@ import { join, relative } from 'node:path';
5
5
  import { createHash } from 'node:crypto';
6
6
 
7
7
  const TYPE_DIRS = new Set(['agents', 'skills', 'commands', 'evals']);
8
+ const SKIP_DIRS = new Set(['docs']);
8
9
 
9
10
  export function sha256(content) {
10
11
  return createHash('sha256').update(content).digest('hex');
@@ -97,7 +98,7 @@ export function walkRegistryTree(baseDir, baseName) {
97
98
  });
98
99
  }
99
100
  }
100
- } else if (entry.isDirectory()) {
101
+ } else if (entry.isDirectory() && !SKIP_DIRS.has(entry.name)) {
101
102
  recurse(fullPath, [...pathSegments, entry.name]);
102
103
  }
103
104
  }