@ghl-ai/aw 0.1.50-beta.1 → 0.1.50
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/c4/templates/scripts/aw-c4-bootstrap.sh +4 -5
- package/cli.mjs +9 -21
- package/commands/c4.mjs +9 -17
- package/commands/doctor.mjs +5 -2
- package/commands/init.mjs +54 -125
- package/commands/integration.mjs +111 -0
- package/commands/nuke.mjs +3 -1
- package/commands/pull.mjs +2 -3
- package/commands/push.mjs +29 -715
- package/commands/startup.mjs +3 -22
- package/constants.mjs +0 -23
- package/ecc.mjs +1 -1
- package/git.mjs +4 -19
- package/integrate.mjs +21 -94
- package/integrations/context-mode.mjs +514 -0
- package/integrations/index.mjs +31 -0
- package/link.mjs +33 -159
- package/mcp.mjs +16 -132
- package/package.json +4 -4
- package/render-rules.mjs +1 -25
- package/startup.mjs +8 -52
- package/commands/integrations.mjs +0 -254
- package/commands/mcp.mjs +0 -90
- package/integrations.mjs +0 -971
package/link.mjs
CHANGED
|
@@ -16,11 +16,6 @@ const IDE_DIRS = ['.claude', '.cursor', '.codex'];
|
|
|
16
16
|
const FILE_TYPES = ['agents'];
|
|
17
17
|
const ALL_KNOWN_TYPES = new Set([...FILE_TYPES, 'skills', 'commands', 'evals', 'references', 'docs']);
|
|
18
18
|
|
|
19
|
-
function realHomeDir() {
|
|
20
|
-
const rawHome = homedir();
|
|
21
|
-
try { return realpathSync(rawHome); } catch { return rawHome; }
|
|
22
|
-
}
|
|
23
|
-
|
|
24
19
|
/**
|
|
25
20
|
* List namespace directories inside .aw_registry/ (skip dotfiles).
|
|
26
21
|
*/
|
|
@@ -82,7 +77,7 @@ function cleanIdeSymlinks(cwd) {
|
|
|
82
77
|
cleanSymlinksRecursive(ideDir);
|
|
83
78
|
}
|
|
84
79
|
// Also clean .agents/skills/ (global only — Codex reads from ~/.agents/skills/)
|
|
85
|
-
const HOME =
|
|
80
|
+
const HOME = homedir();
|
|
86
81
|
if (cwd === HOME) {
|
|
87
82
|
const agentsSkillsDir = join(cwd, '.agents', 'skills');
|
|
88
83
|
if (existsSync(agentsSkillsDir)) cleanSymlinksRecursive(agentsSkillsDir);
|
|
@@ -125,156 +120,6 @@ function flatName(ns, name) {
|
|
|
125
120
|
return `${ns}-${name}`;
|
|
126
121
|
}
|
|
127
122
|
|
|
128
|
-
function stripRegistryPrefix(input) {
|
|
129
|
-
return String(input || '')
|
|
130
|
-
.trim()
|
|
131
|
-
.replace(/\\/g, '/')
|
|
132
|
-
.replace(/\/SKILL\.md$/i, '')
|
|
133
|
-
.replace(/^.*?\.aw_registry\//, '')
|
|
134
|
-
.replace(/^\.\/+/, '')
|
|
135
|
-
.replace(/\/+$/, '');
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
export function normalizeSkillTarget(input) {
|
|
139
|
-
const normalized = stripRegistryPrefix(input);
|
|
140
|
-
const parts = normalized.split('/').filter(Boolean);
|
|
141
|
-
const skillIndex = parts.indexOf('skills');
|
|
142
|
-
|
|
143
|
-
if (skillIndex < 1 || skillIndex === parts.length - 1) {
|
|
144
|
-
throw new Error(`Skill target must look like <team>/<path>/skills/<name>: ${input}`);
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
if (parts.length !== skillIndex + 2) {
|
|
148
|
-
throw new Error(`Skill target must point at a skill folder, not a nested file: ${input}`);
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
const namespace = parts[0];
|
|
152
|
-
const segments = parts.slice(1, skillIndex);
|
|
153
|
-
const skill = parts[skillIndex + 1];
|
|
154
|
-
const registryPath = [namespace, ...segments, 'skills', skill].join('/');
|
|
155
|
-
const flat = [namespace, ...segments, skill].join('-');
|
|
156
|
-
return { namespace, segments, skill, registryPath, flat };
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
function cleanSkillSymlinks(cwd) {
|
|
160
|
-
for (const ide of IDE_DIRS) {
|
|
161
|
-
const skillsDir = join(cwd, ide, 'skills');
|
|
162
|
-
if (existsSync(skillsDir)) cleanSymlinksRecursive(skillsDir);
|
|
163
|
-
const referencesDir = join(cwd, ide, 'references');
|
|
164
|
-
if (existsSync(referencesDir)) cleanSymlinksRecursive(referencesDir);
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
if (cwd === realHomeDir()) {
|
|
168
|
-
const agentsSkillsDir = join(cwd, '.agents', 'skills');
|
|
169
|
-
if (existsSync(agentsSkillsDir)) cleanSymlinksRecursive(agentsSkillsDir);
|
|
170
|
-
const agentsReferencesDir = join(cwd, '.agents', 'references');
|
|
171
|
-
if (existsSync(agentsReferencesDir)) cleanSymlinksRecursive(agentsReferencesDir);
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
function linkOneSkill(cwd, awDir, target) {
|
|
176
|
-
const skillDir = join(awDir, target.namespace, ...target.segments, 'skills', target.skill);
|
|
177
|
-
if (!existsSync(skillDir) || !lstatSync(skillDir).isDirectory()) {
|
|
178
|
-
throw new Error(`Skill not found in registry: ${target.registryPath}`);
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
let created = 0;
|
|
182
|
-
for (const ide of IDE_DIRS) {
|
|
183
|
-
const linkDir = join(cwd, ide, 'skills');
|
|
184
|
-
mkdirSync(linkDir, { recursive: true });
|
|
185
|
-
const linkPath = join(linkDir, target.flat);
|
|
186
|
-
const relTarget = relative(linkDir, skillDir);
|
|
187
|
-
forceSymlink(relTarget, linkPath);
|
|
188
|
-
created++;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
if (cwd === realHomeDir()) {
|
|
192
|
-
const agentsSkillsDir = join(cwd, '.agents', 'skills');
|
|
193
|
-
mkdirSync(agentsSkillsDir, { recursive: true });
|
|
194
|
-
const linkPath = join(agentsSkillsDir, target.flat);
|
|
195
|
-
const relTarget = relative(agentsSkillsDir, skillDir);
|
|
196
|
-
forceSymlink(relTarget, linkPath);
|
|
197
|
-
created++;
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
return created;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
function linkReferenceFiles(cwd, awDir, namespaces = null) {
|
|
204
|
-
const allowedNamespaces = namespaces ? new Set(namespaces) : null;
|
|
205
|
-
const namespacesToLink = listNamespaceDirs(awDir)
|
|
206
|
-
.filter(ns => !allowedNamespaces || allowedNamespaces.has(ns));
|
|
207
|
-
let created = 0;
|
|
208
|
-
|
|
209
|
-
for (const ns of namespacesToLink) {
|
|
210
|
-
for (const { typeDirPath: referencesDir } of findNestedTypeDirs(join(awDir, ns), 'references')) {
|
|
211
|
-
for (const file of readdirSync(referencesDir).filter(f => !f.startsWith('.'))) {
|
|
212
|
-
const targetPath = join(referencesDir, file);
|
|
213
|
-
if (lstatSync(targetPath).isDirectory()) continue;
|
|
214
|
-
|
|
215
|
-
for (const ide of IDE_DIRS) {
|
|
216
|
-
const linkDir = join(cwd, ide, 'references');
|
|
217
|
-
mkdirSync(linkDir, { recursive: true });
|
|
218
|
-
const linkPath = join(linkDir, file);
|
|
219
|
-
const relTarget = relative(linkDir, targetPath);
|
|
220
|
-
try { forceSymlink(relTarget, linkPath); created++; } catch { /* best effort */ }
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
if (cwd === realHomeDir()) {
|
|
224
|
-
const agentsReferencesDir = join(cwd, '.agents', 'references');
|
|
225
|
-
mkdirSync(agentsReferencesDir, { recursive: true });
|
|
226
|
-
const linkPath = join(agentsReferencesDir, file);
|
|
227
|
-
const relTarget = relative(agentsReferencesDir, targetPath);
|
|
228
|
-
try { forceSymlink(relTarget, linkPath); created++; } catch { /* best effort */ }
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
return created;
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
/**
|
|
238
|
-
* Create/refresh symlinks for specific skills only.
|
|
239
|
-
*
|
|
240
|
-
* Unlike linkWorkspace(), this does not walk every namespace. It can be used
|
|
241
|
-
* by lean init flows to avoid exposing every registry skill to IDE runtimes.
|
|
242
|
-
*/
|
|
243
|
-
export function linkSkills(cwd, skillTargets, awDirOverride = null, { silent = false, exclusive = false } = {}) {
|
|
244
|
-
try { cwd = realpathSync(cwd); } catch { /* use as-is */ }
|
|
245
|
-
|
|
246
|
-
const GLOBAL_AW_DIR = join(realHomeDir(), '.aw_registry');
|
|
247
|
-
let awDir = awDirOverride || getLocalRegistryDir(cwd, GLOBAL_AW_DIR);
|
|
248
|
-
try { awDir = realpathSync(awDir); } catch { /* use as-is if it doesn't exist */ }
|
|
249
|
-
if (!existsSync(awDir)) return 0;
|
|
250
|
-
|
|
251
|
-
const targets = [...new Map(skillTargets.map(input => {
|
|
252
|
-
const target = normalizeSkillTarget(input);
|
|
253
|
-
return [target.registryPath, target];
|
|
254
|
-
})).values()];
|
|
255
|
-
|
|
256
|
-
for (const target of targets) {
|
|
257
|
-
const skillDir = join(awDir, target.namespace, ...target.segments, 'skills', target.skill);
|
|
258
|
-
if (!existsSync(skillDir) || !lstatSync(skillDir).isDirectory()) {
|
|
259
|
-
throw new Error(`Skill not found in registry: ${target.registryPath}`);
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
if (exclusive) cleanSkillSymlinks(cwd);
|
|
264
|
-
|
|
265
|
-
let created = 0;
|
|
266
|
-
for (const target of targets) {
|
|
267
|
-
created += linkOneSkill(cwd, awDir, target);
|
|
268
|
-
}
|
|
269
|
-
created += linkReferenceFiles(cwd, awDir, targets.map(target => target.namespace));
|
|
270
|
-
|
|
271
|
-
if (created > 0 && !silent) {
|
|
272
|
-
fmt.logSuccess(`Linked ${created} targeted symlink${created > 1 ? 's' : ''}`);
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
return created;
|
|
276
|
-
}
|
|
277
|
-
|
|
278
123
|
/**
|
|
279
124
|
* Create/refresh all IDE symlinks.
|
|
280
125
|
*
|
|
@@ -295,7 +140,7 @@ export function linkWorkspace(cwd, awDirOverride = null, { silent = false } = {}
|
|
|
295
140
|
// where $HOME may be /var/... but process.cwd() resolves to /private/var/...
|
|
296
141
|
try { cwd = realpathSync(cwd); } catch { /* use as-is */ }
|
|
297
142
|
|
|
298
|
-
const GLOBAL_AW_DIR = join(
|
|
143
|
+
const GLOBAL_AW_DIR = join(homedir(), '.aw_registry');
|
|
299
144
|
let awDir = awDirOverride || getLocalRegistryDir(cwd, GLOBAL_AW_DIR);
|
|
300
145
|
try { awDir = realpathSync(awDir); } catch { /* use as-is if it doesn't exist */ }
|
|
301
146
|
if (!existsSync(awDir)) return 0;
|
|
@@ -383,10 +228,25 @@ export function linkWorkspace(cwd, awDirOverride = null, { silent = false } = {}
|
|
|
383
228
|
|
|
384
229
|
// Shared references: flatten namespace references into each IDE's references/
|
|
385
230
|
// so links like ../../references/foo.md continue to work from flattened skill dirs.
|
|
386
|
-
|
|
231
|
+
for (const ns of namespaces) {
|
|
232
|
+
for (const { typeDirPath: referencesDir } of findNestedTypeDirs(join(awDir, ns), 'references')) {
|
|
233
|
+
for (const file of readdirSync(referencesDir).filter(f => !f.startsWith('.'))) {
|
|
234
|
+
const targetPath = join(referencesDir, file);
|
|
235
|
+
if (lstatSync(targetPath).isDirectory()) continue;
|
|
236
|
+
|
|
237
|
+
for (const ide of IDE_DIRS) {
|
|
238
|
+
const linkDir = join(cwd, ide, 'references');
|
|
239
|
+
mkdirSync(linkDir, { recursive: true });
|
|
240
|
+
const linkPath = join(linkDir, file);
|
|
241
|
+
const relTarget = relative(linkDir, targetPath);
|
|
242
|
+
try { forceSymlink(relTarget, linkPath); created++; } catch { /* best effort */ }
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
387
247
|
|
|
388
248
|
// Codex per-skill symlinks: ~/.agents/skills/<name> (global only)
|
|
389
|
-
if (cwd ===
|
|
249
|
+
if (cwd === homedir()) {
|
|
390
250
|
const agentsSkillsDir = join(cwd, '.agents/skills');
|
|
391
251
|
for (const ns of namespaces) {
|
|
392
252
|
for (const { typeDirPath: skillsDir, segments } of findNestedTypeDirs(join(awDir, ns), 'skills')) {
|
|
@@ -400,6 +260,20 @@ export function linkWorkspace(cwd, awDirOverride = null, { silent = false } = {}
|
|
|
400
260
|
}
|
|
401
261
|
}
|
|
402
262
|
}
|
|
263
|
+
|
|
264
|
+
const agentsReferencesDir = join(cwd, '.agents', 'references');
|
|
265
|
+
for (const ns of namespaces) {
|
|
266
|
+
for (const { typeDirPath: referencesDir } of findNestedTypeDirs(join(awDir, ns), 'references')) {
|
|
267
|
+
mkdirSync(agentsReferencesDir, { recursive: true });
|
|
268
|
+
for (const file of readdirSync(referencesDir).filter(f => !f.startsWith('.'))) {
|
|
269
|
+
const targetPath = join(referencesDir, file);
|
|
270
|
+
if (lstatSync(targetPath).isDirectory()) continue;
|
|
271
|
+
const linkPath = join(agentsReferencesDir, file);
|
|
272
|
+
const relTarget = relative(agentsReferencesDir, targetPath);
|
|
273
|
+
try { forceSymlink(relTarget, linkPath); created++; } catch { /* best effort */ }
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
403
277
|
}
|
|
404
278
|
|
|
405
279
|
// Commands: per-file symlinks (recursive for nested domain dirs)
|
package/mcp.mjs
CHANGED
|
@@ -1,72 +1,16 @@
|
|
|
1
1
|
// mcp.mjs — MCP config generation for Claude Code, Cursor, and Codex (global ~/ configs)
|
|
2
2
|
// Uses native Streamable HTTP — no bridge process needed.
|
|
3
3
|
|
|
4
|
-
import { existsSync, writeFileSync, readFileSync, mkdirSync
|
|
4
|
+
import { existsSync, writeFileSync, readFileSync, mkdirSync } from 'node:fs';
|
|
5
5
|
import { execSync } from 'node:child_process';
|
|
6
6
|
import { createInterface } from 'node:readline';
|
|
7
|
-
import {
|
|
7
|
+
import { join } from 'node:path';
|
|
8
8
|
import { homedir } from 'node:os';
|
|
9
|
-
import { randomBytes } from 'node:crypto';
|
|
10
9
|
import * as p from '@clack/prompts';
|
|
11
10
|
import * as fmt from './fmt.mjs';
|
|
12
11
|
|
|
13
12
|
const HOME = homedir();
|
|
14
13
|
const DEFAULT_MCP_URL = 'https://services.leadconnectorhq.com/agentic-workspace/mcp';
|
|
15
|
-
const MCP_PREFS_FILENAME = 'mcp-preferences.json';
|
|
16
|
-
const ENABLED_MODE = 'enabled';
|
|
17
|
-
const DISABLED_MODE = 'disabled';
|
|
18
|
-
const DISABLE_MCP_ENV = 'AW_DISABLE_MCP';
|
|
19
|
-
const MCP_SERVER_NAME = 'ghl-ai';
|
|
20
|
-
|
|
21
|
-
function mcpPrefsPath(homeDir = HOME) {
|
|
22
|
-
return join(homeDir, '.aw', MCP_PREFS_FILENAME);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function readJson(filePath, fallback = {}) {
|
|
26
|
-
if (!existsSync(filePath)) return fallback;
|
|
27
|
-
try {
|
|
28
|
-
return JSON.parse(readFileSync(filePath, 'utf8'));
|
|
29
|
-
} catch {
|
|
30
|
-
// Malformed or unreadable JSON should behave like an absent optional config.
|
|
31
|
-
return fallback;
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function writeJson(filePath, value) {
|
|
36
|
-
const dir = dirname(filePath);
|
|
37
|
-
mkdirSync(dir, { recursive: true });
|
|
38
|
-
const tmpPath = join(dir, `.${randomBytes(8).toString('hex')}.tmp`);
|
|
39
|
-
writeFileSync(tmpPath, `${JSON.stringify(value, null, 2)}\n`);
|
|
40
|
-
renameSync(tmpPath, filePath);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function envDisablesMcp(env = process.env) {
|
|
44
|
-
const value = String(env[DISABLE_MCP_ENV] || '').trim().toLowerCase();
|
|
45
|
-
return ['1', 'true', 'yes', 'on'].includes(value);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export function loadMcpPreferences(homeDir = HOME) {
|
|
49
|
-
const prefs = readJson(mcpPrefsPath(homeDir), {});
|
|
50
|
-
return {
|
|
51
|
-
mode: prefs.mode === DISABLED_MODE ? DISABLED_MODE : ENABLED_MODE,
|
|
52
|
-
updatedAt: typeof prefs.updatedAt === 'string' ? prefs.updatedAt : null,
|
|
53
|
-
};
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export function saveMcpPreferences(mode, homeDir = HOME) {
|
|
57
|
-
const normalizedMode = mode === DISABLED_MODE ? DISABLED_MODE : ENABLED_MODE;
|
|
58
|
-
const next = {
|
|
59
|
-
mode: normalizedMode,
|
|
60
|
-
updatedAt: new Date().toISOString(),
|
|
61
|
-
};
|
|
62
|
-
writeJson(mcpPrefsPath(homeDir), next);
|
|
63
|
-
return next;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
export function isMcpEnabled(homeDir = HOME, env = process.env) {
|
|
67
|
-
if (envDisablesMcp(env)) return false;
|
|
68
|
-
return loadMcpPreferences(homeDir).mode !== DISABLED_MODE;
|
|
69
|
-
}
|
|
70
14
|
|
|
71
15
|
/**
|
|
72
16
|
* Auto-detect MCP server paths.
|
|
@@ -306,28 +250,11 @@ async function resolveClickUpToken(silent = false, cwd = process.cwd()) {
|
|
|
306
250
|
}
|
|
307
251
|
|
|
308
252
|
/**
|
|
309
|
-
* Setup MCP configs globally for Claude Code
|
|
253
|
+
* Setup MCP configs globally for Claude Code and Cursor.
|
|
310
254
|
* Merges ghl-ai server into existing configs without overwriting other servers.
|
|
311
255
|
* Returns list of file paths that were created or updated.
|
|
312
256
|
*/
|
|
313
257
|
export async function setupMcp(cwd, namespace, { silent = false } = {}) {
|
|
314
|
-
const prefs = loadMcpPreferences(HOME);
|
|
315
|
-
if (prefs.mode === DISABLED_MODE) {
|
|
316
|
-
const removed = removeMcpConfig();
|
|
317
|
-
if (!silent) {
|
|
318
|
-
const reason = `${fmt.chalk.dim(mcpPrefsPath(HOME).replace(HOME, '~'))} is disabled`;
|
|
319
|
-
fmt.logInfo(`MCP disabled (${reason}); removed ${removed} AW-managed MCP config file${removed === 1 ? '' : 's'}`);
|
|
320
|
-
}
|
|
321
|
-
return [];
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
if (envDisablesMcp()) {
|
|
325
|
-
if (!silent) {
|
|
326
|
-
fmt.logInfo(`MCP disabled (${DISABLE_MCP_ENV}=1); skipping MCP config updates`);
|
|
327
|
-
}
|
|
328
|
-
return [];
|
|
329
|
-
}
|
|
330
|
-
|
|
331
258
|
const paths = detectPaths();
|
|
332
259
|
const updatedFiles = [];
|
|
333
260
|
|
|
@@ -359,13 +286,13 @@ export async function setupMcp(cwd, namespace, { silent = false } = {}) {
|
|
|
359
286
|
|
|
360
287
|
// ── Claude Code: ~/.claude.json (global) ──
|
|
361
288
|
const claudeJsonPath = join(HOME, '.claude.json');
|
|
362
|
-
if (mergeJsonMcpServer(claudeJsonPath,
|
|
289
|
+
if (mergeJsonMcpServer(claudeJsonPath, 'ghl-ai', ghlAiServerLocal)) {
|
|
363
290
|
updatedFiles.push(claudeJsonPath);
|
|
364
291
|
}
|
|
365
292
|
|
|
366
293
|
// ── Cursor: ~/.cursor/mcp.json (global) ──
|
|
367
294
|
const cursorMcpPath = join(HOME, '.cursor', 'mcp.json');
|
|
368
|
-
if (mergeJsonMcpServer(cursorMcpPath,
|
|
295
|
+
if (mergeJsonMcpServer(cursorMcpPath, 'ghl-ai', ghlAiServerLocal)) {
|
|
369
296
|
updatedFiles.push(cursorMcpPath);
|
|
370
297
|
}
|
|
371
298
|
|
|
@@ -375,11 +302,11 @@ export async function setupMcp(cwd, namespace, { silent = false } = {}) {
|
|
|
375
302
|
// survives — without this, each re-init overwrites ~/.codex/config.toml
|
|
376
303
|
// from the ECC source which doesn't have the ghl-ai block.
|
|
377
304
|
const codexTomlPath = join(HOME, '.codex', 'config.toml');
|
|
378
|
-
if (mergeTomlMcpServer(codexTomlPath,
|
|
305
|
+
if (mergeTomlMcpServer(codexTomlPath, 'ghl-ai', ghlAiServerLocal)) {
|
|
379
306
|
updatedFiles.push(codexTomlPath);
|
|
380
307
|
}
|
|
381
308
|
const eccCodexTomlPath = join(HOME, '.aw-ecc', '.codex', 'config.toml');
|
|
382
|
-
mergeTomlMcpServer(eccCodexTomlPath,
|
|
309
|
+
mergeTomlMcpServer(eccCodexTomlPath, 'ghl-ai', ghlAiServerLocal);
|
|
383
310
|
|
|
384
311
|
// Deduplicate
|
|
385
312
|
const unique = [...new Set(updatedFiles)];
|
|
@@ -399,6 +326,12 @@ export async function setupMcp(cwd, namespace, { silent = false } = {}) {
|
|
|
399
326
|
* Called by `aw nuke`. Returns number of files modified.
|
|
400
327
|
*/
|
|
401
328
|
export function removeMcpConfig() {
|
|
329
|
+
const targets = [
|
|
330
|
+
join(HOME, '.claude.json'),
|
|
331
|
+
join(HOME, '.cursor', 'mcp.json'),
|
|
332
|
+
];
|
|
333
|
+
|
|
334
|
+
// Also clean legacy locations that older versions wrote to
|
|
402
335
|
const jsonTargets = [
|
|
403
336
|
join(HOME, '.claude.json'),
|
|
404
337
|
join(HOME, '.cursor', 'mcp.json'),
|
|
@@ -412,8 +345,8 @@ export function removeMcpConfig() {
|
|
|
412
345
|
if (!existsSync(filePath)) continue;
|
|
413
346
|
try {
|
|
414
347
|
const config = JSON.parse(readFileSync(filePath, 'utf8'));
|
|
415
|
-
if (!config.mcpServers?.[
|
|
416
|
-
delete config.mcpServers[
|
|
348
|
+
if (!config.mcpServers?.['ghl-ai']) continue;
|
|
349
|
+
delete config.mcpServers['ghl-ai'];
|
|
417
350
|
if (Object.keys(config.mcpServers).length === 0) delete config.mcpServers;
|
|
418
351
|
writeFileSync(filePath, JSON.stringify(config, null, 2) + '\n');
|
|
419
352
|
removed++;
|
|
@@ -421,62 +354,13 @@ export function removeMcpConfig() {
|
|
|
421
354
|
}
|
|
422
355
|
|
|
423
356
|
// Codex: ~/.codex/config.toml (TOML format)
|
|
424
|
-
if (removeTomlMcpServer(join(HOME, '.codex', 'config.toml'),
|
|
425
|
-
removed++;
|
|
426
|
-
}
|
|
427
|
-
if (removeTomlMcpServer(join(HOME, '.aw-ecc', '.codex', 'config.toml'), MCP_SERVER_NAME)) {
|
|
357
|
+
if (removeTomlMcpServer(join(HOME, '.codex', 'config.toml'), 'ghl-ai')) {
|
|
428
358
|
removed++;
|
|
429
359
|
}
|
|
430
360
|
|
|
431
361
|
return removed;
|
|
432
362
|
}
|
|
433
363
|
|
|
434
|
-
function jsonMcpHealth(filePath) {
|
|
435
|
-
const server = readJson(filePath, {})?.mcpServers?.[MCP_SERVER_NAME];
|
|
436
|
-
const authorization = typeof server?.headers?.Authorization === 'string'
|
|
437
|
-
&& server.headers.Authorization.trim().length > 0;
|
|
438
|
-
return {
|
|
439
|
-
path: filePath,
|
|
440
|
-
present: !!server,
|
|
441
|
-
url: typeof server?.url === 'string' && /^https?:\/\//.test(server.url),
|
|
442
|
-
authorization,
|
|
443
|
-
};
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
function tomlMcpHealth(filePath) {
|
|
447
|
-
if (!existsSync(filePath)) {
|
|
448
|
-
return { path: filePath, present: false, url: false, authorization: false };
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
try {
|
|
452
|
-
const content = readFileSync(filePath, 'utf8');
|
|
453
|
-
return {
|
|
454
|
-
path: filePath,
|
|
455
|
-
present: new RegExp(`\\[mcp_servers\\.${MCP_SERVER_NAME}\\]`).test(content),
|
|
456
|
-
url: new RegExp(`\\[mcp_servers\\.${MCP_SERVER_NAME}\\][\\s\\S]*?url\\s*=\\s*"https?:\\/\\/[^"]+"`).test(content),
|
|
457
|
-
authorization: new RegExp(`\\[mcp_servers\\.${MCP_SERVER_NAME}\\.headers\\][\\s\\S]*?Authorization\\s*=\\s*".+"`).test(content),
|
|
458
|
-
};
|
|
459
|
-
} catch {
|
|
460
|
-
// Unreadable TOML should behave like an absent optional MCP config.
|
|
461
|
-
return { path: filePath, present: false, url: false, authorization: false };
|
|
462
|
-
}
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
export function getMcpStatus(homeDir = HOME, env = process.env) {
|
|
466
|
-
const prefs = loadMcpPreferences(homeDir);
|
|
467
|
-
return {
|
|
468
|
-
...prefs,
|
|
469
|
-
effectiveMode: isMcpEnabled(homeDir, env) ? ENABLED_MODE : DISABLED_MODE,
|
|
470
|
-
envDisableMcp: envDisablesMcp(env),
|
|
471
|
-
envDisableMcpName: DISABLE_MCP_ENV,
|
|
472
|
-
preferencesPath: mcpPrefsPath(homeDir),
|
|
473
|
-
claude: jsonMcpHealth(join(homeDir, '.claude.json')),
|
|
474
|
-
cursor: jsonMcpHealth(join(homeDir, '.cursor', 'mcp.json')),
|
|
475
|
-
codex: tomlMcpHealth(join(homeDir, '.codex', 'config.toml')),
|
|
476
|
-
eccCodex: tomlMcpHealth(join(homeDir, '.aw-ecc', '.codex', 'config.toml')),
|
|
477
|
-
};
|
|
478
|
-
}
|
|
479
|
-
|
|
480
364
|
// ── TOML helpers for Codex ~/.codex/config.toml ───────────────────────────
|
|
481
365
|
|
|
482
366
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ghl-ai/aw",
|
|
3
|
-
"version": "0.1.50
|
|
3
|
+
"version": "0.1.50",
|
|
4
4
|
"description": "Agentic Workspace CLI — pull, push & manage agents, skills and commands from the registry",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
"fmt.mjs",
|
|
18
18
|
"git.mjs",
|
|
19
19
|
"glob.mjs",
|
|
20
|
+
"integrations/",
|
|
20
21
|
"integrate.mjs",
|
|
21
22
|
"link.mjs",
|
|
22
23
|
"manifest.mjs",
|
|
@@ -34,7 +35,6 @@
|
|
|
34
35
|
"hooks/",
|
|
35
36
|
"startup.mjs",
|
|
36
37
|
"ecc.mjs",
|
|
37
|
-
"integrations.mjs",
|
|
38
38
|
"render-rules.mjs",
|
|
39
39
|
"telemetry.mjs"
|
|
40
40
|
],
|
|
@@ -52,9 +52,9 @@
|
|
|
52
52
|
"license": "MIT",
|
|
53
53
|
"scripts": {
|
|
54
54
|
"test": "yarn test:vitest && yarn test:node",
|
|
55
|
-
"test:vitest": "vitest run --reporter=verbose tests/commands tests/mcp.test.mjs tests/telemetry.test.mjs tests/c4
|
|
55
|
+
"test:vitest": "vitest run --reporter=verbose tests/commands tests/mcp.test.mjs tests/telemetry.test.mjs tests/c4",
|
|
56
56
|
"test:node": "node tests/run-node-tests.mjs",
|
|
57
|
-
"test:watch": "vitest --reporter=verbose tests/commands tests/mcp.test.mjs tests/telemetry.test.mjs tests/c4
|
|
57
|
+
"test:watch": "vitest --reporter=verbose tests/commands tests/mcp.test.mjs tests/telemetry.test.mjs tests/c4",
|
|
58
58
|
"preuninstall": "node bin.js nuke 2>/dev/null || true"
|
|
59
59
|
},
|
|
60
60
|
"publishConfig": {
|
package/render-rules.mjs
CHANGED
|
@@ -5,7 +5,6 @@ import { dirname, join, relative } from 'node:path';
|
|
|
5
5
|
import { homedir } from 'node:os';
|
|
6
6
|
import * as fmt from './fmt.mjs';
|
|
7
7
|
import { RULES_RUNTIME_DIR } from './constants.mjs';
|
|
8
|
-
import { isDefaultRoutingEnabled } from './startup.mjs';
|
|
9
8
|
|
|
10
9
|
// The marker is placed AFTER the YAML frontmatter so Cursor/Markdown parsers
|
|
11
10
|
// see the frontmatter at byte 0. Putting an HTML comment before --- breaks
|
|
@@ -213,24 +212,6 @@ function pruneStaleGeneratedRules(outputDir, expectedFilenames) {
|
|
|
213
212
|
}
|
|
214
213
|
}
|
|
215
214
|
|
|
216
|
-
function defaultRoutingEnabled(options = {}) {
|
|
217
|
-
if (typeof options.defaultRoutingEnabled === 'boolean') {
|
|
218
|
-
return options.defaultRoutingEnabled;
|
|
219
|
-
}
|
|
220
|
-
return isDefaultRoutingEnabled(options.homeDir || homedir());
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
function cleanupRenderedRules(cwd, options = {}) {
|
|
224
|
-
pruneStaleGeneratedRules(join(cwd, '.cursor', 'rules'), new Set());
|
|
225
|
-
pruneStaleGeneratedRules(join(cwd, '.claude', 'rules', 'platform'), new Set());
|
|
226
|
-
|
|
227
|
-
const HOME = options.homeDir || homedir();
|
|
228
|
-
if (cwd !== HOME) {
|
|
229
|
-
pruneStaleGeneratedRules(join(HOME, '.cursor', 'rules'), new Set());
|
|
230
|
-
pruneStaleGeneratedRules(join(HOME, '.claude', 'rules', 'platform'), new Set());
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
|
|
234
215
|
function stackOverlaysEnabled(options = {}) {
|
|
235
216
|
if (typeof options.enableStackOverlays === 'boolean') {
|
|
236
217
|
return options.enableStackOverlays;
|
|
@@ -720,13 +701,8 @@ export function generateAgentsMdRulesSection(rulesDir, options = {}) {
|
|
|
720
701
|
* 2. Returns sections for CLAUDE.md and AGENTS.md injection
|
|
721
702
|
*/
|
|
722
703
|
export function renderRules(cwd, options = {}) {
|
|
723
|
-
if (!defaultRoutingEnabled(options)) {
|
|
724
|
-
cleanupRenderedRules(cwd, options);
|
|
725
|
-
return { cursorCount: 0, claudeCount: 0, claudeSection: '', agentsSection: '' };
|
|
726
|
-
}
|
|
727
|
-
|
|
728
704
|
const rulesDir = resolveRulesSourceDir(cwd, options);
|
|
729
|
-
if (!rulesDir) return { cursorCount: 0,
|
|
705
|
+
if (!rulesDir) return { cursorCount: 0, claudeSection: '', agentsSection: '' };
|
|
730
706
|
|
|
731
707
|
// Resolve applicable scopes for AGENTS.md / CLAUDE.md MUST-rule list.
|
|
732
708
|
// Order: explicit option → .aw/config.json awRuleScopes → auto-detect.
|
package/startup.mjs
CHANGED
|
@@ -13,7 +13,6 @@ import {
|
|
|
13
13
|
const STARTUP_PREFS_FILENAME = 'startup-preferences.json';
|
|
14
14
|
const ENABLED_MODE = 'enabled';
|
|
15
15
|
const DISABLED_MODE = 'disabled';
|
|
16
|
-
const DISABLE_DEFAULT_ROUTING_ENV = 'AW_DISABLE_DEFAULT_ROUTING';
|
|
17
16
|
|
|
18
17
|
const CLAUDE_DISABLE_DESCRIPTION = 'AW-managed override: disable automatic AW session routing';
|
|
19
18
|
const CLAUDE_TELEMETRY_DESCRIPTION = 'AW usage telemetry';
|
|
@@ -543,35 +542,9 @@ function disableCodexStartup(homeDir = homedir()) {
|
|
|
543
542
|
return updatedFiles;
|
|
544
543
|
}
|
|
545
544
|
|
|
546
|
-
function
|
|
547
|
-
const scriptPath = join(homeDir, '.cursor', 'hooks', 'session-start.sh');
|
|
548
|
-
if (!existsSync(scriptPath)) return false;
|
|
549
|
-
|
|
550
|
-
try {
|
|
551
|
-
const content = readFileSync(scriptPath, 'utf8');
|
|
552
|
-
return content.includes('using-aw-skills/hooks/session-start.sh')
|
|
553
|
-
|| content.includes('skills/using-aw-skills/hooks/session-start.sh');
|
|
554
|
-
} catch {
|
|
555
|
-
// Unreadable Cursor shell hook is safest to treat as non-AW managed.
|
|
556
|
-
return false;
|
|
557
|
-
}
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
function isManagedCursorSessionStartEntry(entry, homeDir = homedir()) {
|
|
545
|
+
function isManagedCursorSessionStartEntry(entry) {
|
|
561
546
|
const command = String(entry?.command || '');
|
|
562
|
-
|
|
563
|
-
return true;
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
if (command.includes('using-aw-skills/hooks/session-start.sh')) {
|
|
567
|
-
return true;
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
if (command.includes('.cursor/hooks/session-start.sh')) {
|
|
571
|
-
return cursorSessionStartShellScriptUsesAwRouting(homeDir);
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
return false;
|
|
547
|
+
return command === CURSOR_SESSION_START_COMMAND || command.endsWith('.cursor/hooks/session-start.js');
|
|
575
548
|
}
|
|
576
549
|
|
|
577
550
|
function hasCursorSessionStartScript(homeDir = homedir()) {
|
|
@@ -587,7 +560,7 @@ function disableCursorStartup(homeDir = homedir()) {
|
|
|
587
560
|
return [];
|
|
588
561
|
}
|
|
589
562
|
|
|
590
|
-
const filtered = config.hooks.sessionStart.filter(entry => !isManagedCursorSessionStartEntry(entry
|
|
563
|
+
const filtered = config.hooks.sessionStart.filter(entry => !isManagedCursorSessionStartEntry(entry));
|
|
591
564
|
if (filtered.length === config.hooks.sessionStart.length) {
|
|
592
565
|
return [];
|
|
593
566
|
}
|
|
@@ -649,16 +622,6 @@ export function loadStartupPreferences(homeDir = homedir()) {
|
|
|
649
622
|
};
|
|
650
623
|
}
|
|
651
624
|
|
|
652
|
-
function envDisablesDefaultRouting(env = process.env) {
|
|
653
|
-
const value = String(env[DISABLE_DEFAULT_ROUTING_ENV] || '').trim().toLowerCase();
|
|
654
|
-
return ['1', 'true', 'yes', 'on'].includes(value);
|
|
655
|
-
}
|
|
656
|
-
|
|
657
|
-
export function isDefaultRoutingEnabled(homeDir = homedir(), env = process.env) {
|
|
658
|
-
if (envDisablesDefaultRouting(env)) return false;
|
|
659
|
-
return loadStartupPreferences(homeDir).mode !== DISABLED_MODE;
|
|
660
|
-
}
|
|
661
|
-
|
|
662
625
|
export function saveStartupPreferences(mode, homeDir = homedir()) {
|
|
663
626
|
const normalizedMode = mode === DISABLED_MODE ? DISABLED_MODE : ENABLED_MODE;
|
|
664
627
|
const next = {
|
|
@@ -695,11 +658,11 @@ export function applyGlobalStartupMode(mode, homeDir = homedir()) {
|
|
|
695
658
|
return [...new Set(updatedFiles)];
|
|
696
659
|
}
|
|
697
660
|
|
|
698
|
-
export function applyStoredStartupPreferences(homeDir = homedir()
|
|
699
|
-
return applyGlobalStartupMode(
|
|
661
|
+
export function applyStoredStartupPreferences(homeDir = homedir()) {
|
|
662
|
+
return applyGlobalStartupMode(loadStartupPreferences(homeDir).mode, homeDir);
|
|
700
663
|
}
|
|
701
664
|
|
|
702
|
-
export function getStartupStatus(homeDir = homedir()
|
|
665
|
+
export function getStartupStatus(homeDir = homedir()) {
|
|
703
666
|
const prefs = loadStartupPreferences(homeDir);
|
|
704
667
|
const claudeSettingsPath = join(homeDir, '.claude', 'settings.json');
|
|
705
668
|
const codexHooksPath = join(homeDir, '.codex', 'hooks.json');
|
|
@@ -707,15 +670,9 @@ export function getStartupStatus(homeDir = homedir(), env = process.env) {
|
|
|
707
670
|
const claudeSettings = readJson(claudeSettingsPath, {});
|
|
708
671
|
const codexHooks = readJson(codexHooksPath, {});
|
|
709
672
|
const cursorHooks = readJson(cursorHooksPath, {});
|
|
710
|
-
const cursorSessionStartEntries = Array.isArray(cursorHooks?.hooks?.sessionStart)
|
|
711
|
-
? cursorHooks.hooks.sessionStart
|
|
712
|
-
: [];
|
|
713
673
|
|
|
714
674
|
return {
|
|
715
675
|
...prefs,
|
|
716
|
-
effectiveMode: isDefaultRoutingEnabled(homeDir, env) ? ENABLED_MODE : DISABLED_MODE,
|
|
717
|
-
envDisableDefaultRouting: envDisablesDefaultRouting(env),
|
|
718
|
-
envDisableDefaultRoutingName: DISABLE_DEFAULT_ROUTING_ENV,
|
|
719
676
|
preferencesPath: startupPrefsPath(homeDir),
|
|
720
677
|
claudePluginEnabled: claudeSettings?.enabledPlugins?.['aw@aw-marketplace'] === true,
|
|
721
678
|
claudePluginInstalled: hasClaudePluginCache(homeDir),
|
|
@@ -727,9 +684,8 @@ export function getStartupStatus(homeDir = homedir(), env = process.env) {
|
|
|
727
684
|
codexSessionStartPresent: Array.isArray(codexHooks?.hooks?.SessionStart) &&
|
|
728
685
|
codexHooks.hooks.SessionStart.some(isManagedCodexSessionStartEntry),
|
|
729
686
|
codexSessionStartScriptInstalled: hasCodexSessionStartScript(homeDir),
|
|
730
|
-
cursorSessionStartPresent:
|
|
731
|
-
|
|
732
|
-
cursorAnySessionStartPresent: cursorSessionStartEntries.length > 0,
|
|
687
|
+
cursorSessionStartPresent: Array.isArray(cursorHooks?.hooks?.sessionStart) &&
|
|
688
|
+
cursorHooks.hooks.sessionStart.length > 0,
|
|
733
689
|
cursorSessionStartScriptInstalled: hasCursorSessionStartScript(homeDir),
|
|
734
690
|
};
|
|
735
691
|
}
|