@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/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 = realHomeDir();
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(realHomeDir(), '.aw_registry');
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
- created += linkReferenceFiles(cwd, awDir);
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 === realHomeDir()) {
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, renameSync } from 'node:fs';
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 { dirname, join } from 'node:path';
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, Cursor, and Codex.
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, MCP_SERVER_NAME, ghlAiServerLocal)) {
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, MCP_SERVER_NAME, ghlAiServerLocal)) {
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, MCP_SERVER_NAME, ghlAiServerLocal)) {
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, MCP_SERVER_NAME, ghlAiServerLocal);
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?.[MCP_SERVER_NAME]) continue;
416
- delete config.mcpServers[MCP_SERVER_NAME];
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'), MCP_SERVER_NAME)) {
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-beta.1",
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 tests/integrations-graphify.test.mjs",
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 tests/integrations-graphify.test.mjs",
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, claudeCount: 0, claudeSection: '', agentsSection: '' };
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 cursorSessionStartShellScriptUsesAwRouting(homeDir = homedir()) {
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
- if (command === CURSOR_SESSION_START_COMMAND || command.endsWith('.cursor/hooks/session-start.js')) {
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, homeDir));
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(), env = process.env) {
699
- return applyGlobalStartupMode(isDefaultRoutingEnabled(homeDir, env) ? ENABLED_MODE : DISABLED_MODE, homeDir);
661
+ export function applyStoredStartupPreferences(homeDir = homedir()) {
662
+ return applyGlobalStartupMode(loadStartupPreferences(homeDir).mode, homeDir);
700
663
  }
701
664
 
702
- export function getStartupStatus(homeDir = homedir(), env = process.env) {
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: cursorSessionStartEntries.some(entry =>
731
- isManagedCursorSessionStartEntry(entry, homeDir)),
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
  }