@ghl-ai/aw 0.1.35-beta.2 → 0.1.35-beta.20

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
@@ -19,7 +19,7 @@ import { generateCommands, copyInstructions, initAwDocs } from '../integrate.mjs
19
19
  import { setupMcp } from '../mcp.mjs';
20
20
  import { autoUpdate, promptUpdate } from '../update.mjs';
21
21
  import { installGlobalHooks } from '../hooks.mjs';
22
- import { installIdeHooks } from '../ide-hooks.mjs';
22
+ import { installAwEcc } from '../ecc.mjs';
23
23
 
24
24
  const __dirname = dirname(fileURLToPath(import.meta.url));
25
25
  const VERSION = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf8')).version;
@@ -201,13 +201,13 @@ export async function initCommand(args) {
201
201
 
202
202
  // Re-link IDE dirs + hooks (idempotent)
203
203
  linkWorkspace(HOME);
204
+ await installAwEcc(cwd, { silent });
204
205
  generateCommands(HOME);
205
206
  copyInstructions(HOME, null, freshCfg?.namespace || team) || [];
206
207
  initAwDocs(HOME);
207
208
  setupMcp(HOME, freshCfg?.namespace || team) || [];
208
209
  if (cwd !== HOME) setupMcp(cwd, freshCfg?.namespace || team);
209
210
  installGlobalHooks();
210
- const ideHookFiles = installIdeHooks(HOME);
211
211
 
212
212
  // Link current project if needed
213
213
  if (cwd !== HOME && !existsSync(join(cwd, '.aw_registry'))) {
@@ -282,13 +282,13 @@ export async function initCommand(args) {
282
282
  // Step 3: Link IDE dirs + setup tasks
283
283
  fmt.logStep('Linking IDE symlinks...');
284
284
  linkWorkspace(HOME);
285
+ await installAwEcc(cwd, { silent });
285
286
  generateCommands(HOME);
286
287
  const instructionFiles = copyInstructions(HOME, null, team) || [];
287
288
  initAwDocs(HOME);
288
289
  const mcpFiles = setupMcp(HOME, team) || [];
289
290
  if (cwd !== HOME) setupMcp(cwd, team);
290
291
  const hooksInstalled = installGlobalHooks();
291
- const ideHookFilesInit = installIdeHooks(HOME);
292
292
  installIdeTasks();
293
293
 
294
294
  // Step 4: Symlink in current directory if it's a git repo
@@ -307,7 +307,6 @@ export async function initCommand(args) {
307
307
  createdFiles: [
308
308
  ...instructionFiles.map(p => p.startsWith(HOME) ? p.slice(HOME.length + 1) : p),
309
309
  ...mcpFiles.map(p => p.startsWith(HOME) ? p.slice(HOME.length + 1) : p),
310
- ...(ideHookFilesInit || []).map(p => p.startsWith(HOME) ? p.slice(HOME.length + 1) : p),
311
310
  ],
312
311
  globalHooksDir: hooksInstalled ? join(HOME, '.aw', 'hooks') : null,
313
312
  };
package/commands/nuke.mjs CHANGED
@@ -9,7 +9,7 @@ import { execSync } from 'node:child_process';
9
9
  import * as fmt from '../fmt.mjs';
10
10
  import { chalk } from '../fmt.mjs';
11
11
  import { removeGlobalHooks } from '../hooks.mjs';
12
- import { removeIdeHooks } from '../ide-hooks.mjs';
12
+ import { uninstallAwEcc } from '../ecc.mjs';
13
13
 
14
14
  const HOME = homedir();
15
15
  const GLOBAL_AW_DIR = join(HOME, '.aw_registry');
@@ -244,31 +244,31 @@ export function nukeCommand(args) {
244
244
  // 2. Remove IDE symlinks (only those pointing to .aw_registry)
245
245
  removeIdeSymlinks();
246
246
 
247
- // 3. Remove .aw_registry symlinks from ALL project directories
247
+ // 3. Remove aw-ecc installed files (agents, commands, rules, skills, hooks)
248
+ uninstallAwEcc();
249
+
250
+ // 4. Remove .aw_registry symlinks from ALL project directories
248
251
  removeProjectSymlinks();
249
252
 
250
- // 4. Remove git hooks (core.hooksPath + legacy template)
253
+ // 5. Remove git hooks (core.hooksPath + legacy template)
251
254
  removeGitHooks(manifest);
252
255
 
253
- // 5. Remove IDE session hooks (superpowers bootstrap)
254
- removeIdeHooks(HOME);
255
-
256
256
  // 6. Remove IDE auto-init tasks
257
257
  removeIdeTasks();
258
258
 
259
- // 5b. Remove upgrade lock/log (inside .aw_registry, must happen before dir removal)
259
+ // Remove upgrade lock/log (inside .aw_registry, must happen before dir removal)
260
260
  for (const p of [join(GLOBAL_AW_DIR, '.aw-upgrade.lock'), join(GLOBAL_AW_DIR, '.aw-upgrade.log')]) {
261
261
  try { if (existsSync(p)) rmSync(p, { recursive: true, force: true }); } catch { /* best effort */ }
262
262
  }
263
263
 
264
- // 6. Remove ~/.aw_docs/
264
+ // 7. Remove ~/.aw_docs/
265
265
  const awDocs = join(HOME, '.aw_docs');
266
266
  if (existsSync(awDocs)) {
267
267
  rmSync(awDocs, { recursive: true, force: true });
268
268
  fmt.logStep('Removed ~/.aw_docs/');
269
269
  }
270
270
 
271
- // 7. Remove any manual `aw` symlinks (e.g. ~/.local/bin/aw)
271
+ // 8. Remove any manual `aw` symlinks (e.g. ~/.local/bin/aw)
272
272
  const manualBins = [
273
273
  join(HOME, '.local', 'bin', 'aw'),
274
274
  join(HOME, 'bin', 'aw'),
@@ -282,7 +282,7 @@ export function nukeCommand(args) {
282
282
  } catch { /* doesn't exist */ }
283
283
  }
284
284
 
285
- // 8. Uninstall npm global package (skip if already in npm uninstall lifecycle)
285
+ // 9. Uninstall npm global package (skip if already in npm uninstall lifecycle)
286
286
  if (!process.env.npm_lifecycle_event) {
287
287
  try {
288
288
  execSync('npm uninstall -g @ghl-ai/aw', { stdio: 'pipe', timeout: 15000 });
@@ -290,7 +290,7 @@ export function nukeCommand(args) {
290
290
  } catch { /* not installed via npm or no permissions */ }
291
291
  }
292
292
 
293
- // 9. Remove ~/.aw_registry/ itself (source of truth — last!)
293
+ // 10. Remove ~/.aw_registry/ itself (source of truth — last!)
294
294
  rmSync(GLOBAL_AW_DIR, { recursive: true, force: true });
295
295
  fmt.logStep('Removed ~/.aw_registry/');
296
296
 
@@ -299,6 +299,7 @@ export function nukeCommand(args) {
299
299
  '',
300
300
  ` ${chalk.green('✓')} Generated files cleaned`,
301
301
  ` ${chalk.green('✓')} IDE symlinks cleaned`,
302
+ ` ${chalk.green('✓')} aw-ecc engine removed`,
302
303
  ` ${chalk.green('✓')} Project symlinks cleaned`,
303
304
  ` ${chalk.green('✓')} Git hooks removed`,
304
305
  ` ${chalk.green('✓')} IDE auto-sync tasks removed`,
package/commands/push.mjs CHANGED
@@ -73,17 +73,12 @@ function collectBatchFiles(folderAbsPath, workspaceDir) {
73
73
  }
74
74
 
75
75
  /**
76
- * Collect all modified + untracked files for no-args push.
77
- * 1. Manifest-tracked files that are modified or template-derived (never pushed).
78
- * 2. Filesystem files not in the manifest at all (newly added by user).
76
+ * Collect all modified files from manifest (for no-args push).
79
77
  * Returns array of { absPath, registryTarget, type, namespace, slug, isDir }.
80
78
  */
81
79
  function collectModifiedFiles(workspaceDir) {
82
80
  const manifest = loadManifest(workspaceDir);
83
- const manifestKeys = new Set(Object.keys(manifest.files || {}));
84
81
  const files = [];
85
-
86
- // 1. Manifest-tracked: modified or never-pushed
87
82
  for (const [key, entry] of Object.entries(manifest.files || {})) {
88
83
  const filePath = join(workspaceDir, key);
89
84
  if (!existsSync(filePath)) continue;
@@ -104,30 +99,6 @@ function collectModifiedFiles(workspaceDir) {
104
99
  }
105
100
  }
106
101
  }
107
-
108
- // 2. Untracked: files on disk but not in manifest (e.g. manually added)
109
- for (const name of readdirSync(workspaceDir, { withFileTypes: true })) {
110
- if (!name.isDirectory() || name.name.startsWith('.')) continue;
111
- const nsDir = join(workspaceDir, name.name);
112
- const entries = walkRegistryTree(nsDir, name.name);
113
- for (const entry of entries) {
114
- // Build the manifest key for this file
115
- const manifestKey = (entry.type === 'skills' || entry.type === 'evals')
116
- ? `${entry.namespacePath}/${entry.type}/${entry.slug}/${entry.skillRelPath || entry.filename}`
117
- : `${entry.namespacePath}/${entry.type}/${entry.filename}`;
118
- if (manifestKeys.has(manifestKey)) continue; // Already handled above
119
- const registryTarget = `${REGISTRY_DIR}/${manifestKey}`;
120
- files.push({
121
- absPath: entry.sourcePath,
122
- registryTarget,
123
- type: entry.type,
124
- namespace: entry.namespacePath,
125
- slug: entry.slug,
126
- isDir: false,
127
- });
128
- }
129
- }
130
-
131
102
  return files;
132
103
  }
133
104
 
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 = 'sync/platform-superpowers-l8ynb';
4
+ export const REGISTRY_BASE_BRANCH = 'main';
5
5
 
6
6
  /** Default registry repository */
7
7
  export const REGISTRY_REPO = 'GoHighLevel/platform-docs';
package/ecc.mjs ADDED
@@ -0,0 +1,180 @@
1
+ import { execSync } from "node:child_process";
2
+ import {
3
+ existsSync, readFileSync, readdirSync,
4
+ mkdirSync, rmSync, writeFileSync,
5
+ } from "node:fs";
6
+ import { dirname, join } from "node:path";
7
+ import { homedir } from "node:os";
8
+ import * as fmt from "./fmt.mjs";
9
+
10
+ const AW_ECC_REPO_SSH = "git@github.com:shreyansh-ghl/aw-ecc.git";
11
+ const AW_ECC_REPO_HTTPS = "https://github.com/shreyansh-ghl/aw-ecc.git";
12
+ const AW_ECC_TAG = "v1.2.0";
13
+
14
+ const MARKETPLACE_NAME = "aw-marketplace";
15
+ const PLUGIN_KEY = `aw@${MARKETPLACE_NAME}`;
16
+
17
+ function eccDir() { return join(homedir(), ".aw-ecc"); }
18
+
19
+ const FILE_COPY_TARGETS = ["cursor", "codex"];
20
+
21
+ const TARGET_STATE = {
22
+ cursor: { state: ".cursor/ecc-install-state.json" },
23
+ codex: { state: ".codex/ecc-install-state.json" },
24
+ };
25
+
26
+ function run(cmd, opts = {}) {
27
+ return execSync(cmd, { stdio: "pipe", ...opts });
28
+ }
29
+
30
+ function cloneOrUpdate(tag, dest) {
31
+ if (existsSync(join(dest, ".git"))) {
32
+ try {
33
+ run(`git -C ${dest} fetch --quiet --depth 1 origin tag ${tag}`);
34
+ run(`git -C ${dest} checkout --quiet ${tag}`);
35
+ return;
36
+ } catch { /* fall through to fresh clone */ }
37
+ }
38
+ if (existsSync(dest)) rmSync(dest, { recursive: true, force: true });
39
+ try {
40
+ run(`git clone --quiet --depth 1 --branch ${tag} ${AW_ECC_REPO_SSH} ${dest}`);
41
+ } catch {
42
+ run(`git clone --quiet --depth 1 --branch ${tag} ${AW_ECC_REPO_HTTPS} ${dest}`);
43
+ }
44
+ }
45
+
46
+ function installClaudePlugin(repoDir) {
47
+ try {
48
+ run(`claude plugin marketplace add ${repoDir} --scope user`);
49
+ } catch {
50
+ try { run(`claude plugin marketplace update ${MARKETPLACE_NAME}`); } catch { /* ok */ }
51
+ }
52
+ run(`claude plugin install ${PLUGIN_KEY} --scope user`);
53
+ }
54
+
55
+ function uninstallClaudePlugin() {
56
+ try { run(`claude plugin uninstall ${PLUGIN_KEY} --scope user`); } catch { /* not installed */ }
57
+ try { run(`claude plugin marketplace remove ${MARKETPLACE_NAME}`); } catch { /* not registered */ }
58
+ }
59
+
60
+ export async function installAwEcc(
61
+ cwd,
62
+ { targets = ["cursor", "claude", "codex"], silent = false } = {},
63
+ ) {
64
+ if (!silent) fmt.logStep("Installing aw-ecc engine...");
65
+
66
+ const repoDir = eccDir();
67
+
68
+ try {
69
+ cloneOrUpdate(AW_ECC_TAG, repoDir);
70
+
71
+ // Claude Code: plugin install via marketplace CLI (proper agent dispatch)
72
+ if (targets.includes("claude")) {
73
+ try {
74
+ installClaudePlugin(repoDir);
75
+ } catch (err) {
76
+ if (!silent) fmt.logWarn(`Claude plugin install skipped: ${err.message}`);
77
+ }
78
+ }
79
+
80
+ // Cursor + Codex: file-copy via install-apply.js
81
+ const fileCopyTargets = targets.filter((t) => FILE_COPY_TARGETS.includes(t));
82
+ if (fileCopyTargets.length > 0) {
83
+ run("npm install --no-audit --no-fund --ignore-scripts --loglevel=error", {
84
+ cwd: repoDir,
85
+ });
86
+ for (const target of fileCopyTargets) {
87
+ try {
88
+ run(
89
+ `node ${join(repoDir, "scripts/install-apply.js")} --target ${target} --profile full`,
90
+ { cwd },
91
+ );
92
+ } catch { /* target not supported — skip */ }
93
+ }
94
+ }
95
+
96
+ if (!silent) fmt.logSuccess("aw-ecc engine installed");
97
+ } catch (err) {
98
+ if (!silent) fmt.logWarn(`aw-ecc install failed: ${err.message}`);
99
+ }
100
+ }
101
+
102
+ export function uninstallAwEcc({ silent = false } = {}) {
103
+ const HOME = homedir();
104
+ let removed = 0;
105
+
106
+ // Claude Code: uninstall plugin + remove marketplace via CLI
107
+ try {
108
+ uninstallClaudePlugin();
109
+ removed++;
110
+ } catch { /* best effort */ }
111
+
112
+ // Cursor + Codex: remove file-copied content via install-state
113
+ for (const cfg of Object.values(TARGET_STATE)) {
114
+ const statePath = join(HOME, cfg.state);
115
+ if (!existsSync(statePath)) continue;
116
+
117
+ try {
118
+ const data = JSON.parse(readFileSync(statePath, "utf8"));
119
+ for (const op of data.operations || []) {
120
+ if (op.destinationPath && existsSync(op.destinationPath)) {
121
+ rmSync(op.destinationPath, { recursive: true, force: true });
122
+ removed++;
123
+ pruneEmptyParents(op.destinationPath, join(HOME, cfg.state.split("/")[0]));
124
+ }
125
+ }
126
+ rmSync(statePath, { force: true });
127
+ pruneEmptyParents(statePath, join(HOME, cfg.state.split("/")[0]));
128
+ } catch { /* corrupted state — skip */ }
129
+ }
130
+
131
+ // Clean leftover claude install-state from older file-copy versions
132
+ const claudeState = join(HOME, ".claude", "ecc", "install-state.json");
133
+ if (existsSync(claudeState)) {
134
+ try {
135
+ const data = JSON.parse(readFileSync(claudeState, "utf8"));
136
+ for (const op of data.operations || []) {
137
+ if (op.destinationPath && existsSync(op.destinationPath)) {
138
+ rmSync(op.destinationPath, { recursive: true, force: true });
139
+ removed++;
140
+ }
141
+ }
142
+ rmSync(claudeState, { force: true });
143
+ pruneEmptyParents(claudeState, join(HOME, ".claude"));
144
+ } catch { /* best effort */ }
145
+ }
146
+
147
+ // Clean leftover manual plugin cache from older versions
148
+ const oldCache = join(HOME, ".claude", "plugins", "cache", "aw");
149
+ if (existsSync(oldCache)) {
150
+ rmSync(oldCache, { recursive: true, force: true });
151
+ removed++;
152
+ }
153
+
154
+ // Remove permanent aw-ecc repo clone
155
+ const repoDir = eccDir();
156
+ if (existsSync(repoDir)) {
157
+ rmSync(repoDir, { recursive: true, force: true });
158
+ removed++;
159
+ }
160
+
161
+ if (!silent && removed > 0)
162
+ fmt.logStep(`Removed ${removed} aw-ecc file${removed > 1 ? "s" : ""}`);
163
+ return removed;
164
+ }
165
+
166
+ function pruneEmptyParents(filePath, stopAt) {
167
+ let dir = dirname(filePath);
168
+ while (dir !== stopAt && dir.startsWith(stopAt)) {
169
+ try {
170
+ if (readdirSync(dir).length === 0) {
171
+ rmSync(dir);
172
+ dir = dirname(dir);
173
+ } else {
174
+ break;
175
+ }
176
+ } catch {
177
+ break;
178
+ }
179
+ }
180
+ }
package/integrate.mjs CHANGED
@@ -136,7 +136,7 @@ Team: ${team} | Local-first orchestration via \`.aw_docs/\` | MCPs: \`memory/*\`
136
136
  └── tasks/BOARD.md # Task board
137
137
  \`\`\`
138
138
 
139
- Symlinked to \`.claude/{agents,skills,commands/aw,evals}\`.
139
+ Symlinked to \`.claude/{agents,skills,commands/ghl,evals}\`.
140
140
 
141
141
  ## Local-First Operations
142
142
 
@@ -172,7 +172,7 @@ stitch/* → External design generation
172
172
  All symlinks are prefixed with their namespace (\`platform-\` or \`${team}-\`):
173
173
  - \`.aw_registry/platform/review/agents/security-reviewer.md\` → \`.claude/agents/platform-review-security-reviewer.md\`
174
174
  - \`.aw_registry/${team}/frontend/agents/frontend-developer.md\` → \`.claude/agents/${team}-frontend-developer.md\`
175
- - \`.aw_registry/${team}/commands/ship.md\` → \`.claude/commands/aw/${team}-ship.md\`
175
+ - \`.aw_registry/${team}/commands/ship.md\` → \`.claude/commands/ghl/${team}-ship.md\`
176
176
 
177
177
  ## Dependency Rule
178
178
 
package/link.mjs CHANGED
@@ -217,7 +217,7 @@ export function linkWorkspace(cwd) {
217
217
  const cmdFileName = [ns, ...segments, file].join('-');
218
218
 
219
219
  for (const ide of IDE_DIRS) {
220
- const linkDir = join(cwd, ide, 'commands', 'aw');
220
+ const linkDir = join(cwd, ide, 'commands', 'ghl');
221
221
  mkdirSync(linkDir, { recursive: true });
222
222
  const linkPath = join(linkDir, cmdFileName);
223
223
  const targetPath = join(commandsDir, file);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ghl-ai/aw",
3
- "version": "0.1.35-beta.2",
3
+ "version": "0.1.35-beta.20",
4
4
  "description": "Agentic Workspace CLI — pull, push & manage agents, skills and commands from the registry",
5
5
  "type": "module",
6
6
  "bin": {
@@ -25,7 +25,7 @@
25
25
  "apply.mjs",
26
26
  "update.mjs",
27
27
  "hooks.mjs",
28
- "ide-hooks.mjs"
28
+ "ecc.mjs"
29
29
  ],
30
30
  "engines": {
31
31
  "node": ">=18.0.0"
package/ide-hooks.mjs DELETED
@@ -1,193 +0,0 @@
1
- // ide-hooks.mjs — Generate IDE session-start hooks for superpowers bootstrap.
2
- //
3
- // installIdeHooks(cwd) → writes/merges hook configs into ~/.claude/ and ~/.cursor/
4
- // removeIdeHooks(cwd) → removes aw-generated hook entries from IDE hook configs
5
-
6
- import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
7
- import { join } from 'node:path';
8
- import { homedir } from 'node:os';
9
- import * as fmt from './fmt.mjs';
10
-
11
- const HOME = homedir();
12
- const AW_REGISTRY = join(HOME, '.aw_registry');
13
- const HOOKS_DIR = join(AW_REGISTRY, 'platform', 'superpowers', 'hooks');
14
- const SESSION_START = join(HOOKS_DIR, 'session-start');
15
- const RUN_HOOK_CMD = join(HOOKS_DIR, 'run-hook.cmd');
16
-
17
- const AW_MARKER = 'aw-superpowers-session';
18
-
19
- /**
20
- * Install IDE session-start hooks that bootstrap using-superpowers.
21
- * Only installs if the superpowers hooks exist in the registry.
22
- * Merges with existing hook configs — never clobbers user hooks.
23
- * @returns {string[]} paths of created/modified hook config files
24
- */
25
- export function installIdeHooks(cwd) {
26
- if (!existsSync(SESSION_START)) return [];
27
-
28
- const created = [];
29
-
30
- const claudeResult = installClaudeCodeHooks(cwd);
31
- if (claudeResult) created.push(claudeResult);
32
-
33
- const cursorResult = installCursorHooks(cwd);
34
- if (cursorResult) created.push(cursorResult);
35
-
36
- if (created.length > 0) {
37
- fmt.logStep('IDE session hooks installed (superpowers bootstrap)');
38
- }
39
-
40
- return created;
41
- }
42
-
43
- /**
44
- * Remove aw-generated hook entries from IDE hook configs.
45
- */
46
- export function removeIdeHooks(cwd) {
47
- removeClaudeCodeHooks(cwd);
48
- removeCursorHooks(cwd);
49
- fmt.logStep('IDE session hooks removed');
50
- }
51
-
52
- // ── Claude Code ─────────────────────────────────────────────────────────
53
-
54
- function installClaudeCodeHooks(cwd) {
55
- const hooksPath = join(HOME, '.claude', 'hooks.json');
56
-
57
- const hookEntry = {
58
- matcher: AW_MARKER,
59
- hooks: [{
60
- type: 'command',
61
- command: `"${RUN_HOOK_CMD}" session-start`,
62
- async: false,
63
- }],
64
- };
65
-
66
- let config;
67
- if (existsSync(hooksPath)) {
68
- try {
69
- config = JSON.parse(readFileSync(hooksPath, 'utf8'));
70
- } catch {
71
- return null;
72
- }
73
-
74
- if (!config.hooks) config.hooks = {};
75
- if (!Array.isArray(config.hooks.SessionStart)) config.hooks.SessionStart = [];
76
-
77
- const existing = config.hooks.SessionStart.findIndex(
78
- e => e.matcher === AW_MARKER
79
- );
80
- if (existing !== -1) {
81
- config.hooks.SessionStart[existing] = hookEntry;
82
- } else {
83
- config.hooks.SessionStart.push(hookEntry);
84
- }
85
- } else {
86
- mkdirSync(join(HOME, '.claude'), { recursive: true });
87
- config = {
88
- hooks: {
89
- SessionStart: [hookEntry],
90
- },
91
- };
92
- }
93
-
94
- writeFileSync(hooksPath, JSON.stringify(config, null, 2) + '\n');
95
- return hooksPath;
96
- }
97
-
98
- function removeClaudeCodeHooks(cwd) {
99
- const hooksPath = join(HOME, '.claude', 'hooks.json');
100
- if (!existsSync(hooksPath)) return;
101
-
102
- try {
103
- const config = JSON.parse(readFileSync(hooksPath, 'utf8'));
104
- if (!config.hooks?.SessionStart) return;
105
-
106
- config.hooks.SessionStart = config.hooks.SessionStart.filter(
107
- e => e.matcher !== AW_MARKER
108
- );
109
-
110
- if (config.hooks.SessionStart.length === 0) {
111
- delete config.hooks.SessionStart;
112
- }
113
- if (Object.keys(config.hooks).length === 0) {
114
- delete config.hooks;
115
- }
116
-
117
- if (Object.keys(config).length === 0) {
118
- writeFileSync(hooksPath, '{}\n');
119
- } else {
120
- writeFileSync(hooksPath, JSON.stringify(config, null, 2) + '\n');
121
- }
122
- } catch { /* best effort */ }
123
- }
124
-
125
- // ── Cursor ──────────────────────────────────────────────────────────────
126
-
127
- function installCursorHooks(cwd) {
128
- const hooksPath = join(HOME, '.cursor', 'hooks.json');
129
-
130
- const hookEntry = {
131
- command: SESSION_START,
132
- _aw: AW_MARKER,
133
- };
134
-
135
- let config;
136
- if (existsSync(hooksPath)) {
137
- try {
138
- config = JSON.parse(readFileSync(hooksPath, 'utf8'));
139
- } catch {
140
- return null;
141
- }
142
-
143
- if (!config.hooks) config.hooks = {};
144
- if (!Array.isArray(config.hooks.sessionStart)) config.hooks.sessionStart = [];
145
-
146
- const existing = config.hooks.sessionStart.findIndex(
147
- e => e._aw === AW_MARKER
148
- );
149
- if (existing !== -1) {
150
- config.hooks.sessionStart[existing] = hookEntry;
151
- } else {
152
- config.hooks.sessionStart.push(hookEntry);
153
- }
154
- } else {
155
- mkdirSync(join(HOME, '.cursor'), { recursive: true });
156
- config = {
157
- version: 1,
158
- hooks: {
159
- sessionStart: [hookEntry],
160
- },
161
- };
162
- }
163
-
164
- writeFileSync(hooksPath, JSON.stringify(config, null, 2) + '\n');
165
- return hooksPath;
166
- }
167
-
168
- function removeCursorHooks(cwd) {
169
- const hooksPath = join(HOME, '.cursor', 'hooks.json');
170
- if (!existsSync(hooksPath)) return;
171
-
172
- try {
173
- const config = JSON.parse(readFileSync(hooksPath, 'utf8'));
174
- if (!config.hooks?.sessionStart) return;
175
-
176
- config.hooks.sessionStart = config.hooks.sessionStart.filter(
177
- e => e._aw !== AW_MARKER
178
- );
179
-
180
- if (config.hooks.sessionStart.length === 0) {
181
- delete config.hooks.sessionStart;
182
- }
183
- if (config.hooks && Object.keys(config.hooks).length === 0) {
184
- delete config.hooks;
185
- }
186
-
187
- if (Object.keys(config).length <= 1 && config.version) {
188
- writeFileSync(hooksPath, JSON.stringify({ version: config.version }, null, 2) + '\n');
189
- } else {
190
- writeFileSync(hooksPath, JSON.stringify(config, null, 2) + '\n');
191
- }
192
- } catch { /* best effort */ }
193
- }