@ghl-ai/aw 0.1.73 → 0.1.76
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/codex.mjs +20 -7
- package/commands/doctor.mjs +66 -4
- package/commands/init.mjs +17 -14
- package/commands/nuke.mjs +6 -0
- package/commands/push.mjs +18 -0
- package/commands/startup.mjs +9 -7
- package/ecc.mjs +1 -1
- package/hooks/aw-usage/hooks/aw-usage-commit-created.js +13 -4
- package/hooks/aw-usage/hooks/aw-usage-post-tool-use.js +65 -0
- package/hooks/aw-usage/hooks/aw-usage-prompt-submit.js +3 -1
- package/hooks/aw-usage/lib/aw-usage-telemetry.js +85 -1
- package/hooks.mjs +2 -2
- package/link.mjs +38 -13
- package/mcp.mjs +13 -4
- package/package.json +3 -3
- package/render-rules.mjs +2 -2
- package/startup.mjs +207 -19
package/codex.mjs
CHANGED
|
@@ -88,14 +88,27 @@ set -euo pipefail
|
|
|
88
88
|
|
|
89
89
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
90
90
|
ROOT_DIR="$(cd "\${SCRIPT_DIR}/.." && pwd)"
|
|
91
|
-
TARGET_SCRIPT="\$ROOT_DIR/.aw_registry/platform/core/skills/using-aw-skills/hooks/session-start.sh"
|
|
92
91
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
92
|
+
# Resolve the using-aw-skills router. Prefer the aw/ namespace (canonical since the
|
|
93
|
+
# platform/core -> aw skills rename); fall back to the platform/core mirror and the
|
|
94
|
+
# global ~/.aw registry so a workspace with a partial local registry still routes.
|
|
95
|
+
# Pointing only at platform/core injected an EMPTY router after the rename (the cause
|
|
96
|
+
# of the SDLC skill-invocation regression).
|
|
97
|
+
TARGET_CANDIDATES=(
|
|
98
|
+
"\$ROOT_DIR/.aw_registry/aw/skills/using-aw-skills/hooks/session-start.sh"
|
|
99
|
+
"\$HOME/.aw/.aw_registry/aw/skills/using-aw-skills/hooks/session-start.sh"
|
|
100
|
+
"\$ROOT_DIR/.aw_registry/platform/core/skills/using-aw-skills/hooks/session-start.sh"
|
|
101
|
+
"\$HOME/.aw/.aw_registry/platform/core/skills/using-aw-skills/hooks/session-start.sh"
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
for TARGET_SCRIPT in "\${TARGET_CANDIDATES[@]}"; do
|
|
105
|
+
if [[ -f "\$TARGET_SCRIPT" ]]; then
|
|
106
|
+
exec bash "\$TARGET_SCRIPT"
|
|
107
|
+
fi
|
|
108
|
+
done
|
|
109
|
+
|
|
110
|
+
echo '{"hookSpecificOutput": {"hookEventName": "SessionStart", "additionalContext": "WARNING: AW using-aw-skills router not found in .aw_registry (aw/ or platform/core). Run aw pull aw or aw init."}}'
|
|
111
|
+
exit 0
|
|
99
112
|
`;
|
|
100
113
|
|
|
101
114
|
export function ensureCodexWorkspaceDefaults(cwd) {
|
package/commands/doctor.mjs
CHANGED
|
@@ -141,6 +141,9 @@ function parseLegacyClaudeHookTargets(sessionStartEntries = []) {
|
|
|
141
141
|
const targets = [];
|
|
142
142
|
for (const entry of sessionStartEntries) {
|
|
143
143
|
if (!Array.isArray(entry?.hooks)) continue;
|
|
144
|
+
// Only the deprecated platform/core path is "legacy"; the canonical aw/ path (used by
|
|
145
|
+
// the managed router's for-loop command) must NOT be treated as a stale hook.
|
|
146
|
+
if (entry?.description === 'AW-managed: using-aw-skills session router') continue;
|
|
144
147
|
for (const hook of entry.hooks) {
|
|
145
148
|
const command = String(hook?.command || '');
|
|
146
149
|
if (!command.includes('.aw_registry/platform/core/skills/using-aw-skills/hooks/session-start.sh')) continue;
|
|
@@ -582,21 +585,80 @@ function missingCoreClaudeCommandFiles(pluginRoot) {
|
|
|
582
585
|
.filter(relativePath => !existsSync(join(pluginRoot, relativePath)));
|
|
583
586
|
}
|
|
584
587
|
|
|
588
|
+
const RUNTIME_HOOK_FIX = 'Run `aw init` or `aw pull platform` to restore the AW runtime hook.';
|
|
589
|
+
|
|
590
|
+
// Anti-false-green gate. A session-start hook can be WIRED yet resolve to a HOLLOW
|
|
591
|
+
// script that emits no routing card — the platform/core -> aw rename left exactly such
|
|
592
|
+
// a stub. Existence alone is not enough: execute the resolved router and require its
|
|
593
|
+
// output to actually name the router, so `aw doctor` reports a problem instead of PASS.
|
|
594
|
+
function probeRouterCard(resolvedPath) {
|
|
595
|
+
if (process.platform === 'win32') return { status: 'warn', reason: 'exec-unavailable' };
|
|
596
|
+
let res;
|
|
597
|
+
try {
|
|
598
|
+
res = spawnSync('bash', [resolvedPath], {
|
|
599
|
+
input: '',
|
|
600
|
+
encoding: 'utf8',
|
|
601
|
+
timeout: 5000,
|
|
602
|
+
env: { ...process.env, CLAUDE_PLUGIN_ROOT: '1' },
|
|
603
|
+
});
|
|
604
|
+
} catch (error) {
|
|
605
|
+
return { status: 'fail', reason: 'exec-error', detail: error instanceof Error ? error.message : String(error) };
|
|
606
|
+
}
|
|
607
|
+
if (res.error || res.status !== 0) {
|
|
608
|
+
const detail = (res.error?.message || `exit ${res.status}`)
|
|
609
|
+
+ (res.stderr ? ` :: ${String(res.stderr).trim().slice(-200)}` : '');
|
|
610
|
+
return { status: 'fail', reason: 'exec-error', detail };
|
|
611
|
+
}
|
|
612
|
+
// Substring detection on raw stdout — robust to JSON-escaping quirks (the real script
|
|
613
|
+
// escapes via python3 with an awk fallback); the hollow stub prints "ok" with no card.
|
|
614
|
+
const out = String(res.stdout || '');
|
|
615
|
+
if (!out.includes('using-aw-skills')) return { status: 'fail', reason: 'hollow' };
|
|
616
|
+
if (!out.includes('/aw:plan')) return { status: 'warn', reason: 'partial' };
|
|
617
|
+
return { status: 'pass', reason: 'card' };
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
function buildRuntimeHookCheck(resolvedPath, homeDir) {
|
|
621
|
+
if (!resolvedPath) {
|
|
622
|
+
return makeCheck('runtime-hook', 'AW runtime hook', 'fail',
|
|
623
|
+
'using-aw-skills hook is missing from ~/.aw_registry', RUNTIME_HOOK_FIX);
|
|
624
|
+
}
|
|
625
|
+
const shortPath = resolvedPath.replace(`${homeDir}/`, '~/');
|
|
626
|
+
const probe = probeRouterCard(resolvedPath);
|
|
627
|
+
if (probe.status === 'pass') {
|
|
628
|
+
return makeCheck('runtime-hook', 'AW runtime hook', 'pass',
|
|
629
|
+
`using-aw-skills router at ${shortPath} emits the routing card`);
|
|
630
|
+
}
|
|
631
|
+
if (probe.reason === 'exec-unavailable') {
|
|
632
|
+
return makeCheck('runtime-hook', 'AW runtime hook', 'warn',
|
|
633
|
+
`Found using-aw-skills hook at ${shortPath}; card content not verified on this platform`);
|
|
634
|
+
}
|
|
635
|
+
if (probe.status === 'warn') {
|
|
636
|
+
return makeCheck('runtime-hook', 'AW runtime hook', 'warn',
|
|
637
|
+
`using-aw-skills router at ${shortPath} emits a partial card (missing /aw:plan surface)`, RUNTIME_HOOK_FIX);
|
|
638
|
+
}
|
|
639
|
+
const why = probe.reason === 'hollow'
|
|
640
|
+
? `using-aw-skills router at ${shortPath} resolves to a hollow script that emits no routing card`
|
|
641
|
+
: `using-aw-skills router at ${shortPath} resolved but failed to emit a card (${probe.detail || probe.reason})`;
|
|
642
|
+
return makeCheck('runtime-hook', 'AW runtime hook', 'fail', why, RUNTIME_HOOK_FIX);
|
|
643
|
+
}
|
|
644
|
+
|
|
585
645
|
function buildDoctorChecks(homeDir, cwd) {
|
|
586
646
|
const startup = getStartupStatus(homeDir);
|
|
587
647
|
const checks = [];
|
|
588
648
|
const awRegistryDir = resolveAwRegistryDir(homeDir);
|
|
589
649
|
const rulesDir = resolveRulesDir(homeDir);
|
|
590
650
|
|
|
651
|
+
// Resolve the on-disk using-aw-skills router, preferring the aw/ namespace (canonical
|
|
652
|
+
// since the platform/core -> aw rename) over the legacy platform/core mirror, then probe
|
|
653
|
+
// that it actually emits a routing card (not a hollow stub).
|
|
591
654
|
const usingAwHookPath = [
|
|
655
|
+
join(homeDir, '.aw_registry', 'aw', 'skills', 'using-aw-skills', 'hooks', 'session-start.sh'),
|
|
656
|
+
join(homeDir, '.aw', '.aw_registry', 'aw', 'skills', 'using-aw-skills', 'hooks', 'session-start.sh'),
|
|
592
657
|
join(homeDir, '.aw_registry', 'platform', 'core', 'skills', 'using-aw-skills', 'hooks', 'session-start.sh'),
|
|
593
658
|
join(homeDir, '.aw', '.aw_registry', 'platform', 'core', 'skills', 'using-aw-skills', 'hooks', 'session-start.sh'),
|
|
594
659
|
].find(existsSync);
|
|
595
660
|
|
|
596
|
-
checks.push(usingAwHookPath
|
|
597
|
-
? makeCheck('runtime-hook', 'AW runtime hook', 'pass', `Found using-aw-skills hook at ${usingAwHookPath.replace(`${homeDir}/`, '~/')}`)
|
|
598
|
-
: makeCheck('runtime-hook', 'AW runtime hook', 'fail', 'using-aw-skills hook is missing from ~/.aw_registry', 'Run `aw init` or `aw pull platform` to restore the AW runtime hook.'),
|
|
599
|
-
);
|
|
661
|
+
checks.push(buildRuntimeHookCheck(usingAwHookPath, homeDir));
|
|
600
662
|
|
|
601
663
|
checks.push(startup.mode === 'enabled'
|
|
602
664
|
? makeCheck('routing-mode', 'Routing mode', 'pass', `Global routing mode is ${startup.mode}`)
|
package/commands/init.mjs
CHANGED
|
@@ -78,6 +78,19 @@ function writeHookManifestBestEffort(manifest, context) {
|
|
|
78
78
|
}
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
+
function installAwUsageHooksBestEffort({ silent = false } = {}) {
|
|
82
|
+
ensureTelemetryConfig();
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
if (!isDefaultRoutingEnabled(HOME, process.env)) return null;
|
|
86
|
+
const result = installAwUsageHooks();
|
|
87
|
+
return formatAwUsageHooksInstallReport(result);
|
|
88
|
+
} catch (e) {
|
|
89
|
+
if (!silent) fmt.note(`aw-usage hooks install: ${e.message}`, 'Telemetry');
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
81
94
|
function formatIntegrationStatusSummary(statuses, installedNow = []) {
|
|
82
95
|
if (!statuses || statuses.length === 0) return null;
|
|
83
96
|
|
|
@@ -512,6 +525,8 @@ export async function initCommand(args) {
|
|
|
512
525
|
if (cwd !== HOME) installLocalCommitHook(cwd);
|
|
513
526
|
if (!silent) fmt.logStep(`IDE wired — ${chalk.bold(symlinks)} symlinks · ${chalk.bold(commands)} commands`);
|
|
514
527
|
|
|
528
|
+
const awUsageHooksReport = installAwUsageHooksBestEffort({ silent });
|
|
529
|
+
|
|
515
530
|
// Write hook manifest after all hook installation is complete
|
|
516
531
|
writeHookManifestBestEffort({ eccVersion: AW_ECC_TAG, awVersion: VERSION });
|
|
517
532
|
|
|
@@ -539,6 +554,7 @@ export async function initCommand(args) {
|
|
|
539
554
|
'',
|
|
540
555
|
` ${chalk.green('✓')} Registry synced`,
|
|
541
556
|
` ${chalk.green('✓')} IDE refreshed — ${chalk.bold(symlinks)} symlinks · ${chalk.bold(commands)} commands`,
|
|
557
|
+
awUsageHooksReport ? ` ${chalk.green('✓')} ${awUsageHooksReport}` : null,
|
|
542
558
|
removedLegacyStartupFiles.length > 0
|
|
543
559
|
? ` ${chalk.green('✓')} Removed ${removedLegacyStartupFiles.length} legacy repo startup file${removedLegacyStartupFiles.length > 1 ? 's' : ''}`
|
|
544
560
|
: null,
|
|
@@ -686,25 +702,12 @@ export async function initCommand(args) {
|
|
|
686
702
|
];
|
|
687
703
|
if (!silent) fmt.logStep(`IDE wired — ${chalk.bold(symlinks)} symlinks · ${chalk.bold(commands)} commands`);
|
|
688
704
|
|
|
689
|
-
// Ensure telemetry config exists (generates machine_id on first run)
|
|
690
|
-
ensureTelemetryConfig();
|
|
691
|
-
|
|
692
705
|
// Install bundled aw-usage producer hooks into ~/.claude.
|
|
693
706
|
// Copies the scripts + lib files and non-destructively merges 5 hook phases
|
|
694
707
|
// (SessionStart, UserPromptSubmit, PostToolUse, PostToolUseFailure, Stop)
|
|
695
708
|
// into ~/.claude/settings.json. Idempotent. After this, every Claude Code
|
|
696
709
|
// session emits usage_events to the live telemetry API automatically.
|
|
697
|
-
|
|
698
|
-
try {
|
|
699
|
-
if (isDefaultRoutingEnabled(HOME, process.env)) {
|
|
700
|
-
const result = installAwUsageHooks();
|
|
701
|
-
awUsageHooksReport = formatAwUsageHooksInstallReport(result);
|
|
702
|
-
}
|
|
703
|
-
} catch (e) {
|
|
704
|
-
// Non-fatal — telemetry is observational. Surface the error in silent mode
|
|
705
|
-
// logs but don't block the install.
|
|
706
|
-
if (!silent) fmt.note(`aw-usage hooks install: ${e.message}`, 'Telemetry');
|
|
707
|
-
}
|
|
710
|
+
const awUsageHooksReport = installAwUsageHooksBestEffort({ silent });
|
|
708
711
|
|
|
709
712
|
// Write hook manifest after all hook installation is complete, including
|
|
710
713
|
// bundled usage hooks, so `aw nuke` can prune AW-managed settings entries.
|
package/commands/nuke.mjs
CHANGED
|
@@ -14,6 +14,7 @@ import { chalk } from '../fmt.mjs';
|
|
|
14
14
|
import { removeGlobalHooks } from '../hooks.mjs';
|
|
15
15
|
import { uninstallAwEcc } from '../ecc.mjs';
|
|
16
16
|
import { removeMcpConfig } from '../mcp.mjs';
|
|
17
|
+
import { purgeManagedClaudeStartupHooks, disableCursorAwRouting } from '../startup.mjs';
|
|
17
18
|
import { removeContextModeIntegration } from '../integrations/context-mode.mjs';
|
|
18
19
|
import { listProjectWorktrees } from '../git.mjs';
|
|
19
20
|
import { removeWorkspaceHookDefaults } from '../codex.mjs';
|
|
@@ -245,6 +246,11 @@ export async function nukeCommand(args) {
|
|
|
245
246
|
const createdCount = removeCreatedFiles(manifest);
|
|
246
247
|
const ideCount = removeIdeSymlinks();
|
|
247
248
|
uninstallAwEcc();
|
|
249
|
+
// Strip AW-managed startup hooks that enable installs into the user's settings
|
|
250
|
+
// (Claude SessionStart router + UserPromptSubmit reminder, Cursor AW session hook).
|
|
251
|
+
// uninstallAwEcc deliberately preserves user settings files, so these are removed here.
|
|
252
|
+
purgeManagedClaudeStartupHooks(HOME);
|
|
253
|
+
disableCursorAwRouting(HOME);
|
|
248
254
|
removeMcpConfig();
|
|
249
255
|
removeContextModeIntegration(HOME);
|
|
250
256
|
// Prune stale hook entries using manifest (catches entries not tracked by install-state)
|
package/commands/push.mjs
CHANGED
|
@@ -201,6 +201,18 @@ function awDocsFeatureScope(featureSlug) {
|
|
|
201
201
|
if (!slug || slug === 'true') {
|
|
202
202
|
throw new Error('Missing feature slug. Use: aw push --aw-docs-only --feature <feature-slug>');
|
|
203
203
|
}
|
|
204
|
+
if (slug.includes('/')) {
|
|
205
|
+
const stripped = normalizeRelPath(slug).replace(/^\.aw_docs\//, '');
|
|
206
|
+
if (AW_DOCS_ROOT_NAMESPACES.includes(stripped.split('/')[0])) {
|
|
207
|
+
// Root-namespace trees (e.g. pr-reviews/<repo>/pr-<n>/<run>) are valid —
|
|
208
|
+
// they are just passed positionally, not via --feature.
|
|
209
|
+
throw new Error(
|
|
210
|
+
`--feature takes a flat slug, but "${slug}" is a nested ${stripped.split('/')[0]} path. ` +
|
|
211
|
+
`Pass it positionally instead: aw push --aw-docs-only .aw_docs/${stripped}`
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
throw new Error(`Invalid feature slug "${slug}". Feature slugs may not contain "/". For nested doc trees use a root namespace path: aw push --aw-docs-only .aw_docs/pr-reviews/<repo>/pr-<n>/<run>`);
|
|
215
|
+
}
|
|
204
216
|
if (!/^[A-Za-z0-9._-]+$/.test(slug)) {
|
|
205
217
|
throw new Error(`Invalid feature slug "${slug}". Feature slugs may contain letters, numbers, dot, underscore, and dash only.`);
|
|
206
218
|
}
|
|
@@ -1138,6 +1150,11 @@ function resolveColocatedEvals(registrySubDir, namespace, parentType, slug, chan
|
|
|
1138
1150
|
// ── CODEOWNERS helpers ────────────────────────────────────────────────
|
|
1139
1151
|
|
|
1140
1152
|
async function getGitHubUser() {
|
|
1153
|
+
// GitHub App installation tokens cannot call `gh api user` (403), which made
|
|
1154
|
+
// headless publishes namespace docs under "unknown". Allow CI to set the
|
|
1155
|
+
// publish identity explicitly.
|
|
1156
|
+
const override = (process.env.AW_DOCS_PUBLISH_USER || '').trim();
|
|
1157
|
+
if (override) return override;
|
|
1141
1158
|
try {
|
|
1142
1159
|
const { stdout } = await exec('gh api user --jq .login');
|
|
1143
1160
|
return stdout.trim();
|
|
@@ -1669,4 +1686,5 @@ export const __test__ = {
|
|
|
1669
1686
|
awDocsRootScope,
|
|
1670
1687
|
resolveAwDocsScope,
|
|
1671
1688
|
assertDocsOnlyScopeOrAll,
|
|
1689
|
+
getGitHubUser,
|
|
1672
1690
|
};
|
package/commands/startup.mjs
CHANGED
|
@@ -73,13 +73,15 @@ function renderStatus() {
|
|
|
73
73
|
: 'disabled';
|
|
74
74
|
const claudeStatus = status.claudeDisabled
|
|
75
75
|
? 'disabled via ~/.claude/settings.json override'
|
|
76
|
-
: status.
|
|
77
|
-
? 'enabled (
|
|
78
|
-
: status.
|
|
79
|
-
? 'enabled (
|
|
80
|
-
: status.claudePluginEnabled &&
|
|
81
|
-
? '
|
|
82
|
-
:
|
|
76
|
+
: status.claudeRouterPresent
|
|
77
|
+
? 'enabled (managed router)'
|
|
78
|
+
: status.claudeLegacySessionStartPresent
|
|
79
|
+
? 'enabled (legacy hook present; run aw routing enable to clean)'
|
|
80
|
+
: status.claudePluginEnabled && status.claudePluginInstalled
|
|
81
|
+
? 'enabled (plugin installed)'
|
|
82
|
+
: status.claudePluginEnabled && !status.claudePluginInstalled
|
|
83
|
+
? 'misconfigured (plugin enabled in settings but not installed)'
|
|
84
|
+
: 'disabled (plugin not installed)';
|
|
83
85
|
const cursorStatus = status.cursorSessionStartPresent
|
|
84
86
|
? 'enabled'
|
|
85
87
|
: status.cursorAnySessionStartPresent
|
package/ecc.mjs
CHANGED
|
@@ -12,7 +12,7 @@ import { applyStoredStartupPreferences } from "./startup.mjs";
|
|
|
12
12
|
|
|
13
13
|
const AW_ECC_REPO_SSH = "git@github.com:shreyansh-ghl/aw-ecc.git";
|
|
14
14
|
const AW_ECC_REPO_HTTPS = "https://github.com/shreyansh-ghl/aw-ecc.git";
|
|
15
|
-
export const AW_ECC_TAG = "v1.4.
|
|
15
|
+
export const AW_ECC_TAG = "v1.4.67";
|
|
16
16
|
const REQUIRED_ECC_FILES = [
|
|
17
17
|
"package.json",
|
|
18
18
|
"scripts/install-apply.js",
|
|
@@ -11,14 +11,23 @@
|
|
|
11
11
|
|
|
12
12
|
'use strict';
|
|
13
13
|
|
|
14
|
-
const {
|
|
14
|
+
const {
|
|
15
|
+
buildEvent,
|
|
16
|
+
sendAsync,
|
|
17
|
+
isDisabled,
|
|
18
|
+
findRecentSdlcSessionForProject,
|
|
19
|
+
} = require('../lib/aw-usage-telemetry');
|
|
15
20
|
|
|
16
21
|
function buildCommitCreatedEvent({ commitHash = 'unknown', branch = 'unknown', cwd = process.cwd() } = {}) {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
22
|
+
const linkedSession = findRecentSdlcSessionForProject(cwd);
|
|
23
|
+
const event = buildEvent({
|
|
24
|
+
cwd,
|
|
25
|
+
...(linkedSession?.session_id ? { session_id: linkedSession.session_id } : {}),
|
|
26
|
+
}, 'commit_created', {
|
|
20
27
|
commit_hash: commitHash,
|
|
28
|
+
commit_sha: commitHash,
|
|
21
29
|
branch,
|
|
30
|
+
...(linkedSession?.session_id ? { linked_session_source: 'project_recent_sdlc_session' } : {}),
|
|
22
31
|
});
|
|
23
32
|
|
|
24
33
|
// Override harness to 'git' since this fires from a git hook, not a harness.
|
|
@@ -112,6 +112,44 @@ function getCommand(input) {
|
|
|
112
112
|
);
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
+
function commandSegments(command) {
|
|
116
|
+
return String(command || '')
|
|
117
|
+
.split(/\n|&&|\|\||;/)
|
|
118
|
+
.map(segment => segment.trim())
|
|
119
|
+
.filter(Boolean)
|
|
120
|
+
.map(segment => segment.replace(/^(?:[A-Za-z_][A-Za-z0-9_]*=(?:"[^"]*"|'[^']*'|\S+)\s+)*/, ''))
|
|
121
|
+
.filter(segment => !segment.startsWith('#'));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function detectDeployCommand(command) {
|
|
125
|
+
const patterns = [
|
|
126
|
+
{ regex: /^(?:npx\s+)?aw\s+deploy\b/i, tool: 'aw', provider: 'aw' },
|
|
127
|
+
{ regex: /^(?:npm|yarn|pnpm|bun)\s+(?:run\s+)?deploy(?::[a-z0-9_-]+)?\b/i, tool: 'package-script', provider: 'package-script' },
|
|
128
|
+
{ regex: /^gcloud\s+run\s+deploy\b/i, tool: 'gcloud', provider: 'cloud-run' },
|
|
129
|
+
{ regex: /^firebase\s+deploy\b/i, tool: 'firebase', provider: 'firebase' },
|
|
130
|
+
{ regex: /^vercel\s+(?:deploy\b|--prod\b)/i, tool: 'vercel', provider: 'vercel' },
|
|
131
|
+
{ regex: /^wrangler\s+(?:deploy|pages\s+deploy)\b/i, tool: 'wrangler', provider: 'cloudflare' },
|
|
132
|
+
{ regex: /^helm\s+upgrade\b/i, tool: 'helm', provider: 'kubernetes' },
|
|
133
|
+
{ regex: /^kubectl\s+(?:rollout\s+restart|set\s+image)\b/i, tool: 'kubectl', provider: 'kubernetes' },
|
|
134
|
+
{ regex: /^gh\s+workflow\s+run\b.*\b(?:deploy|release|staging|production|prod)\b/i, tool: 'gh', provider: 'github-actions' },
|
|
135
|
+
{ regex: /^aws\s+(?:ecs\s+update-service|lambda\s+update-function-code)\b/i, tool: 'aws', provider: 'aws' },
|
|
136
|
+
{ regex: /^(?:sam|serverless|sls)\s+deploy\b/i, tool: 'serverless', provider: 'serverless' },
|
|
137
|
+
];
|
|
138
|
+
|
|
139
|
+
for (const segment of commandSegments(command)) {
|
|
140
|
+
for (const pattern of patterns) {
|
|
141
|
+
if (pattern.regex.test(segment)) {
|
|
142
|
+
return {
|
|
143
|
+
deploy_tool: pattern.tool,
|
|
144
|
+
deploy_provider: pattern.provider,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
|
|
115
153
|
function normalizeToolResult(input) {
|
|
116
154
|
const rawToolResponse = parseMaybeJsonObject(input?.tool_response);
|
|
117
155
|
const rawToolOutput = parseMaybeJsonObject(input?.tool_output);
|
|
@@ -232,6 +270,32 @@ function collectPostToolUseEvents(input, options = {}) {
|
|
|
232
270
|
|
|
233
271
|
if (toolName === 'Shell' || toolName === 'Bash') {
|
|
234
272
|
const cmd = getCommand(input);
|
|
273
|
+
const deploy = detectDeployCommand(cmd);
|
|
274
|
+
if (deploy) {
|
|
275
|
+
const deployPayload = {
|
|
276
|
+
...deploy,
|
|
277
|
+
tool_name: toolName,
|
|
278
|
+
sdlc_correlated_command: slashCmd ? slashCmd.command_name : null,
|
|
279
|
+
sdlc_correlated_namespace: slashCmd ? slashCmd.command_namespace : null,
|
|
280
|
+
sdlc_correlated_is_sdlc_stage: slashCmd ? Boolean(slashCmd.is_sdlc_stage) : false,
|
|
281
|
+
};
|
|
282
|
+
const failed = isExplicitFailureExitCode(toolResult.exitCode)
|
|
283
|
+
|| inferShellFailureFromMessage(toolName, toolResult.rawErrorMessage);
|
|
284
|
+
|
|
285
|
+
events.push({
|
|
286
|
+
eventType: 'deploy_triggered',
|
|
287
|
+
payload: deployPayload,
|
|
288
|
+
});
|
|
289
|
+
events.push({
|
|
290
|
+
eventType: failed ? 'deploy_failed' : 'deploy_success',
|
|
291
|
+
payload: {
|
|
292
|
+
...deployPayload,
|
|
293
|
+
status: failed ? 'failed' : 'success',
|
|
294
|
+
...(isExplicitFailureExitCode(toolResult.exitCode) ? { exit_code: toolResult.exitCode } : {}),
|
|
295
|
+
},
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
|
|
235
299
|
// Codex skill detection: Bash commands that read SKILL.md files.
|
|
236
300
|
if (!promptSkillOverride) {
|
|
237
301
|
const skillCmdMatch = cmd.match(/\/skills\/([^/]+)\/SKILL\.md/i);
|
|
@@ -362,6 +426,7 @@ if (require.main === module) {
|
|
|
362
426
|
|
|
363
427
|
module.exports = {
|
|
364
428
|
collectPostToolUseEvents,
|
|
429
|
+
detectDeployCommand,
|
|
365
430
|
detectSdlcArtifact,
|
|
366
431
|
detectTestFramework,
|
|
367
432
|
inferShellFailureFromMessage,
|
|
@@ -109,7 +109,9 @@ function processPromptSubmitInput(input, deps = {}) {
|
|
|
109
109
|
|
|
110
110
|
if (slash) {
|
|
111
111
|
persistSkill(getSessionId(input), input?.turn_id || null, slash);
|
|
112
|
-
persistSlashCmd(getSessionId(input), slash
|
|
112
|
+
persistSlashCmd(getSessionId(input), slash, {
|
|
113
|
+
cwd: input?.cwd || (input?.workspace_roots && input.workspace_roots[0]) || null,
|
|
114
|
+
});
|
|
113
115
|
events.push({
|
|
114
116
|
eventType: 'skill_invoked',
|
|
115
117
|
payload: {
|
|
@@ -21,6 +21,7 @@ const SENDER_SCRIPT = path.join(__dirname, '..', 'hooks', 'aw-usage-telemetry-se
|
|
|
21
21
|
const AW_HOME = path.join(os.homedir(), '.aw');
|
|
22
22
|
const CONFIG_PATH = path.join(AW_HOME, 'telemetry', 'config.json');
|
|
23
23
|
const SESSION_DIR = path.join(AW_HOME, 'telemetry', 'sessions');
|
|
24
|
+
const SESSION_PROJECT_DIR = path.join(SESSION_DIR, 'by-project');
|
|
24
25
|
const DEDUPE_DIR = path.join(os.tmpdir(), 'aw-usage-telemetry-dedupe');
|
|
25
26
|
|
|
26
27
|
// ── Git config cache (once per process) ──────────────────────────────
|
|
@@ -157,6 +158,16 @@ function computeProjectHash(cwd) {
|
|
|
157
158
|
return crypto.createHash('sha256').update(cwd).digest('hex').slice(0, 16);
|
|
158
159
|
}
|
|
159
160
|
|
|
161
|
+
function normalizeProjectHash(value) {
|
|
162
|
+
return typeof value === 'string' && /^[a-f0-9]{16}$/i.test(value) ? value.toLowerCase() : null;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function resolveProjectHash(context) {
|
|
166
|
+
if (!context || typeof context !== 'object') return null;
|
|
167
|
+
return normalizeProjectHash(context.project_hash || context.projectHash)
|
|
168
|
+
|| computeProjectHash(context.cwd);
|
|
169
|
+
}
|
|
170
|
+
|
|
160
171
|
// ── Session file cleanup ─────────────────────────────────────────────
|
|
161
172
|
// Prune session files older than SESSION_MAX_AGE_MS to prevent unbounded growth.
|
|
162
173
|
// Called once per session start — best-effort, never blocks.
|
|
@@ -241,21 +252,40 @@ function readSessionSkill(sessionId, turnId) {
|
|
|
241
252
|
// to the originating /aw:* invocation. Separate from `last_skill` because
|
|
242
253
|
// the source is the prompt, not the tool, and the lifetime is the whole
|
|
243
254
|
// session (not just one turn).
|
|
244
|
-
function
|
|
255
|
+
function writeProjectSessionIndex(projectHash, sessionId, slashCommand) {
|
|
256
|
+
if (!projectHash || !sessionId || !slashCommand?.is_sdlc_stage) return;
|
|
257
|
+
try {
|
|
258
|
+
fs.mkdirSync(SESSION_PROJECT_DIR, { recursive: true });
|
|
259
|
+
fs.writeFileSync(path.join(SESSION_PROJECT_DIR, `${projectHash}.json`), JSON.stringify({
|
|
260
|
+
project_hash: projectHash,
|
|
261
|
+
session_id: sessionId,
|
|
262
|
+
command_namespace: slashCommand.command_namespace || null,
|
|
263
|
+
command_name: slashCommand.command_name,
|
|
264
|
+
is_sdlc_stage: Boolean(slashCommand.is_sdlc_stage),
|
|
265
|
+
updated_at: new Date().toISOString(),
|
|
266
|
+
}));
|
|
267
|
+
} catch { /* ignore */ }
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function persistSessionSlashCommand(sessionId, slashCommand, context = {}) {
|
|
245
271
|
if (!sessionId || !slashCommand?.command_name) return;
|
|
246
272
|
try {
|
|
247
273
|
fs.mkdirSync(SESSION_DIR, { recursive: true });
|
|
248
274
|
const state = readSessionState(sessionId);
|
|
275
|
+
const projectHash = resolveProjectHash(context) || state.project_hash || null;
|
|
249
276
|
fs.writeFileSync(path.join(SESSION_DIR, sessionId + '.json'), JSON.stringify({
|
|
250
277
|
...state,
|
|
278
|
+
...(projectHash ? { project_hash: projectHash } : {}),
|
|
251
279
|
last_slash_command: {
|
|
252
280
|
command_namespace: slashCommand.command_namespace || null,
|
|
253
281
|
command_name: slashCommand.command_name,
|
|
254
282
|
command_args: slashCommand.command_args || '',
|
|
255
283
|
is_sdlc_stage: Boolean(slashCommand.is_sdlc_stage),
|
|
284
|
+
...(projectHash ? { project_hash: projectHash } : {}),
|
|
256
285
|
updated_at: new Date().toISOString(),
|
|
257
286
|
},
|
|
258
287
|
}));
|
|
288
|
+
writeProjectSessionIndex(projectHash, sessionId, slashCommand);
|
|
259
289
|
} catch { /* ignore */ }
|
|
260
290
|
}
|
|
261
291
|
|
|
@@ -265,6 +295,59 @@ function readSessionLastSlashCommand(sessionId) {
|
|
|
265
295
|
return cmd;
|
|
266
296
|
}
|
|
267
297
|
|
|
298
|
+
function parseUpdatedAt(value) {
|
|
299
|
+
const millis = Date.parse(value || '');
|
|
300
|
+
return Number.isFinite(millis) ? millis : 0;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function isRecentSdlcSessionCandidate(candidate, projectHash, maxAgeMs) {
|
|
304
|
+
if (!candidate?.session_id) return false;
|
|
305
|
+
if (!candidate?.is_sdlc_stage) return false;
|
|
306
|
+
if (candidate.project_hash !== projectHash) return false;
|
|
307
|
+
const updatedAt = parseUpdatedAt(candidate.updated_at);
|
|
308
|
+
if (!updatedAt) return false;
|
|
309
|
+
return Date.now() - updatedAt <= maxAgeMs;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function readProjectSessionIndex(projectHash, maxAgeMs) {
|
|
313
|
+
try {
|
|
314
|
+
const candidate = JSON.parse(fs.readFileSync(path.join(SESSION_PROJECT_DIR, `${projectHash}.json`), 'utf8'));
|
|
315
|
+
return isRecentSdlcSessionCandidate(candidate, projectHash, maxAgeMs) ? candidate : null;
|
|
316
|
+
} catch {
|
|
317
|
+
return null;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function findRecentSdlcSessionForProject(cwd, maxAgeMs = SESSION_MAX_AGE_MS) {
|
|
322
|
+
const projectHash = computeProjectHash(cwd);
|
|
323
|
+
if (!projectHash) return null;
|
|
324
|
+
|
|
325
|
+
const indexed = readProjectSessionIndex(projectHash, maxAgeMs);
|
|
326
|
+
if (indexed) return indexed;
|
|
327
|
+
|
|
328
|
+
let best = null;
|
|
329
|
+
try {
|
|
330
|
+
const entries = fs.readdirSync(SESSION_DIR);
|
|
331
|
+
for (const entry of entries) {
|
|
332
|
+
if (!entry.endsWith('.json')) continue;
|
|
333
|
+
const sessionId = entry.slice(0, -'.json'.length);
|
|
334
|
+
const state = readSessionState(sessionId);
|
|
335
|
+
const cmd = state.last_slash_command;
|
|
336
|
+
const candidate = {
|
|
337
|
+
session_id: sessionId,
|
|
338
|
+
project_hash: cmd?.project_hash || state.project_hash || null,
|
|
339
|
+
is_sdlc_stage: Boolean(cmd?.is_sdlc_stage),
|
|
340
|
+
updated_at: cmd?.updated_at || state.updated_at || null,
|
|
341
|
+
};
|
|
342
|
+
if (!isRecentSdlcSessionCandidate(candidate, projectHash, maxAgeMs)) continue;
|
|
343
|
+
if (!best || parseUpdatedAt(candidate.updated_at) > parseUpdatedAt(best.updated_at)) {
|
|
344
|
+
best = candidate;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
} catch { /* ignore */ }
|
|
348
|
+
return best;
|
|
349
|
+
}
|
|
350
|
+
|
|
268
351
|
// ── Short-TTL dedupe guards ──────────────────────────────────────────
|
|
269
352
|
|
|
270
353
|
function normalizeDedupePart(value) {
|
|
@@ -503,6 +586,7 @@ module.exports = {
|
|
|
503
586
|
readSessionSkill,
|
|
504
587
|
persistSessionSlashCommand,
|
|
505
588
|
readSessionLastSlashCommand,
|
|
589
|
+
findRecentSdlcSessionForProject,
|
|
506
590
|
readLastAssistantFromTranscript,
|
|
507
591
|
resolvePromptText,
|
|
508
592
|
getAwCliVersionDetails,
|
package/hooks.mjs
CHANGED
|
@@ -102,7 +102,7 @@ if git log -1 --format='%b' HEAD 2>/dev/null | grep -qF "Co-Authored-By: AW"; th
|
|
|
102
102
|
TELEMETRY_HOOK="$HOME/.aw-ecc/scripts/hooks/aw-usage-commit-created.js"
|
|
103
103
|
fi
|
|
104
104
|
if command -v node >/dev/null 2>&1 && [ -f "$TELEMETRY_HOOK" ]; then
|
|
105
|
-
COMMIT_HASH="$(git rev-parse
|
|
105
|
+
COMMIT_HASH="$(git rev-parse HEAD 2>/dev/null || echo unknown)"
|
|
106
106
|
BRANCH="$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo unknown)"
|
|
107
107
|
node "$TELEMETRY_HOOK" "$COMMIT_HASH" "$BRANCH" "$(pwd)" >/dev/null 2>&1
|
|
108
108
|
fi
|
|
@@ -286,7 +286,7 @@ if git log -1 --format='%b' HEAD 2>/dev/null | grep -qF "Co-Authored-By: AW"; th
|
|
|
286
286
|
TELEMETRY_HOOK="$HOME/.aw-ecc/scripts/hooks/aw-usage-commit-created.js"
|
|
287
287
|
fi
|
|
288
288
|
if command -v node >/dev/null 2>&1 && [ -f "$TELEMETRY_HOOK" ]; then
|
|
289
|
-
COMMIT_HASH="$(git rev-parse
|
|
289
|
+
COMMIT_HASH="$(git rev-parse HEAD 2>/dev/null || echo unknown)"
|
|
290
290
|
BRANCH="$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo unknown)"
|
|
291
291
|
node "$TELEMETRY_HOOK" "$COMMIT_HASH" "$BRANCH" "$(pwd)" >/dev/null 2>&1
|
|
292
292
|
fi
|
package/link.mjs
CHANGED
|
@@ -1,19 +1,41 @@
|
|
|
1
1
|
// link.mjs — Create symlinks from IDE dirs → .aw_registry/
|
|
2
2
|
|
|
3
|
-
import { existsSync, lstatSync, mkdirSync, readdirSync, unlinkSync, symlinkSync, rmdirSync, realpathSync } from 'node:fs';
|
|
3
|
+
import { existsSync, lstatSync, mkdirSync, readdirSync, unlinkSync, symlinkSync, rmSync, rmdirSync, realpathSync } from 'node:fs';
|
|
4
4
|
import { join, relative } from 'node:path';
|
|
5
5
|
import { homedir } from 'node:os';
|
|
6
6
|
import * as fmt from './fmt.mjs';
|
|
7
7
|
import { getLocalRegistryDir } from './git.mjs';
|
|
8
8
|
|
|
9
|
-
function forceSymlink(target, linkPath) {
|
|
10
|
-
try {
|
|
9
|
+
function forceSymlink(target, linkPath, { replaceManagedDir = false } = {}) {
|
|
10
|
+
try {
|
|
11
|
+
unlinkSync(linkPath);
|
|
12
|
+
} catch {
|
|
13
|
+
// unlinkSync does not remove a real directory. A stale copied dir from an older
|
|
14
|
+
// install (e.g. an ECC copy, or a previous copy-not-symlink install) can collide
|
|
15
|
+
// with the canonical symlink name — clear it so the registry symlink can take the
|
|
16
|
+
// name. Only do this for AW-managed catalog links, scoped by replaceManagedDir:
|
|
17
|
+
// 'skill' → only when the dir is itself a skill (has SKILL.md), so an unrelated
|
|
18
|
+
// non-skill directory still surfaces the EEXIST error to the caller
|
|
19
|
+
// instead of being silently deleted (no user-data loss).
|
|
20
|
+
// true → managed catalog dirs (evals/docs) that AW fully owns; a stale real
|
|
21
|
+
// copy must be replaced or the canonical link is never refreshed.
|
|
22
|
+
try {
|
|
23
|
+
if (existsSync(linkPath) && lstatSync(linkPath).isDirectory()) {
|
|
24
|
+
const shouldReplace = replaceManagedDir === 'skill'
|
|
25
|
+
? existsSync(join(linkPath, 'SKILL.md'))
|
|
26
|
+
: replaceManagedDir === true;
|
|
27
|
+
if (shouldReplace) {
|
|
28
|
+
rmSync(linkPath, { recursive: true, force: true });
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
} catch { /* best effort */ }
|
|
32
|
+
}
|
|
11
33
|
symlinkSync(target, linkPath);
|
|
12
34
|
}
|
|
13
35
|
|
|
14
36
|
function linkNestedSkillRoot(target, linkPath, { silent }) {
|
|
15
37
|
try {
|
|
16
|
-
forceSymlink(target, linkPath);
|
|
38
|
+
forceSymlink(target, linkPath, { replaceManagedDir: 'skill' });
|
|
17
39
|
return true;
|
|
18
40
|
} catch (error) {
|
|
19
41
|
if (!silent) {
|
|
@@ -161,6 +183,9 @@ function cleanSymlinksRecursive(dir) {
|
|
|
161
183
|
* Compute flat IDE name: namespace-slug (always includes namespace).
|
|
162
184
|
*/
|
|
163
185
|
function flatName(ns, name) {
|
|
186
|
+
// Avoid a redundant namespace prefix when the skill/agent name already begins with
|
|
187
|
+
// the namespace (e.g. ns=aw, name=aw-plan -> aw-plan, not aw-aw-plan).
|
|
188
|
+
if (name === ns || name.startsWith(`${ns}-`)) return name;
|
|
164
189
|
return `${ns}-${name}`;
|
|
165
190
|
}
|
|
166
191
|
|
|
@@ -219,7 +244,7 @@ export function linkWorkspace(cwd, awDirOverride = null, { silent = false } = {}
|
|
|
219
244
|
// Skills: per-skill directory symlinks (recursive for nested domain dirs)
|
|
220
245
|
for (const { typeDirPath: skillsDir, segments } of findNestedTypeDirs(join(awDir, ns), 'skills')) {
|
|
221
246
|
for (const skill of listDirs(skillsDir)) {
|
|
222
|
-
const flat =
|
|
247
|
+
const flat = flatName(ns, [...segments, skill].join('-'));
|
|
223
248
|
|
|
224
249
|
for (const ide of IDE_DIRS) {
|
|
225
250
|
const linkDir = join(cwd, ide, 'skills');
|
|
@@ -227,13 +252,13 @@ export function linkWorkspace(cwd, awDirOverride = null, { silent = false } = {}
|
|
|
227
252
|
const linkPath = join(linkDir, flat);
|
|
228
253
|
const targetPath = join(skillsDir, skill);
|
|
229
254
|
const relTarget = relative(linkDir, targetPath);
|
|
230
|
-
try { forceSymlink(relTarget, linkPath); created++; } catch { /* best effort */ }
|
|
255
|
+
try { forceSymlink(relTarget, linkPath, { replaceManagedDir: 'skill' }); created++; } catch { /* best effort */ }
|
|
231
256
|
}
|
|
232
257
|
}
|
|
233
258
|
|
|
234
259
|
for (const { skillDirPath, segments: skillSegments } of findSkillRootDirs(skillsDir)) {
|
|
235
260
|
if (skillSegments.length <= 1) continue;
|
|
236
|
-
const flat =
|
|
261
|
+
const flat = flatName(ns, [...segments, ...skillSegments].join('-'));
|
|
237
262
|
|
|
238
263
|
for (const ide of IDE_DIRS) {
|
|
239
264
|
const linkDir = join(cwd, ide, 'skills');
|
|
@@ -261,7 +286,7 @@ export function linkWorkspace(cwd, awDirOverride = null, { silent = false } = {}
|
|
|
261
286
|
const linkPath = join(linkDir, flat);
|
|
262
287
|
const targetPath = join(evalsDir, evalSlug);
|
|
263
288
|
const relTarget = relative(linkDir, targetPath);
|
|
264
|
-
try { forceSymlink(relTarget, linkPath); created++; } catch { /* best effort */ }
|
|
289
|
+
try { forceSymlink(relTarget, linkPath, { replaceManagedDir: true }); created++; } catch { /* best effort */ }
|
|
265
290
|
}
|
|
266
291
|
}
|
|
267
292
|
} else {
|
|
@@ -276,7 +301,7 @@ export function linkWorkspace(cwd, awDirOverride = null, { silent = false } = {}
|
|
|
276
301
|
const linkPath = join(linkDir, flat);
|
|
277
302
|
const targetPath = join(subDir, evalName);
|
|
278
303
|
const relTarget = relative(linkDir, targetPath);
|
|
279
|
-
try { forceSymlink(relTarget, linkPath); created++; } catch { /* best effort */ }
|
|
304
|
+
try { forceSymlink(relTarget, linkPath, { replaceManagedDir: true }); created++; } catch { /* best effort */ }
|
|
280
305
|
}
|
|
281
306
|
}
|
|
282
307
|
}
|
|
@@ -310,16 +335,16 @@ export function linkWorkspace(cwd, awDirOverride = null, { silent = false } = {}
|
|
|
310
335
|
for (const { typeDirPath: skillsDir, segments } of findNestedTypeDirs(join(awDir, ns), 'skills')) {
|
|
311
336
|
mkdirSync(agentsSkillsDir, { recursive: true });
|
|
312
337
|
for (const skill of listDirs(skillsDir)) {
|
|
313
|
-
const flat =
|
|
338
|
+
const flat = flatName(ns, [...segments, skill].join('-'));
|
|
314
339
|
const linkPath = join(agentsSkillsDir, flat);
|
|
315
340
|
const targetPath = join(skillsDir, skill);
|
|
316
341
|
const relTarget = relative(agentsSkillsDir, targetPath);
|
|
317
|
-
try { forceSymlink(relTarget, linkPath); created++; } catch { /* best effort */ }
|
|
342
|
+
try { forceSymlink(relTarget, linkPath, { replaceManagedDir: 'skill' }); created++; } catch { /* best effort */ }
|
|
318
343
|
}
|
|
319
344
|
|
|
320
345
|
for (const { skillDirPath, segments: skillSegments } of findSkillRootDirs(skillsDir)) {
|
|
321
346
|
if (skillSegments.length <= 1) continue;
|
|
322
|
-
const flat =
|
|
347
|
+
const flat = flatName(ns, [...segments, ...skillSegments].join('-'));
|
|
323
348
|
const linkPath = join(agentsSkillsDir, flat);
|
|
324
349
|
const relTarget = relative(agentsSkillsDir, skillDirPath);
|
|
325
350
|
if (linkNestedSkillRoot(relTarget, linkPath, { silent })) created++;
|
|
@@ -378,7 +403,7 @@ export function linkWorkspace(cwd, awDirOverride = null, { silent = false } = {}
|
|
|
378
403
|
mkdirSync(linkDir, { recursive: true });
|
|
379
404
|
const linkPath = join(linkDir, 'docs');
|
|
380
405
|
const relTarget = relative(linkDir, platformDocsDir);
|
|
381
|
-
try { forceSymlink(relTarget, linkPath); created++; } catch { /* best effort */ }
|
|
406
|
+
try { forceSymlink(relTarget, linkPath, { replaceManagedDir: true }); created++; } catch { /* best effort */ }
|
|
382
407
|
}
|
|
383
408
|
}
|
|
384
409
|
|
package/mcp.mjs
CHANGED
|
@@ -46,6 +46,10 @@ function envDisablesMcp(env = process.env) {
|
|
|
46
46
|
return ['1', 'true', 'yes', 'on'].includes(value);
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
function hasInteractiveTerminal() {
|
|
50
|
+
return Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
51
|
+
}
|
|
52
|
+
|
|
49
53
|
export function loadMcpPreferences(homeDir = HOME) {
|
|
50
54
|
const prefs = readJson(mcpPrefsPath(homeDir), {});
|
|
51
55
|
return {
|
|
@@ -126,8 +130,8 @@ function resolveGitHubToken(silent = false) {
|
|
|
126
130
|
}
|
|
127
131
|
} catch { /* not authenticated yet */ }
|
|
128
132
|
|
|
129
|
-
// 2b. Not authenticated — skip browser login
|
|
130
|
-
if (silent) return null;
|
|
133
|
+
// 2b. Not authenticated — skip browser login when no terminal is available.
|
|
134
|
+
if (silent || !hasInteractiveTerminal()) return null;
|
|
131
135
|
|
|
132
136
|
fmt.logStep('GitHub CLI found but not authenticated — launching login...');
|
|
133
137
|
try {
|
|
@@ -237,8 +241,13 @@ async function resolveClickUpToken(silent = false, cwd = process.cwd()) {
|
|
|
237
241
|
return existing.token;
|
|
238
242
|
}
|
|
239
243
|
|
|
240
|
-
// In silent mode (git hooks, auto-pull) there is no
|
|
241
|
-
|
|
244
|
+
// In silent/non-TTY mode (git hooks, Jenkins, auto-pull) there is no safe
|
|
245
|
+
// terminal prompt. ClickUp is optional, so skip instead of letting Clack
|
|
246
|
+
// initialize node:tty and crash.
|
|
247
|
+
if (silent || !hasInteractiveTerminal()) {
|
|
248
|
+
if (!silent) fmt.logInfo('Skipping ClickUp — no interactive terminal available');
|
|
249
|
+
return null;
|
|
250
|
+
}
|
|
242
251
|
|
|
243
252
|
// 3. Ask first — don't assume the user wants ClickUp
|
|
244
253
|
const wantsClickUp = await p.select({
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ghl-ai/aw",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "Agentic Workspace CLI
|
|
3
|
+
"version": "0.1.76",
|
|
4
|
+
"description": "Agentic Workspace CLI — pull, push & manage agents, skills and commands from the registry",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"aw": "bin.js"
|
|
@@ -74,4 +74,4 @@
|
|
|
74
74
|
"devDependencies": {
|
|
75
75
|
"vitest": "^4.1.2"
|
|
76
76
|
}
|
|
77
|
-
}
|
|
77
|
+
}
|
package/render-rules.mjs
CHANGED
|
@@ -358,11 +358,11 @@ ${GENERATED_MARKER}
|
|
|
358
358
|
|
|
359
359
|
For every non-trivial request, execute these steps in order before any substantive response:
|
|
360
360
|
|
|
361
|
-
1. **Load the router** — Read
|
|
361
|
+
1. **Load the router** — Read \`.cursor/skills/aw-using-aw-skills/SKILL.md\` from the active workspace first; if it is missing, fall back to \`~/.cursor/skills/aw-using-aw-skills/SKILL.md\`, then \`~/.agents/skills/aw-using-aw-skills/SKILL.md\`.
|
|
362
362
|
|
|
363
363
|
2. **Select route** — using the decision tree below, pick the smallest correct AW route.
|
|
364
364
|
|
|
365
|
-
3. **Read the stage skill** — you MUST Read the matching skill file before responding
|
|
365
|
+
3. **Read the stage skill** — you MUST Read the matching skill file before responding. Skills are in the active workspace \`.cursor/skills/\` first, then \`~/.cursor/skills/\`, then \`~/.agents/skills/\`:
|
|
366
366
|
- /aw-adk → Read aw-adk/SKILL.md
|
|
367
367
|
- /aw-publish → Read aw-publish/SKILL.md
|
|
368
368
|
- /aw-plan → Read aw-plan/SKILL.md
|
package/startup.mjs
CHANGED
|
@@ -16,6 +16,18 @@ const DISABLED_MODE = 'disabled';
|
|
|
16
16
|
const DISABLE_DEFAULT_ROUTING_ENV = 'AW_DISABLE_DEFAULT_ROUTING';
|
|
17
17
|
|
|
18
18
|
const CLAUDE_DISABLE_DESCRIPTION = 'AW-managed override: disable automatic AW session routing';
|
|
19
|
+
|
|
20
|
+
// using-aw-skills router hooks. The SessionStart hook injects the AW routing card once
|
|
21
|
+
// per session; the UserPromptSubmit hook re-asserts it on every prompt (the stronger
|
|
22
|
+
// always-on auto-trigger). Both were missing after the ecc->registry migration, which is
|
|
23
|
+
// why SDLC skills stopped auto-triggering in Claude. The command resolves the first
|
|
24
|
+
// existing on-disk router script across both registry roots and namespaces.
|
|
25
|
+
const CLAUDE_ROUTER_DESCRIPTION = 'AW-managed: using-aw-skills session router';
|
|
26
|
+
const CLAUDE_REMINDER_DESCRIPTION = 'AW-managed: using-aw-skills per-prompt reminder';
|
|
27
|
+
const CLAUDE_ROUTER_COMMAND =
|
|
28
|
+
'for f in "$HOME/.aw/.aw_registry/aw/skills/using-aw-skills/hooks/session-start.sh" "$HOME/.aw_registry/aw/skills/using-aw-skills/hooks/session-start.sh" "$HOME/.aw/.aw_registry/platform/core/skills/using-aw-skills/hooks/session-start.sh" "$HOME/.aw_registry/platform/core/skills/using-aw-skills/hooks/session-start.sh"; do [ -f "$f" ] && exec bash "$f"; done; exit 0';
|
|
29
|
+
const CLAUDE_REMINDER_COMMAND =
|
|
30
|
+
'echo \'{"hookSpecificOutput":{"hookEventName":"UserPromptSubmit","additionalContext":"[AW] using-aw-skills is active. Select the smallest correct AW route (/aw:plan, /aw:execute, /aw:verify, /aw:deploy, /aw:ship) before substantive work."}}\'';
|
|
19
31
|
const CLAUDE_TELEMETRY_DESCRIPTION = 'AW usage telemetry';
|
|
20
32
|
const CLAUDE_TELEMETRY_HOOKS = [
|
|
21
33
|
{
|
|
@@ -47,6 +59,24 @@ const CLAUDE_TELEMETRY_HOOKS = [
|
|
|
47
59
|
const CURSOR_SESSION_START_COMMAND = 'node .cursor/hooks/session-start.js';
|
|
48
60
|
const CURSOR_SESSION_START_DESCRIPTION = 'Load previous context and detect environment';
|
|
49
61
|
const REPO_CURSOR_SESSION_START_COMMAND = 'bash "$(git rev-parse --show-toplevel)/hooks/aw-session-start"';
|
|
62
|
+
|
|
63
|
+
// Deterministic local-Cursor AW router. Local Cursor previously had no AW session hook
|
|
64
|
+
// (only an advisory .mdc rule the model could under-weight), so SDLC skills did not
|
|
65
|
+
// auto-trigger. This managed sessionStart hook injects the using-aw-skills router via the
|
|
66
|
+
// concise session-start.sh (CURSOR_PLUGIN_ROOT branch emits Cursor's {additional_context}).
|
|
67
|
+
const CURSOR_AW_ROUTER_DESCRIPTION = 'AW-managed: using-aw-skills router';
|
|
68
|
+
const CURSOR_AW_ROUTER_COMMAND = 'bash "$HOME/.cursor/hooks/aw-session-start.sh"';
|
|
69
|
+
const CURSOR_AW_HOOK_MARKER = '# aw-managed: cursor-aw-session-start';
|
|
70
|
+
const CURSOR_AW_HOOK_SCRIPT = `#!/usr/bin/env bash
|
|
71
|
+
${CURSOR_AW_HOOK_MARKER}
|
|
72
|
+
set -euo pipefail
|
|
73
|
+
for f in "$HOME/.aw/.aw_registry/aw/skills/using-aw-skills/hooks/session-start.sh" "$HOME/.aw_registry/aw/skills/using-aw-skills/hooks/session-start.sh" "$HOME/.aw/.aw_registry/platform/core/skills/using-aw-skills/hooks/session-start.sh" "$HOME/.aw_registry/platform/core/skills/using-aw-skills/hooks/session-start.sh"; do
|
|
74
|
+
if [[ -f "$f" ]]; then
|
|
75
|
+
CURSOR_PLUGIN_ROOT=1 exec bash "$f"
|
|
76
|
+
fi
|
|
77
|
+
done
|
|
78
|
+
echo '{"additional_context": "[AW] using-aw-skills router not found in ~/.aw registry. Run aw pull aw or aw init."}'
|
|
79
|
+
`;
|
|
50
80
|
const REPO_CLAUDE_SESSION_START_COMMAND = '"$CLAUDE_PROJECT_DIR"/hooks/aw-session-start';
|
|
51
81
|
const CODEX_HOME_PHASE_DEFINITIONS = getCodexHomePhaseDefinitions();
|
|
52
82
|
const CODEX_HOME_PHASE_BY_NAME = new Map(
|
|
@@ -164,6 +194,9 @@ function hasClaudePluginCache(homeDir = homedir()) {
|
|
|
164
194
|
}
|
|
165
195
|
|
|
166
196
|
function isLegacyClaudeSessionStartEntry(entry) {
|
|
197
|
+
// The managed router intentionally lists the platform/core path as a fallback candidate;
|
|
198
|
+
// it is the current managed entry, not a stale legacy hook.
|
|
199
|
+
if (entry?.description === CLAUDE_ROUTER_DESCRIPTION) return false;
|
|
167
200
|
return Array.isArray(entry?.hooks)
|
|
168
201
|
&& entry.hooks.some(hook =>
|
|
169
202
|
hook?.type === 'command'
|
|
@@ -172,6 +205,38 @@ function isLegacyClaudeSessionStartEntry(entry) {
|
|
|
172
205
|
);
|
|
173
206
|
}
|
|
174
207
|
|
|
208
|
+
function isManagedClaudeRouterEntry(entry) {
|
|
209
|
+
return entry?.description === CLAUDE_ROUTER_DESCRIPTION;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function isManagedClaudeReminderEntry(entry) {
|
|
213
|
+
return entry?.description === CLAUDE_REMINDER_DESCRIPTION;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Orphaned `{ matcher: '*', hooks: [] }` stubs with no description accumulated in some
|
|
217
|
+
// settings.json files and are invisible to the disable-override filter. Strip them.
|
|
218
|
+
function isEmptyClaudeSessionStartStub(entry) {
|
|
219
|
+
return entry?.matcher === '*'
|
|
220
|
+
&& Array.isArray(entry?.hooks)
|
|
221
|
+
&& entry.hooks.length === 0
|
|
222
|
+
&& !entry?.description;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function buildClaudeRouterEntry() {
|
|
226
|
+
return {
|
|
227
|
+
matcher: 'startup|resume|clear|compact',
|
|
228
|
+
hooks: [{ type: 'command', command: CLAUDE_ROUTER_COMMAND }],
|
|
229
|
+
description: CLAUDE_ROUTER_DESCRIPTION,
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function buildClaudeReminderEntry() {
|
|
234
|
+
return {
|
|
235
|
+
hooks: [{ type: 'command', command: CLAUDE_REMINDER_COMMAND }],
|
|
236
|
+
description: CLAUDE_REMINDER_DESCRIPTION,
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
175
240
|
function hasCommandHook(entry, command) {
|
|
176
241
|
return Array.isArray(entry?.hooks)
|
|
177
242
|
&& entry.hooks.some(hook =>
|
|
@@ -336,45 +401,98 @@ function disableClaudeStartup(homeDir = homedir()) {
|
|
|
336
401
|
const config = readJson(settingsPath, {});
|
|
337
402
|
ensureHooksObject(config);
|
|
338
403
|
|
|
404
|
+
const before = JSON.stringify(config.hooks);
|
|
405
|
+
|
|
339
406
|
const current = Array.isArray(config.hooks.SessionStart) ? config.hooks.SessionStart : [];
|
|
340
|
-
|
|
407
|
+
config.hooks.SessionStart = [
|
|
341
408
|
{
|
|
342
409
|
matcher: '*',
|
|
343
410
|
hooks: [],
|
|
344
411
|
description: CLAUDE_DISABLE_DESCRIPTION,
|
|
345
412
|
},
|
|
346
|
-
|
|
413
|
+
// Strip everything enableClaudeStartup installs: the managed router, orphaned
|
|
414
|
+
// empty stubs, legacy platform/core entries, and any prior disable override.
|
|
415
|
+
...current.filter(entry =>
|
|
416
|
+
!isManagedClaudeSessionStartOverride(entry)
|
|
417
|
+
&& !isLegacyClaudeSessionStartEntry(entry)
|
|
418
|
+
&& !isEmptyClaudeSessionStartStub(entry)
|
|
419
|
+
&& !isManagedClaudeRouterEntry(entry)),
|
|
347
420
|
];
|
|
348
421
|
|
|
349
|
-
|
|
422
|
+
// Remove the managed per-prompt reminder installed by enableClaudeStartup.
|
|
423
|
+
if (Array.isArray(config.hooks.UserPromptSubmit)) {
|
|
424
|
+
const filtered = config.hooks.UserPromptSubmit.filter(entry => !isManagedClaudeReminderEntry(entry));
|
|
425
|
+
if (filtered.length > 0) config.hooks.UserPromptSubmit = filtered;
|
|
426
|
+
else delete config.hooks.UserPromptSubmit;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
if (JSON.stringify(config.hooks) === before) {
|
|
350
430
|
return [];
|
|
351
431
|
}
|
|
352
432
|
|
|
353
|
-
config.hooks.SessionStart = next;
|
|
354
433
|
writeJson(settingsPath, config);
|
|
355
434
|
return [settingsPath];
|
|
356
435
|
}
|
|
357
436
|
|
|
358
|
-
|
|
437
|
+
// Full removal of every AW-managed Claude startup hook (router, reminder, disable
|
|
438
|
+
// override, orphaned stubs, legacy entries) for `aw nuke` — restores settings.json
|
|
439
|
+
// toward its pre-AW state and prunes the file/hooks object if nothing else remains.
|
|
440
|
+
export function purgeManagedClaudeStartupHooks(homeDir = homedir()) {
|
|
359
441
|
const settingsPath = join(homeDir, '.claude', 'settings.json');
|
|
360
442
|
if (!existsSync(settingsPath)) return [];
|
|
361
|
-
|
|
362
443
|
const config = readJson(settingsPath, {});
|
|
363
|
-
if (!isObject(config.hooks)
|
|
364
|
-
|
|
444
|
+
if (!isObject(config.hooks)) return [];
|
|
445
|
+
|
|
446
|
+
const before = JSON.stringify(config.hooks);
|
|
447
|
+
|
|
448
|
+
if (Array.isArray(config.hooks.SessionStart)) {
|
|
449
|
+
const filtered = config.hooks.SessionStart.filter(entry =>
|
|
450
|
+
!isManagedClaudeSessionStartOverride(entry)
|
|
451
|
+
&& !isLegacyClaudeSessionStartEntry(entry)
|
|
452
|
+
&& !isEmptyClaudeSessionStartStub(entry)
|
|
453
|
+
&& !isManagedClaudeRouterEntry(entry));
|
|
454
|
+
if (filtered.length > 0) config.hooks.SessionStart = filtered;
|
|
455
|
+
else delete config.hooks.SessionStart;
|
|
365
456
|
}
|
|
457
|
+
if (Array.isArray(config.hooks.UserPromptSubmit)) {
|
|
458
|
+
const filtered = config.hooks.UserPromptSubmit.filter(entry => !isManagedClaudeReminderEntry(entry));
|
|
459
|
+
if (filtered.length > 0) config.hooks.UserPromptSubmit = filtered;
|
|
460
|
+
else delete config.hooks.UserPromptSubmit;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
if (JSON.stringify(config.hooks) === before) return [];
|
|
464
|
+
|
|
465
|
+
if (isEmptyObject(config.hooks)) delete config.hooks;
|
|
466
|
+
if (!pruneEmptySettingsFile(settingsPath, config)) writeJson(settingsPath, config);
|
|
467
|
+
return [settingsPath];
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function enableClaudeStartup(homeDir = homedir()) {
|
|
471
|
+
const settingsPath = join(homeDir, '.claude', 'settings.json');
|
|
472
|
+
const config = readJson(settingsPath, {});
|
|
473
|
+
ensureHooksObject(config);
|
|
366
474
|
|
|
367
|
-
const
|
|
368
|
-
|
|
475
|
+
const before = JSON.stringify(config.hooks);
|
|
476
|
+
|
|
477
|
+
// SessionStart: install the using-aw-skills router. Drop AW disable-overrides,
|
|
478
|
+
// legacy platform/core router entries, orphaned empty stubs, and any prior managed
|
|
479
|
+
// router entry so re-runs replace rather than accumulate (idempotent).
|
|
480
|
+
const ssCurrent = Array.isArray(config.hooks.SessionStart) ? config.hooks.SessionStart : [];
|
|
481
|
+
const ssCleaned = ssCurrent.filter(entry =>
|
|
482
|
+
!isManagedClaudeSessionStartOverride(entry)
|
|
483
|
+
&& !isLegacyClaudeSessionStartEntry(entry)
|
|
484
|
+
&& !isEmptyClaudeSessionStartStub(entry)
|
|
485
|
+
&& !isManagedClaudeRouterEntry(entry),
|
|
369
486
|
);
|
|
370
|
-
|
|
371
|
-
return [];
|
|
372
|
-
}
|
|
487
|
+
config.hooks.SessionStart = [...ssCleaned, buildClaudeRouterEntry()];
|
|
373
488
|
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
489
|
+
// UserPromptSubmit: install the per-prompt reminder (the strongest always-on trigger).
|
|
490
|
+
const upCurrent = Array.isArray(config.hooks.UserPromptSubmit) ? config.hooks.UserPromptSubmit : [];
|
|
491
|
+
const upCleaned = upCurrent.filter(entry => !isManagedClaudeReminderEntry(entry));
|
|
492
|
+
config.hooks.UserPromptSubmit = [...upCleaned, buildClaudeReminderEntry()];
|
|
493
|
+
|
|
494
|
+
if (JSON.stringify(config.hooks) === before) {
|
|
495
|
+
return [];
|
|
378
496
|
}
|
|
379
497
|
|
|
380
498
|
if (isEmptyObject(config.hooks)) {
|
|
@@ -575,7 +693,8 @@ function isManagedCursorSessionStartEntry(entry, homeDir = homedir()) {
|
|
|
575
693
|
}
|
|
576
694
|
|
|
577
695
|
function hasCursorSessionStartScript(homeDir = homedir()) {
|
|
578
|
-
return existsSync(join(homeDir, '.cursor', 'hooks', 'session-start.js'))
|
|
696
|
+
return existsSync(join(homeDir, '.cursor', 'hooks', 'session-start.js'))
|
|
697
|
+
|| existsSync(join(homeDir, '.cursor', 'hooks', 'aw-session-start.sh'));
|
|
579
698
|
}
|
|
580
699
|
|
|
581
700
|
function disableCursorStartup(homeDir = homedir()) {
|
|
@@ -641,6 +760,71 @@ function enableCursorStartup(homeDir = homedir()) {
|
|
|
641
760
|
return [hooksPath];
|
|
642
761
|
}
|
|
643
762
|
|
|
763
|
+
function isManagedCursorAwRouterEntry(entry) {
|
|
764
|
+
return entry?.description === CURSOR_AW_ROUTER_DESCRIPTION
|
|
765
|
+
|| String(entry?.command || '') === CURSOR_AW_ROUTER_COMMAND;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
function enableCursorAwRouting(homeDir = homedir()) {
|
|
769
|
+
const updated = [];
|
|
770
|
+
|
|
771
|
+
// 1. Write the managed AW router hook script (idempotent).
|
|
772
|
+
const scriptPath = join(homeDir, '.cursor', 'hooks', 'aw-session-start.sh');
|
|
773
|
+
const existing = existsSync(scriptPath) ? readFileSync(scriptPath, 'utf8') : null;
|
|
774
|
+
if (existing !== CURSOR_AW_HOOK_SCRIPT) {
|
|
775
|
+
mkdirSync(dirname(scriptPath), { recursive: true });
|
|
776
|
+
writeFileSync(scriptPath, CURSOR_AW_HOOK_SCRIPT);
|
|
777
|
+
try { chmodSync(scriptPath, 0o755); } catch { /* best effort */ }
|
|
778
|
+
updated.push(scriptPath);
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
// 2. Wire it into ~/.cursor/hooks.json sessionStart, additive and idempotent — preserve
|
|
782
|
+
// Cursor's native session-start hook and any unrelated entries.
|
|
783
|
+
const hooksPath = join(homeDir, '.cursor', 'hooks.json');
|
|
784
|
+
const config = readJson(hooksPath, {});
|
|
785
|
+
if (!isObject(config.hooks)) config.hooks = {};
|
|
786
|
+
config.version = Number.isInteger(config.version) ? config.version : 1;
|
|
787
|
+
const current = Array.isArray(config.hooks.sessionStart) ? config.hooks.sessionStart : [];
|
|
788
|
+
if (!current.some(isManagedCursorAwRouterEntry)) {
|
|
789
|
+
config.hooks.sessionStart = [
|
|
790
|
+
...current,
|
|
791
|
+
{ command: CURSOR_AW_ROUTER_COMMAND, event: 'sessionStart', description: CURSOR_AW_ROUTER_DESCRIPTION },
|
|
792
|
+
];
|
|
793
|
+
writeJson(hooksPath, config);
|
|
794
|
+
updated.push(hooksPath);
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
return updated;
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
export function disableCursorAwRouting(homeDir = homedir()) {
|
|
801
|
+
const updated = [];
|
|
802
|
+
|
|
803
|
+
const hooksPath = join(homeDir, '.cursor', 'hooks.json');
|
|
804
|
+
if (existsSync(hooksPath)) {
|
|
805
|
+
const config = readJson(hooksPath, {});
|
|
806
|
+
if (isObject(config.hooks) && Array.isArray(config.hooks.sessionStart)) {
|
|
807
|
+
const filtered = config.hooks.sessionStart.filter(entry => !isManagedCursorAwRouterEntry(entry));
|
|
808
|
+
if (filtered.length !== config.hooks.sessionStart.length) {
|
|
809
|
+
if (filtered.length > 0) config.hooks.sessionStart = filtered;
|
|
810
|
+
else delete config.hooks.sessionStart;
|
|
811
|
+
if (isEmptyObject(config.hooks)) delete config.hooks;
|
|
812
|
+
if (config.version === undefined) config.version = 1;
|
|
813
|
+
if (!pruneEmptySettingsFile(hooksPath, config)) writeJson(hooksPath, config);
|
|
814
|
+
updated.push(hooksPath);
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
const scriptPath = join(homeDir, '.cursor', 'hooks', 'aw-session-start.sh');
|
|
820
|
+
if (existsSync(scriptPath) && readFileSync(scriptPath, 'utf8').includes(CURSOR_AW_HOOK_MARKER)) {
|
|
821
|
+
rmSync(scriptPath, { force: true });
|
|
822
|
+
updated.push(scriptPath);
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
return updated;
|
|
826
|
+
}
|
|
827
|
+
|
|
644
828
|
export function loadStartupPreferences(homeDir = homedir()) {
|
|
645
829
|
const prefs = readJson(startupPrefsPath(homeDir), {});
|
|
646
830
|
return {
|
|
@@ -685,11 +869,13 @@ export function applyGlobalStartupMode(mode, homeDir = homedir()) {
|
|
|
685
869
|
updatedFiles.push(...disableClaudeTelemetryHooks(homeDir));
|
|
686
870
|
updatedFiles.push(...disableCodexStartup(homeDir));
|
|
687
871
|
updatedFiles.push(...disableCursorStartup(homeDir));
|
|
872
|
+
updatedFiles.push(...disableCursorAwRouting(homeDir));
|
|
688
873
|
} else {
|
|
689
874
|
updatedFiles.push(...enableClaudeStartup(homeDir));
|
|
690
875
|
updatedFiles.push(...enableClaudeTelemetryHooks(homeDir));
|
|
691
876
|
updatedFiles.push(...enableCodexStartup(homeDir));
|
|
692
877
|
updatedFiles.push(...enableCursorStartup(homeDir));
|
|
878
|
+
updatedFiles.push(...enableCursorAwRouting(homeDir));
|
|
693
879
|
}
|
|
694
880
|
|
|
695
881
|
return [...new Set(updatedFiles)];
|
|
@@ -719,6 +905,8 @@ export function getStartupStatus(homeDir = homedir(), env = process.env) {
|
|
|
719
905
|
preferencesPath: startupPrefsPath(homeDir),
|
|
720
906
|
claudePluginEnabled: claudeSettings?.enabledPlugins?.['aw@aw-marketplace'] === true,
|
|
721
907
|
claudePluginInstalled: hasClaudePluginCache(homeDir),
|
|
908
|
+
claudeRouterPresent: Array.isArray(claudeSettings?.hooks?.SessionStart) &&
|
|
909
|
+
claudeSettings.hooks.SessionStart.some(isManagedClaudeRouterEntry),
|
|
722
910
|
claudeDisabled: Array.isArray(claudeSettings?.hooks?.SessionStart) &&
|
|
723
911
|
claudeSettings.hooks.SessionStart.some(isManagedClaudeSessionStartOverride),
|
|
724
912
|
claudeLegacySessionStartPresent: Array.isArray(claudeSettings?.hooks?.SessionStart) &&
|
|
@@ -728,7 +916,7 @@ export function getStartupStatus(homeDir = homedir(), env = process.env) {
|
|
|
728
916
|
codexHooks.hooks.SessionStart.some(isManagedCodexSessionStartEntry),
|
|
729
917
|
codexSessionStartScriptInstalled: hasCodexSessionStartScript(homeDir),
|
|
730
918
|
cursorSessionStartPresent: cursorSessionStartEntries.some(entry =>
|
|
731
|
-
isManagedCursorSessionStartEntry(entry, homeDir)),
|
|
919
|
+
isManagedCursorSessionStartEntry(entry, homeDir) || isManagedCursorAwRouterEntry(entry)),
|
|
732
920
|
cursorAnySessionStartPresent: cursorSessionStartEntries.length > 0,
|
|
733
921
|
cursorSessionStartScriptInstalled: hasCursorSessionStartScript(homeDir),
|
|
734
922
|
};
|