@amsterdamdatalabs/enact-extensions 0.1.0 → 0.1.3
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/README.md +96 -21
- package/dist/index.d.ts +5 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/install.d.ts +171 -1
- package/dist/install.d.ts.map +1 -1
- package/dist/install.js +402 -49
- package/dist/install.js.map +1 -1
- package/dist/internal/codex.d.ts.map +1 -1
- package/dist/internal/codex.js +7 -1
- package/dist/internal/codex.js.map +1 -1
- package/dist/internal/platform.d.ts +8 -0
- package/dist/internal/platform.d.ts.map +1 -1
- package/dist/internal/platform.js +46 -2
- package/dist/internal/platform.js.map +1 -1
- package/dist/provision.d.ts +30 -0
- package/dist/provision.d.ts.map +1 -0
- package/dist/provision.js +202 -0
- package/dist/provision.js.map +1 -0
- package/dist/validate/index.d.ts +44 -0
- package/dist/validate/index.d.ts.map +1 -1
- package/dist/validate/index.js +157 -0
- package/dist/validate/index.js.map +1 -1
- package/extensions/cmux/.agents/plugin.json +37 -0
- package/extensions/cmux/skills/cmux/SKILL.md +82 -0
- package/extensions/cmux/skills/cmux/agents/openai.yaml +4 -0
- package/extensions/cmux/skills/cmux/references/handles-and-identify.md +35 -0
- package/extensions/cmux/skills/cmux/references/panes-surfaces.md +37 -0
- package/extensions/cmux/skills/cmux/references/trigger-flash-and-health.md +23 -0
- package/extensions/cmux/skills/cmux/references/windows-workspaces.md +31 -0
- package/extensions/cmux/skills/cmux-vm-monitor/SKILL.md +122 -0
- package/extensions/cmux/skills/cmux-vm-monitor/agents/openai.yaml +4 -0
- package/extensions/cmux/skills/cmux-vm-monitor/references/cmux-commands.md +66 -0
- package/extensions/cmux/skills/cmux-vm-monitor/scripts/codex_vm_monitor.sh +45 -0
- package/extensions/cmux/skills/cmux-workspace/SKILL.md +93 -0
- package/extensions/dev-state/.agents/plugin.json +35 -0
- package/extensions/dev-state/skills/dev-state-plan-graduation/SKILL.md +194 -0
- package/extensions/dev-state/skills/dev-state-plan-graduation/agents/openai.yaml +4 -0
- package/extensions/dev-state/skills/dev-state-plan-graduation/references/reference.md +130 -0
- package/extensions/devops/.agents/plugin.json +36 -0
- package/extensions/devops/skills/azure-devops-cli/SKILL.md +431 -0
- package/extensions/devops/skills/azure-devops-cli/agents/openai.yaml +4 -0
- package/extensions/devops/skills/ci-pipeline-strategy/SKILL.md +217 -0
- package/extensions/devops/skills/ci-pipeline-strategy/agents/openai.yaml +4 -0
- package/extensions/enact-context/.agents/plugin.json +40 -0
- package/extensions/enact-context/.mcp.json +8 -0
- package/extensions/enact-context/README.md +25 -0
- package/extensions/enact-context/assets/icon.png +0 -0
- package/extensions/enact-context/assets/logo.png +0 -0
- package/extensions/enact-context/hooks/hooks.json +115 -0
- package/extensions/enact-context/skills/enact-context/SKILL.md +149 -0
- package/extensions/enact-context/skills/enact-context/scripts/install.sh +69 -0
- package/extensions/enact-factory/.agents/plugin.json +42 -0
- package/extensions/enact-factory/.mcp.json +8 -0
- package/extensions/enact-factory/assets/icon.png +0 -0
- package/extensions/enact-factory/assets/logo.png +0 -0
- package/extensions/enact-factory/hooks/user-prompt-submit.mjs +67 -0
- package/extensions/enact-factory/skills/testing-strategy/SKILL.md +167 -0
- package/extensions/enact-factory/skills/workitem-triage/SKILL.md +22 -0
- package/extensions/enact-operator/.agents/plugin.json +57 -0
- package/extensions/enact-operator/.app.json +3 -0
- package/extensions/enact-operator/.mcp.json +10 -0
- package/extensions/enact-operator/_taxonomy.md +86 -0
- package/extensions/enact-operator/agents/README.md +5 -0
- package/extensions/enact-operator/agents/architect.toml +25 -0
- package/extensions/enact-operator/agents/code-reviewer.toml +24 -0
- package/extensions/enact-operator/agents/critic.toml +30 -0
- package/extensions/enact-operator/agents/executor.toml +24 -0
- package/extensions/enact-operator/agents/explore.toml +23 -0
- package/extensions/enact-operator/agents/planner.toml +24 -0
- package/extensions/enact-operator/agents/verifier.toml +24 -0
- package/extensions/enact-operator/assets/icon.png +0 -0
- package/extensions/enact-operator/assets/logo.png +0 -0
- package/extensions/enact-operator/commands/doctor.md +39 -0
- package/extensions/enact-operator/commands/setup.md +51 -0
- package/extensions/enact-operator/hooks/hooks.json +146 -0
- package/extensions/enact-operator/skills/_variants.md +44 -0
- package/extensions/enact-operator/skills/ai-slop-cleaner/SKILL.md +50 -0
- package/extensions/enact-operator/skills/analyze/SKILL.md +91 -0
- package/extensions/enact-operator/skills/ask/SKILL.md +47 -0
- package/extensions/enact-operator/skills/autopilot/SKILL.md +170 -0
- package/extensions/enact-operator/skills/autoresearch-goal/SKILL.md +79 -0
- package/extensions/enact-operator/skills/cancel/SKILL.md +99 -0
- package/extensions/enact-operator/skills/configure-notifications/SKILL.md +77 -0
- package/extensions/enact-operator/skills/deep-interview/SKILL.md +80 -0
- package/extensions/enact-operator/skills/doctor/SKILL.md +48 -0
- package/extensions/enact-operator/skills/hud/SKILL.md +49 -0
- package/extensions/enact-operator/skills/hyperplan/SKILL.md +47 -0
- package/extensions/enact-operator/skills/plan/SKILL.md +78 -0
- package/extensions/enact-operator/skills/ralph/SKILL.md +201 -0
- package/extensions/enact-operator/skills/ralph/gemini.md +18 -0
- package/extensions/enact-operator/skills/ralplan/SKILL.md +151 -0
- package/extensions/enact-operator/skills/remove-deadcode/SKILL.md +45 -0
- package/extensions/enact-operator/skills/research/SKILL.md +74 -0
- package/extensions/enact-operator/skills/review/SKILL.md +58 -0
- package/extensions/enact-operator/skills/security-research/SKILL.md +54 -0
- package/extensions/enact-operator/skills/setup/SKILL.md +91 -0
- package/extensions/enact-operator/skills/setup/scripts/install.sh +50 -0
- package/extensions/enact-operator/skills/skill/SKILL.md +82 -0
- package/extensions/enact-operator/skills/tdd/SKILL.md +59 -0
- package/extensions/enact-operator/skills/team/SKILL.md +199 -0
- package/extensions/enact-operator/skills/trace/SKILL.md +41 -0
- package/extensions/enact-operator/skills/ultragoal/SKILL.md +99 -0
- package/extensions/enact-operator/skills/ultraqa/SKILL.md +113 -0
- package/extensions/enact-operator/skills/ultrawork/SKILL.md +145 -0
- package/extensions/enact-operator/skills/ultrawork/planner.md +28 -0
- package/extensions/enact-operator/skills/wiki/SKILL.md +41 -0
- package/extensions/enact-operator/skills/work-with-workitem/SKILL.md +51 -0
- package/extensions/enact-wiki/.agents/plugin.json +42 -0
- package/extensions/enact-wiki/.mcp.json +15 -0
- package/extensions/enact-wiki/README.md +44 -0
- package/extensions/enact-wiki/assets/icon.png +0 -0
- package/extensions/enact-wiki/assets/logo.png +0 -0
- package/extensions/enact-wiki/skills/document-parser/SKILL.md +17 -0
- package/extensions/enact-wiki/skills/document-parser/scripts/parse.sh +60 -0
- package/extensions/enact-wiki/skills/document-parser/skill.json +9 -0
- package/extensions/enact-wiki/skills/enact-wiki/SKILL.md +30 -0
- package/extensions/enact-wiki/skills/enact-wiki/references/ingest.md +62 -0
- package/extensions/enact-wiki/skills/enact-wiki/references/manage.md +34 -0
- package/extensions/enact-wiki/skills/enact-wiki/references/query.md +59 -0
- package/extensions/enact-wiki/skills/search-lab/SKILL.md +57 -0
- package/extensions/enact-wiki/skills/search-lab/scripts/analyze.ts +23 -0
- package/{plugins/net-revenue-management/.codex-plugin → extensions/net-revenue-management/.agents}/plugin.json +10 -6
- package/extensions/plugin-dev/.agents/plugin.json +42 -0
- package/extensions/plugin-dev/.mcp.json +3 -0
- package/extensions/plugin-dev/agents/agent-creator.md +199 -0
- package/extensions/plugin-dev/agents/plugin-validator.md +91 -0
- package/extensions/plugin-dev/agents/skill-reviewer.md +212 -0
- package/extensions/plugin-dev/commands/_archive/create-marketplace.md +427 -0
- package/extensions/plugin-dev/commands/_archive/plugin-dev-guide.md +12 -0
- package/extensions/plugin-dev/commands/create-plugin.md +498 -0
- package/extensions/plugin-dev/commands/start.md +81 -0
- package/extensions/plugin-dev/hooks/hooks.json +3 -0
- package/extensions/plugin-dev/skills/agent-development/SKILL.md +641 -0
- package/extensions/plugin-dev/skills/agent-development/examples/agent-creation-prompt.md +250 -0
- package/extensions/plugin-dev/skills/agent-development/examples/complete-agent-examples.md +461 -0
- package/extensions/plugin-dev/skills/agent-development/references/advanced-agent-fields.md +246 -0
- package/extensions/plugin-dev/skills/agent-development/references/agent-creation-system-prompt.md +216 -0
- package/extensions/plugin-dev/skills/agent-development/references/permission-modes-rules.md +226 -0
- package/extensions/plugin-dev/skills/agent-development/references/system-prompt-design.md +464 -0
- package/extensions/plugin-dev/skills/agent-development/references/triggering-examples.md +474 -0
- package/extensions/plugin-dev/skills/agent-development/scripts/create-agent-skeleton.sh +176 -0
- package/extensions/plugin-dev/skills/agent-development/scripts/test-agent-trigger.sh +227 -0
- package/extensions/plugin-dev/skills/agent-development/scripts/validate-agent.sh +227 -0
- package/extensions/plugin-dev/skills/command-development/SKILL.md +763 -0
- package/extensions/plugin-dev/skills/command-development/examples/plugin-commands.md +612 -0
- package/extensions/plugin-dev/skills/command-development/examples/simple-commands.md +527 -0
- package/extensions/plugin-dev/skills/command-development/references/advanced-workflows.md +762 -0
- package/extensions/plugin-dev/skills/command-development/references/documentation-patterns.md +769 -0
- package/extensions/plugin-dev/skills/command-development/references/frontmatter-reference.md +508 -0
- package/extensions/plugin-dev/skills/command-development/references/interactive-commands.md +966 -0
- package/extensions/plugin-dev/skills/command-development/references/marketplace-considerations.md +943 -0
- package/extensions/plugin-dev/skills/command-development/references/plugin-features-reference.md +637 -0
- package/extensions/plugin-dev/skills/command-development/references/plugin-integration.md +191 -0
- package/extensions/plugin-dev/skills/command-development/references/skill-tool.md +447 -0
- package/extensions/plugin-dev/skills/command-development/references/testing-strategies.md +723 -0
- package/extensions/plugin-dev/skills/command-development/scripts/check-frontmatter.sh +234 -0
- package/extensions/plugin-dev/skills/command-development/scripts/validate-command.sh +160 -0
- package/extensions/plugin-dev/skills/hook-development/SKILL.md +861 -0
- package/extensions/plugin-dev/skills/hook-development/examples/load-context.sh +55 -0
- package/extensions/plugin-dev/skills/hook-development/examples/validate-bash.sh +57 -0
- package/extensions/plugin-dev/skills/hook-development/examples/validate-write.sh +48 -0
- package/extensions/plugin-dev/skills/hook-development/references/advanced.md +871 -0
- package/extensions/plugin-dev/skills/hook-development/references/hook-input-schemas.md +145 -0
- package/extensions/plugin-dev/skills/hook-development/references/migration.md +392 -0
- package/extensions/plugin-dev/skills/hook-development/references/patterns.md +430 -0
- package/extensions/plugin-dev/skills/hook-development/scripts/README.md +181 -0
- package/extensions/plugin-dev/skills/hook-development/scripts/hook-linter.sh +153 -0
- package/extensions/plugin-dev/skills/hook-development/scripts/test-hook.sh +276 -0
- package/extensions/plugin-dev/skills/hook-development/scripts/validate-hook-schema.sh +159 -0
- package/extensions/plugin-dev/skills/mcp-integration/SKILL.md +775 -0
- package/extensions/plugin-dev/skills/mcp-integration/examples/http-server.json +20 -0
- package/extensions/plugin-dev/skills/mcp-integration/examples/sse-server.json +19 -0
- package/extensions/plugin-dev/skills/mcp-integration/examples/stdio-server.json +38 -0
- package/extensions/plugin-dev/skills/mcp-integration/examples/ws-server.json +26 -0
- package/extensions/plugin-dev/skills/mcp-integration/references/authentication.md +601 -0
- package/extensions/plugin-dev/skills/mcp-integration/references/server-discovery.md +190 -0
- package/extensions/plugin-dev/skills/mcp-integration/references/server-types.md +572 -0
- package/extensions/plugin-dev/skills/mcp-integration/references/tool-usage.md +623 -0
- package/extensions/plugin-dev/skills/plugin-dev-guide/SKILL.md +222 -0
- package/extensions/plugin-dev/skills/plugin-structure/SKILL.md +705 -0
- package/extensions/plugin-dev/skills/plugin-structure/examples/advanced-plugin.md +774 -0
- package/extensions/plugin-dev/skills/plugin-structure/examples/minimal-plugin.md +83 -0
- package/extensions/plugin-dev/skills/plugin-structure/examples/standard-plugin.md +611 -0
- package/extensions/plugin-dev/skills/plugin-structure/references/advanced-topics.md +289 -0
- package/extensions/plugin-dev/skills/plugin-structure/references/component-patterns.md +592 -0
- package/extensions/plugin-dev/skills/plugin-structure/references/github-actions.md +233 -0
- package/extensions/plugin-dev/skills/plugin-structure/references/headless-ci-mode.md +193 -0
- package/extensions/plugin-dev/skills/plugin-structure/references/manifest-reference.md +625 -0
- package/extensions/plugin-dev/skills/plugin-structure/references/output-styles.md +116 -0
- package/extensions/plugin-dev/skills/skill-development/SKILL.md +564 -0
- package/extensions/plugin-dev/skills/skill-development/examples/complete-skill.md +465 -0
- package/extensions/plugin-dev/skills/skill-development/examples/frontmatter-templates.md +167 -0
- package/extensions/plugin-dev/skills/skill-development/examples/minimal-skill.md +111 -0
- package/extensions/plugin-dev/skills/skill-development/references/advanced-frontmatter.md +225 -0
- package/extensions/plugin-dev/skills/skill-development/references/commands-vs-skills.md +39 -0
- package/extensions/plugin-dev/skills/skill-development/references/skill-creation-workflow.md +379 -0
- package/extensions/plugin-dev/skills/skill-development/references/skill-creator-original.md +210 -0
- package/package.json +8 -11
- package/scripts/enact-extensions.mjs +823 -21
- package/scripts/hooks/session-start-drift-check.mjs +58 -0
- package/scripts/lib/build-index.mjs +50 -0
- package/scripts/lib/bundle-hash.mjs +137 -0
- package/scripts/lib/hooks.mjs +741 -0
- package/scripts/lib/ledger.mjs +163 -0
- package/scripts/lib/list-bundles.mjs +70 -0
- package/scripts/lib/outdated.mjs +144 -0
- package/scripts/lib/provision-mcp.mjs +16 -0
- package/scripts/lib/resolve-bundle.mjs +121 -0
- package/scripts/lib/run-install.mjs +402 -38
- package/scripts/lib/run-prune.mjs +73 -0
- package/scripts/lib/run-sync.mjs +9 -1
- package/scripts/lib/run-uninstall.mjs +244 -0
- package/scripts/lib/run-update.mjs +152 -0
- package/scripts/lib/run-validate.mjs +21 -18
- package/scripts/lib/serve.mjs +472 -0
- package/scripts/postinstall.mjs +63 -0
- package/scripts/setup-enact-context.sh +2 -2
- package/scripts/version-bump.sh +463 -0
- package/spec/codex.json +1 -11
- package/spec/index.json +59 -0
- package/web/assets/README.md +111 -0
- package/web/assets/logo-full.png +0 -0
- package/web/assets/logo-slim.png +0 -0
- package/web/assets/tokens/base.css +45 -0
- package/web/assets/tokens/colors.css +248 -0
- package/web/assets/tokens/effects.css +24 -0
- package/web/assets/tokens/fonts.css +8 -0
- package/web/assets/tokens/index.css +18 -0
- package/web/assets/tokens/spacing.css +50 -0
- package/web/index.html +1188 -0
- package/.agents/plugins/marketplace.json +0 -20
- package/catalog/enact-context.json +0 -9
- package/catalog/enact-factory.json +0 -7
- package/catalog/enact-operator.json +0 -7
- package/catalog/enact-wiki.json +0 -7
- package/catalog/net-revenue-management.json +0 -8
- package/scripts/rename-supervisor-to-operator.pl +0 -66
- package/scripts/sync-manifests.mjs +0 -23
- package/scripts/validate-catalog.mjs +0 -37
- package/scripts/validate-plugin.mjs +0 -10
- /package/{plugins → extensions}/net-revenue-management/.mcp.json +0 -0
- /package/{plugins → extensions}/net-revenue-management/skills/net-revenue-risks/SKILL.md +0 -0
- /package/{plugins → extensions}/net-revenue-management/skills/net-revenue-scenario/SKILL.md +0 -0
|
@@ -0,0 +1,741 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* hooks.mjs — Drift-check hook registration logic for all surfaces.
|
|
3
|
+
*
|
|
4
|
+
* Supports: claude, codex, cursor, enact
|
|
5
|
+
*
|
|
6
|
+
* Per-surface registration details:
|
|
7
|
+
*
|
|
8
|
+
* CLAUDE:
|
|
9
|
+
* Writes to <claudeHome>/settings.json (creates/merges JSON).
|
|
10
|
+
* Adds a `hooks.SessionStart` array entry with a command that invokes the
|
|
11
|
+
* drift-check script via `node <abs path>`. Idempotent (keyed by a stable
|
|
12
|
+
* marker string in the command). This matches the Claude Code hook format:
|
|
13
|
+
* https://docs.anthropic.com/en/docs/claude-code/hooks
|
|
14
|
+
*
|
|
15
|
+
* CODEX / ENACT:
|
|
16
|
+
* Writes to <home>/config.toml.
|
|
17
|
+
* Adds a `[[hooks.session_start]]` entry. The codex-fork's exact TOML hook
|
|
18
|
+
* schema is approximated from the enact-agent config conventions. The entry
|
|
19
|
+
* is clearly namespaced (`[hooks]` / `[[hooks.session_start]]`) and is
|
|
20
|
+
* idempotent (guarded by a stable comment marker). If the exact schema is
|
|
21
|
+
* different in a given codex/enact build, the TOML entry is still safe to
|
|
22
|
+
* have present (unknown sections are ignored by most TOML parsers).
|
|
23
|
+
*
|
|
24
|
+
* CURSOR:
|
|
25
|
+
* Cursor uses a settings.json at <cursorHome>/settings.json, similar to
|
|
26
|
+
* Claude. Registration mirrors the claude approach (SessionStart hooks array).
|
|
27
|
+
* If Cursor's actual hook schema differs, the entry is still reversible and
|
|
28
|
+
* clearly marked.
|
|
29
|
+
*
|
|
30
|
+
* All registrations are IDEMPOTENT (safe to run twice) and REVERSIBLE
|
|
31
|
+
* (--remove cleanly undoes them).
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
import {
|
|
35
|
+
existsSync,
|
|
36
|
+
mkdirSync,
|
|
37
|
+
readFileSync,
|
|
38
|
+
writeFileSync,
|
|
39
|
+
} from "node:fs";
|
|
40
|
+
import { homedir } from "node:os";
|
|
41
|
+
import { dirname, join, resolve } from "node:path";
|
|
42
|
+
import { fileURLToPath } from "node:url";
|
|
43
|
+
|
|
44
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
45
|
+
const __dirname = dirname(__filename);
|
|
46
|
+
const REPO_ROOT = resolve(__dirname, "..", "..");
|
|
47
|
+
|
|
48
|
+
// Absolute path to the drift-check hook script.
|
|
49
|
+
const DRIFT_CHECK_SCRIPT = join(REPO_ROOT, "scripts", "hooks", "session-start-drift-check.mjs");
|
|
50
|
+
|
|
51
|
+
// Stable marker used to detect an existing entry (idempotency key).
|
|
52
|
+
const CLAUDE_HOOK_MARKER = "enact-extensions:session-start-drift-check";
|
|
53
|
+
const TOML_HOOK_MARKER = "enact-extensions:session-start-drift-check";
|
|
54
|
+
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
// Default home paths per surface
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
function defaultHome(surface) {
|
|
59
|
+
const home = homedir();
|
|
60
|
+
switch (surface) {
|
|
61
|
+
case "claude": return join(home, ".claude");
|
|
62
|
+
case "codex": return join(home, ".codex");
|
|
63
|
+
case "enact": return join(home, ".enact", "agent");
|
|
64
|
+
case "cursor": return join(home, ".cursor");
|
|
65
|
+
default: throw new Error(`Unknown surface: ${surface}`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
// Claude settings.json helpers
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Build the SessionStart hook entry object for Claude's settings.json.
|
|
75
|
+
* The command invokes node directly with the absolute hook script path.
|
|
76
|
+
*
|
|
77
|
+
* Claude Code hook format (from docs + live ~/.claude/settings.json):
|
|
78
|
+
* { matcher: ".*", hooks: [{ type: "command", command: "..." }] }
|
|
79
|
+
*
|
|
80
|
+
* The stable marker lives in the command string itself for idempotency
|
|
81
|
+
* detection (see CLAUDE_HOOK_MARKER / "session-start-drift-check").
|
|
82
|
+
*
|
|
83
|
+
* @returns {object} Claude hook entry in the required shape.
|
|
84
|
+
*/
|
|
85
|
+
function makeClaudeHookEntry() {
|
|
86
|
+
return {
|
|
87
|
+
matcher: ".*",
|
|
88
|
+
hooks: [
|
|
89
|
+
{
|
|
90
|
+
type: "command",
|
|
91
|
+
command: `node ${DRIFT_CHECK_SCRIPT}`,
|
|
92
|
+
},
|
|
93
|
+
],
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Read and parse settings.json. Returns empty object on missing/error.
|
|
99
|
+
*/
|
|
100
|
+
function readSettings(settingsPath) {
|
|
101
|
+
if (!existsSync(settingsPath)) return {};
|
|
102
|
+
try {
|
|
103
|
+
return JSON.parse(readFileSync(settingsPath, "utf8"));
|
|
104
|
+
} catch {
|
|
105
|
+
return {};
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Write settings.json (pretty-printed JSON, creates parent dir).
|
|
111
|
+
*/
|
|
112
|
+
function writeSettings(settingsPath, settings) {
|
|
113
|
+
mkdirSync(dirname(settingsPath), { recursive: true });
|
|
114
|
+
writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf8");
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Register the drift-check hook in Claude's (or Cursor's) settings.json.
|
|
119
|
+
* Idempotent: skips if marker already present.
|
|
120
|
+
*
|
|
121
|
+
* @param {string} settingsPath - Absolute path to settings.json
|
|
122
|
+
* @returns {"registered"|"already_registered"} result
|
|
123
|
+
*/
|
|
124
|
+
function registerClaudeHook(settingsPath) {
|
|
125
|
+
const settings = readSettings(settingsPath);
|
|
126
|
+
if (!settings.hooks) settings.hooks = {};
|
|
127
|
+
if (!Array.isArray(settings.hooks.SessionStart)) settings.hooks.SessionStart = [];
|
|
128
|
+
|
|
129
|
+
// Check if already registered (idempotency).
|
|
130
|
+
// Marker is the path substring "session-start-drift-check" in the nested command.
|
|
131
|
+
const entryMatchesDriftCheck = (e) =>
|
|
132
|
+
e &&
|
|
133
|
+
(
|
|
134
|
+
// New schema: { matcher, hooks: [{ type, command }] }
|
|
135
|
+
(Array.isArray(e.hooks) && e.hooks.some((h) => (h.command ?? "").includes("session-start-drift-check"))) ||
|
|
136
|
+
// Old schema (pre-fix): { _enact_marker, command } — tolerate during transition.
|
|
137
|
+
e._enact_marker === CLAUDE_HOOK_MARKER ||
|
|
138
|
+
(e.command ?? "").includes("session-start-drift-check")
|
|
139
|
+
);
|
|
140
|
+
const alreadyPresent = settings.hooks.SessionStart.some(entryMatchesDriftCheck);
|
|
141
|
+
if (alreadyPresent) return "already_registered";
|
|
142
|
+
|
|
143
|
+
settings.hooks.SessionStart.push(makeClaudeHookEntry());
|
|
144
|
+
writeSettings(settingsPath, settings);
|
|
145
|
+
return "registered";
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Remove the drift-check hook from Claude's (or Cursor's) settings.json.
|
|
150
|
+
* Idempotent: safe if entry not present.
|
|
151
|
+
*
|
|
152
|
+
* @param {string} settingsPath
|
|
153
|
+
* @returns {"removed"|"not_found"} result
|
|
154
|
+
*/
|
|
155
|
+
function removeClaudeHook(settingsPath) {
|
|
156
|
+
if (!existsSync(settingsPath)) return "not_found";
|
|
157
|
+
const settings = readSettings(settingsPath);
|
|
158
|
+
if (!settings.hooks?.SessionStart) return "not_found";
|
|
159
|
+
|
|
160
|
+
const before = settings.hooks.SessionStart.length;
|
|
161
|
+
settings.hooks.SessionStart = settings.hooks.SessionStart.filter(
|
|
162
|
+
(e) => !(
|
|
163
|
+
e &&
|
|
164
|
+
(
|
|
165
|
+
// New schema: { matcher, hooks: [{ type, command }] }
|
|
166
|
+
(Array.isArray(e.hooks) && e.hooks.some((h) => (h.command ?? "").includes("session-start-drift-check"))) ||
|
|
167
|
+
// Old schema (pre-fix): { _enact_marker, command }
|
|
168
|
+
e._enact_marker === CLAUDE_HOOK_MARKER ||
|
|
169
|
+
(e.command ?? "").includes("session-start-drift-check")
|
|
170
|
+
)
|
|
171
|
+
),
|
|
172
|
+
);
|
|
173
|
+
if (settings.hooks.SessionStart.length === before) return "not_found";
|
|
174
|
+
|
|
175
|
+
writeSettings(settingsPath, settings);
|
|
176
|
+
return "removed";
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// ---------------------------------------------------------------------------
|
|
180
|
+
// Codex / Enact config.toml helpers
|
|
181
|
+
//
|
|
182
|
+
// Assumption: the codex-fork and enact-agent config.toml supports a
|
|
183
|
+
// `[[hooks.session_start]]` array of tables, each with a `command` field.
|
|
184
|
+
// This mirrors common codex-rs config patterns. If the exact schema differs,
|
|
185
|
+
// the entry is still safely parseable as TOML and clearly marked for removal.
|
|
186
|
+
// ---------------------------------------------------------------------------
|
|
187
|
+
|
|
188
|
+
const TOML_HOOK_BLOCK = (scriptPath) => `
|
|
189
|
+
# [enact-extensions:session-start-drift-check] BEGIN — do not edit this line
|
|
190
|
+
[[hooks.session_start]]
|
|
191
|
+
command = "node ${scriptPath}"
|
|
192
|
+
# [enact-extensions:session-start-drift-check] END
|
|
193
|
+
`;
|
|
194
|
+
|
|
195
|
+
const TOML_BEGIN_MARKER = `# [${TOML_HOOK_MARKER}] BEGIN`;
|
|
196
|
+
const TOML_END_MARKER = `# [${TOML_HOOK_MARKER}] END`;
|
|
197
|
+
|
|
198
|
+
function pluginMarker(pluginName) {
|
|
199
|
+
return `enact-extensions:plugin-hooks:${pluginName}`;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function eventTomlName(eventName) {
|
|
203
|
+
return String(eventName)
|
|
204
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1_$2")
|
|
205
|
+
.replace(/([A-Z])([A-Z][a-z])/g, "$1_$2")
|
|
206
|
+
.toLowerCase();
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Map a PascalCase Claude/Enact event name to the camelCase key Cursor uses
|
|
211
|
+
* in its plugin-shipped hooks.json.
|
|
212
|
+
*
|
|
213
|
+
* Cursor hook events (from spec/cursor.md and plugin format):
|
|
214
|
+
* SessionStart → sessionStart
|
|
215
|
+
* UserPromptSubmit → beforeSubmitPrompt
|
|
216
|
+
* PreToolUse → preToolUse
|
|
217
|
+
* PostToolUse → postToolUse
|
|
218
|
+
* Stop → stop
|
|
219
|
+
* PreCompact → preCompact
|
|
220
|
+
* SessionEnd → sessionEnd
|
|
221
|
+
* SubagentStart → subagentStart
|
|
222
|
+
* SubagentStop → subagentStop
|
|
223
|
+
*
|
|
224
|
+
* Any unmapped event falls back to lowercase-first (simple camelCase).
|
|
225
|
+
*
|
|
226
|
+
* NOTE: Cursor reads hooks from the plugin-bundled hooks/hooks.json
|
|
227
|
+
* (installed into ~/.cursor/plugins/local/<name>/hooks/hooks.json via
|
|
228
|
+
* installCursorPluginBundle). There is NO global ~/.cursor/hooks.json.
|
|
229
|
+
* This function exists so the bundled hooks.json can be serialised with
|
|
230
|
+
* the Cursor-native camelCase event keys when needed.
|
|
231
|
+
*
|
|
232
|
+
* @param {string} event — PascalCase event name
|
|
233
|
+
* @returns {string} camelCase Cursor event name
|
|
234
|
+
*/
|
|
235
|
+
export function eventCursorName(event) {
|
|
236
|
+
const MAP = {
|
|
237
|
+
SessionStart: "sessionStart",
|
|
238
|
+
UserPromptSubmit: "beforeSubmitPrompt",
|
|
239
|
+
PreToolUse: "preToolUse",
|
|
240
|
+
PostToolUse: "postToolUse",
|
|
241
|
+
Stop: "stop",
|
|
242
|
+
PreCompact: "preCompact",
|
|
243
|
+
SessionEnd: "sessionEnd",
|
|
244
|
+
SubagentStart: "subagentStart",
|
|
245
|
+
SubagentStop: "subagentStop",
|
|
246
|
+
};
|
|
247
|
+
const mapped = MAP[String(event)];
|
|
248
|
+
if (mapped) return mapped;
|
|
249
|
+
// Fallback: lowercase the first character.
|
|
250
|
+
const s = String(event);
|
|
251
|
+
return s.charAt(0).toLowerCase() + s.slice(1);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function tomlString(value) {
|
|
255
|
+
return JSON.stringify(String(value));
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function flattenHookCommands(hookConfig) {
|
|
259
|
+
const out = [];
|
|
260
|
+
const hooks = hookConfig?.hooks && typeof hookConfig.hooks === "object" ? hookConfig.hooks : {};
|
|
261
|
+
for (const [eventName, groups] of Object.entries(hooks)) {
|
|
262
|
+
if (!Array.isArray(groups)) continue;
|
|
263
|
+
for (const group of groups) {
|
|
264
|
+
if (!group || typeof group !== "object") continue;
|
|
265
|
+
const commands = Array.isArray(group.hooks) ? group.hooks : [];
|
|
266
|
+
for (const hook of commands) {
|
|
267
|
+
if (!hook || typeof hook !== "object" || hook.type !== "command" || typeof hook.command !== "string") {
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
out.push({
|
|
271
|
+
eventName,
|
|
272
|
+
matcher: typeof group.matcher === "string" ? group.matcher : undefined,
|
|
273
|
+
command: hook.command,
|
|
274
|
+
statusMessage: typeof hook.statusMessage === "string" ? hook.statusMessage : undefined,
|
|
275
|
+
timeout: typeof hook.timeout === "number" ? hook.timeout : undefined,
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
return out;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function commandLooksPluginOwned(command) {
|
|
284
|
+
return /^enact-(?:operator|context)\s+hook\s+/.test(String(command));
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* True when `command` is owned by the named plugin's runtime CLI, i.e. it
|
|
289
|
+
* starts with `<pluginName> hook ` (the operator/context binaries invoke their
|
|
290
|
+
* hooks as `enact-operator hook <x>` / `enact-context hook <x>`).
|
|
291
|
+
*
|
|
292
|
+
* Scoping removal to the SPECIFIC plugin avoids reconciling one plugin from
|
|
293
|
+
* stripping a sibling plugin's marker-less entries. Falls back to the broad
|
|
294
|
+
* operator|context match when no plugin name is given.
|
|
295
|
+
*/
|
|
296
|
+
function commandOwnedByPlugin(command, pluginName) {
|
|
297
|
+
if (!pluginName) return commandLooksPluginOwned(command);
|
|
298
|
+
const esc = String(pluginName).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
299
|
+
return new RegExp(`^${esc}\\s+hook\\s+`).test(String(command));
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function registerJsonPluginHooks(settingsPath, pluginName, hookConfig) {
|
|
303
|
+
const settings = readSettings(settingsPath);
|
|
304
|
+
if (!settings.hooks) settings.hooks = {};
|
|
305
|
+
const marker = pluginMarker(pluginName);
|
|
306
|
+
let registered = 0;
|
|
307
|
+
let already = 0;
|
|
308
|
+
|
|
309
|
+
for (const entry of flattenHookCommands(hookConfig)) {
|
|
310
|
+
if (!commandLooksPluginOwned(entry.command)) continue;
|
|
311
|
+
if (!Array.isArray(settings.hooks[entry.eventName])) settings.hooks[entry.eventName] = [];
|
|
312
|
+
const exists = settings.hooks[entry.eventName].some((group) =>
|
|
313
|
+
group?._enact_marker === marker &&
|
|
314
|
+
Array.isArray(group.hooks) &&
|
|
315
|
+
group.hooks.some((hook) => hook?.command === entry.command),
|
|
316
|
+
);
|
|
317
|
+
if (exists) {
|
|
318
|
+
already += 1;
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
settings.hooks[entry.eventName].push({
|
|
322
|
+
_enact_marker: marker,
|
|
323
|
+
...(entry.matcher ? { matcher: entry.matcher } : {}),
|
|
324
|
+
hooks: [
|
|
325
|
+
{
|
|
326
|
+
type: "command",
|
|
327
|
+
command: entry.command,
|
|
328
|
+
...(entry.statusMessage ? { statusMessage: entry.statusMessage } : {}),
|
|
329
|
+
...(entry.timeout ? { timeout: entry.timeout } : {}),
|
|
330
|
+
},
|
|
331
|
+
],
|
|
332
|
+
});
|
|
333
|
+
registered += 1;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
if (registered > 0) writeSettings(settingsPath, settings);
|
|
337
|
+
return registered > 0 ? "registered" : already > 0 ? "already_registered" : "skipped";
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function removeJsonPluginHooks(settingsPath, pluginName) {
|
|
341
|
+
if (!existsSync(settingsPath)) return "not_found";
|
|
342
|
+
const settings = readSettings(settingsPath);
|
|
343
|
+
if (!settings.hooks || typeof settings.hooks !== "object") return "not_found";
|
|
344
|
+
const marker = pluginMarker(pluginName);
|
|
345
|
+
let removed = 0;
|
|
346
|
+
|
|
347
|
+
for (const [eventName, groups] of Object.entries(settings.hooks)) {
|
|
348
|
+
if (!Array.isArray(groups)) continue;
|
|
349
|
+
const next = groups.filter((group) => {
|
|
350
|
+
const markerMatch = group?._enact_marker === marker;
|
|
351
|
+
const commandMatch =
|
|
352
|
+
Array.isArray(group?.hooks) &&
|
|
353
|
+
group.hooks.some((hook) => commandOwnedByPlugin(hook?.command, pluginName));
|
|
354
|
+
const shouldRemove = markerMatch || commandMatch;
|
|
355
|
+
if (shouldRemove) removed += 1;
|
|
356
|
+
return !shouldRemove;
|
|
357
|
+
});
|
|
358
|
+
// Drop event keys we have fully emptied so a removed/unsupported event
|
|
359
|
+
// (e.g. SessionIdle) does not linger as `[]` and trip host "unknown hook
|
|
360
|
+
// event" warnings. Personal/other-plugin entries keep the key alive.
|
|
361
|
+
if (next.length === 0) delete settings.hooks[eventName];
|
|
362
|
+
else settings.hooks[eventName] = next;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
if (removed === 0) return "not_found";
|
|
366
|
+
writeSettings(settingsPath, settings);
|
|
367
|
+
return "removed";
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function pluginTomlBlock(pluginName, entry) {
|
|
371
|
+
const marker = pluginMarker(pluginName);
|
|
372
|
+
const eventName = eventTomlName(entry.eventName);
|
|
373
|
+
const suffix = `${entry.eventName}:${entry.command}`;
|
|
374
|
+
const fields = [
|
|
375
|
+
`# [${marker}] BEGIN ${suffix}`,
|
|
376
|
+
`[[hooks.${eventName}]]`,
|
|
377
|
+
...(entry.matcher ? [`matcher = ${tomlString(entry.matcher)}`] : []),
|
|
378
|
+
`command = ${tomlString(entry.command)}`,
|
|
379
|
+
...(entry.statusMessage ? [`status_message = ${tomlString(entry.statusMessage)}`] : []),
|
|
380
|
+
...(entry.timeout ? [`timeout = ${entry.timeout}`] : []),
|
|
381
|
+
`# [${marker}] END ${suffix}`,
|
|
382
|
+
];
|
|
383
|
+
return `${fields.join("\n")}\n`;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function registerTomlPluginHooks(configPath, pluginName, hookConfig) {
|
|
387
|
+
const existing = readToml(configPath);
|
|
388
|
+
const marker = pluginMarker(pluginName);
|
|
389
|
+
const blocks = [];
|
|
390
|
+
let already = 0;
|
|
391
|
+
for (const entry of flattenHookCommands(hookConfig)) {
|
|
392
|
+
if (!commandLooksPluginOwned(entry.command)) continue;
|
|
393
|
+
const suffix = `${entry.eventName}:${entry.command}`;
|
|
394
|
+
if (existing.includes(`# [${marker}] BEGIN ${suffix}`)) {
|
|
395
|
+
already += 1;
|
|
396
|
+
continue;
|
|
397
|
+
}
|
|
398
|
+
blocks.push(pluginTomlBlock(pluginName, entry));
|
|
399
|
+
}
|
|
400
|
+
if (blocks.length === 0) return already > 0 ? "already_registered" : "skipped";
|
|
401
|
+
const next = `${existing.replace(/\s+$/, "")}${existing.trim() ? "\n\n" : ""}${blocks.join("\n")}`;
|
|
402
|
+
writeToml(configPath, next.endsWith("\n") ? next : `${next}\n`);
|
|
403
|
+
return "registered";
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function removeTomlPluginHooks(configPath, pluginName) {
|
|
407
|
+
const existing = readToml(configPath);
|
|
408
|
+
const marker = pluginMarker(pluginName);
|
|
409
|
+
const escaped = marker.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
410
|
+
const pattern = new RegExp(`\\n?# \\[${escaped}\\] BEGIN[^\\n]*\\n[\\s\\S]*?# \\[${escaped}\\] END[^\\n]*\\n?`, "g");
|
|
411
|
+
let next = existing.replace(pattern, "\n");
|
|
412
|
+
let removed = next !== existing;
|
|
413
|
+
|
|
414
|
+
// Legacy cleanup: older codex installs could preserve the hook tables while
|
|
415
|
+
// dropping the marker comments when config.toml was reserialized. Remove any
|
|
416
|
+
// remaining plain [[hooks.*]] blocks that still point at the operator runtime
|
|
417
|
+
// command set.
|
|
418
|
+
const lines = next.split(/\r?\n/);
|
|
419
|
+
const out = [];
|
|
420
|
+
for (let i = 0; i < lines.length;) {
|
|
421
|
+
const line = lines[i];
|
|
422
|
+
if (line.startsWith("[[hooks.")) {
|
|
423
|
+
const block = [line];
|
|
424
|
+
i += 1;
|
|
425
|
+
while (
|
|
426
|
+
i < lines.length &&
|
|
427
|
+
!lines[i].startsWith("[[hooks.") &&
|
|
428
|
+
!/^\[[^\[]/.test(lines[i]) &&
|
|
429
|
+
!lines[i].startsWith(`# [${marker}] BEGIN`)
|
|
430
|
+
) {
|
|
431
|
+
block.push(lines[i]);
|
|
432
|
+
i += 1;
|
|
433
|
+
}
|
|
434
|
+
const commandLine = block.find((entry) => entry.trim().startsWith("command = "));
|
|
435
|
+
const commandMatch = commandLine
|
|
436
|
+
? commandOwnedByPlugin(commandLine.replace(/^command\s*=\s*/, "").replace(/^"|"$/g, ""), pluginName)
|
|
437
|
+
: false;
|
|
438
|
+
const markerMatch = block.some((entry) => entry.includes(marker));
|
|
439
|
+
if (markerMatch || commandMatch) {
|
|
440
|
+
removed = true;
|
|
441
|
+
continue;
|
|
442
|
+
}
|
|
443
|
+
out.push(...block);
|
|
444
|
+
continue;
|
|
445
|
+
}
|
|
446
|
+
out.push(line);
|
|
447
|
+
i += 1;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
if (!removed) return "not_found";
|
|
451
|
+
next = out.join("\n").replace(/\n{3,}/g, "\n\n").replace(/^\n+/, "");
|
|
452
|
+
writeToml(configPath, next.endsWith("\n") ? next : `${next}\n`);
|
|
453
|
+
return "removed";
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* Read config.toml content. Returns empty string on missing.
|
|
458
|
+
*/
|
|
459
|
+
function readToml(configPath) {
|
|
460
|
+
if (!existsSync(configPath)) return "";
|
|
461
|
+
try {
|
|
462
|
+
return readFileSync(configPath, "utf8");
|
|
463
|
+
} catch {
|
|
464
|
+
return "";
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* Write config.toml (creates parent dir).
|
|
470
|
+
*/
|
|
471
|
+
function writeToml(configPath, content) {
|
|
472
|
+
mkdirSync(dirname(configPath), { recursive: true });
|
|
473
|
+
writeFileSync(configPath, content, "utf8");
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* Register the drift-check hook in a codex/enact config.toml.
|
|
478
|
+
* Idempotent: checks for the BEGIN marker before appending.
|
|
479
|
+
*
|
|
480
|
+
* @param {string} configPath
|
|
481
|
+
* @returns {"registered"|"already_registered"} result
|
|
482
|
+
*/
|
|
483
|
+
function registerTomlHook(configPath) {
|
|
484
|
+
const existing = readToml(configPath);
|
|
485
|
+
if (existing.includes(TOML_BEGIN_MARKER)) return "already_registered";
|
|
486
|
+
|
|
487
|
+
const block = TOML_HOOK_BLOCK(DRIFT_CHECK_SCRIPT);
|
|
488
|
+
writeToml(configPath, existing + block);
|
|
489
|
+
return "registered";
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* Remove the drift-check hook block from config.toml.
|
|
494
|
+
* Removes the content between BEGIN and END markers (inclusive).
|
|
495
|
+
*
|
|
496
|
+
* @param {string} configPath
|
|
497
|
+
* @returns {"removed"|"not_found"} result
|
|
498
|
+
*/
|
|
499
|
+
function removeTomlHook(configPath) {
|
|
500
|
+
const existing = readToml(configPath);
|
|
501
|
+
if (!existing.includes(TOML_BEGIN_MARKER)) return "not_found";
|
|
502
|
+
|
|
503
|
+
// Remove the marked block (BEGIN line through END line, inclusive).
|
|
504
|
+
const beginIdx = existing.indexOf(TOML_BEGIN_MARKER);
|
|
505
|
+
const endIdx = existing.indexOf(TOML_END_MARKER);
|
|
506
|
+
if (beginIdx === -1 || endIdx === -1) return "not_found";
|
|
507
|
+
|
|
508
|
+
// Include the newline before BEGIN and after END.
|
|
509
|
+
const beforeBegin = existing.slice(0, beginIdx).replace(/\n$/, "");
|
|
510
|
+
const afterEnd = existing.slice(endIdx + TOML_END_MARKER.length).replace(/^\n/, "");
|
|
511
|
+
const newContent = beforeBegin + (afterEnd ? "\n" + afterEnd : "");
|
|
512
|
+
|
|
513
|
+
writeToml(configPath, newContent);
|
|
514
|
+
return "removed";
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// ---------------------------------------------------------------------------
|
|
518
|
+
// Public API: registerHook / removeHook for each surface
|
|
519
|
+
// ---------------------------------------------------------------------------
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* Register the session-start drift-check hook for a given surface.
|
|
523
|
+
*
|
|
524
|
+
* @param {string} surface — "claude" | "codex" | "cursor" | "enact"
|
|
525
|
+
* @param {object} [opts]
|
|
526
|
+
* @param {string} [opts.claudeHome]
|
|
527
|
+
* @param {string} [opts.codexHome]
|
|
528
|
+
* @param {string} [opts.cursorHome]
|
|
529
|
+
* @param {string} [opts.enactHome]
|
|
530
|
+
* @param {boolean} [opts.local] — if true, use project-local home under cwd
|
|
531
|
+
* @param {string} [opts.cwd] — cwd for --local resolution
|
|
532
|
+
* @returns {{ surface: string, result: "registered"|"already_registered"|"skipped", note?: string }}
|
|
533
|
+
*/
|
|
534
|
+
export function registerHook(surface, opts = {}) {
|
|
535
|
+
try {
|
|
536
|
+
switch (surface) {
|
|
537
|
+
case "claude": {
|
|
538
|
+
const home = opts.claudeHome ?? (opts.local ? join(opts.cwd ?? process.cwd(), ".claude") : defaultHome("claude"));
|
|
539
|
+
const settingsPath = join(home, "settings.json");
|
|
540
|
+
const result = registerClaudeHook(settingsPath);
|
|
541
|
+
return { surface, result, location: settingsPath };
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
case "cursor": {
|
|
545
|
+
// Cursor mirrors Claude's settings.json approach.
|
|
546
|
+
// NOTE: Cursor's actual hook schema may differ — this uses the same
|
|
547
|
+
// SessionStart format as Claude. Reverse with --remove if unwanted.
|
|
548
|
+
const home = opts.cursorHome ?? (opts.local ? join(opts.cwd ?? process.cwd(), ".cursor") : defaultHome("cursor"));
|
|
549
|
+
const settingsPath = join(home, "settings.json");
|
|
550
|
+
const result = registerClaudeHook(settingsPath);
|
|
551
|
+
return {
|
|
552
|
+
surface,
|
|
553
|
+
result,
|
|
554
|
+
location: settingsPath,
|
|
555
|
+
note: "Assumption: Cursor uses settings.json SessionStart hooks (same as Claude Code). Verify if Cursor's actual hook schema differs.",
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
case "codex": {
|
|
560
|
+
const home = opts.codexHome ?? (opts.local ? join(opts.cwd ?? process.cwd(), ".codex") : defaultHome("codex"));
|
|
561
|
+
const configPath = join(home, "config.toml");
|
|
562
|
+
const result = registerTomlHook(configPath);
|
|
563
|
+
return {
|
|
564
|
+
surface,
|
|
565
|
+
result,
|
|
566
|
+
location: configPath,
|
|
567
|
+
note: "Assumption: codex config.toml supports [[hooks.session_start]] with a command field.",
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
case "enact": {
|
|
572
|
+
const home = opts.enactHome ?? (opts.local ? join(opts.cwd ?? process.cwd(), ".enact", "agent") : defaultHome("enact"));
|
|
573
|
+
const configPath = join(home, "config.toml");
|
|
574
|
+
const result = registerTomlHook(configPath);
|
|
575
|
+
return {
|
|
576
|
+
surface,
|
|
577
|
+
result,
|
|
578
|
+
location: configPath,
|
|
579
|
+
note: "Assumption: enact-agent config.toml supports [[hooks.session_start]] with a command field.",
|
|
580
|
+
};
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
default:
|
|
584
|
+
return { surface, result: "skipped", note: `Unknown surface: ${surface}` };
|
|
585
|
+
}
|
|
586
|
+
} catch (err) {
|
|
587
|
+
// Never crash — return skipped with the error note.
|
|
588
|
+
return {
|
|
589
|
+
surface,
|
|
590
|
+
result: "skipped",
|
|
591
|
+
note: `Error: ${err instanceof Error ? err.message : String(err)}`,
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
/**
|
|
597
|
+
* Remove the session-start drift-check hook for a given surface.
|
|
598
|
+
*
|
|
599
|
+
* @param {string} surface
|
|
600
|
+
* @param {object} [opts] — same as registerHook
|
|
601
|
+
* @returns {{ surface: string, result: "removed"|"not_found"|"skipped", note?: string }}
|
|
602
|
+
*/
|
|
603
|
+
export function removeHook(surface, opts = {}) {
|
|
604
|
+
try {
|
|
605
|
+
switch (surface) {
|
|
606
|
+
case "claude": {
|
|
607
|
+
const home = opts.claudeHome ?? (opts.local ? join(opts.cwd ?? process.cwd(), ".claude") : defaultHome("claude"));
|
|
608
|
+
const settingsPath = join(home, "settings.json");
|
|
609
|
+
const result = removeClaudeHook(settingsPath);
|
|
610
|
+
return { surface, result, location: settingsPath };
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
case "cursor": {
|
|
614
|
+
const home = opts.cursorHome ?? (opts.local ? join(opts.cwd ?? process.cwd(), ".cursor") : defaultHome("cursor"));
|
|
615
|
+
const settingsPath = join(home, "settings.json");
|
|
616
|
+
const result = removeClaudeHook(settingsPath);
|
|
617
|
+
return { surface, result, location: settingsPath };
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
case "codex": {
|
|
621
|
+
const home = opts.codexHome ?? (opts.local ? join(opts.cwd ?? process.cwd(), ".codex") : defaultHome("codex"));
|
|
622
|
+
const configPath = join(home, "config.toml");
|
|
623
|
+
const result = removeTomlHook(configPath);
|
|
624
|
+
return { surface, result, location: configPath };
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
case "enact": {
|
|
628
|
+
const home = opts.enactHome ?? (opts.local ? join(opts.cwd ?? process.cwd(), ".enact", "agent") : defaultHome("enact"));
|
|
629
|
+
const configPath = join(home, "config.toml");
|
|
630
|
+
const result = removeTomlHook(configPath);
|
|
631
|
+
return { surface, result, location: configPath };
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
default:
|
|
635
|
+
return { surface, result: "skipped", note: `Unknown surface: ${surface}` };
|
|
636
|
+
}
|
|
637
|
+
} catch (err) {
|
|
638
|
+
return {
|
|
639
|
+
surface,
|
|
640
|
+
result: "skipped",
|
|
641
|
+
note: `Error: ${err instanceof Error ? err.message : String(err)}`,
|
|
642
|
+
};
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
export function registerPluginHooks(surface, pluginName, hookConfig, opts = {}) {
|
|
647
|
+
try {
|
|
648
|
+
switch (surface) {
|
|
649
|
+
case "claude": {
|
|
650
|
+
const home = opts.claudeHome ?? (opts.local ? join(opts.cwd ?? process.cwd(), ".claude") : defaultHome("claude"));
|
|
651
|
+
const settingsPath = join(home, "settings.json");
|
|
652
|
+
const result = registerJsonPluginHooks(settingsPath, pluginName, hookConfig);
|
|
653
|
+
return { surface, result, location: settingsPath };
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
case "cursor": {
|
|
657
|
+
// Cursor reads hooks from the plugin-bundled hooks/hooks.json
|
|
658
|
+
// (installed to ~/.cursor/plugins/local/<name>/hooks/hooks.json by
|
|
659
|
+
// installCursorPluginBundle). There is NO global ~/.cursor/hooks.json
|
|
660
|
+
// to write to. Registering into ~/.cursor/settings.json is incorrect.
|
|
661
|
+
// Hook lifecycle is handled by the plugin bundle itself — skip here.
|
|
662
|
+
return {
|
|
663
|
+
surface,
|
|
664
|
+
result: "skipped",
|
|
665
|
+
note: "Cursor reads hooks from the plugin bundle (hooks/hooks.json installed via installCursorPluginBundle). No global hook file to register into.",
|
|
666
|
+
};
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
case "codex": {
|
|
670
|
+
const home = opts.codexHome ?? (opts.local ? join(opts.cwd ?? process.cwd(), ".codex") : defaultHome("codex"));
|
|
671
|
+
const configPath = join(home, "config.toml");
|
|
672
|
+
const result = registerTomlPluginHooks(configPath, pluginName, hookConfig);
|
|
673
|
+
return { surface, result, location: configPath };
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
case "enact": {
|
|
677
|
+
const home = opts.enactHome ?? (opts.local ? join(opts.cwd ?? process.cwd(), ".enact", "agent") : defaultHome("enact"));
|
|
678
|
+
const configPath = join(home, "config.toml");
|
|
679
|
+
const result = registerTomlPluginHooks(configPath, pluginName, hookConfig);
|
|
680
|
+
return { surface, result, location: configPath };
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
default:
|
|
684
|
+
return { surface, result: "skipped", note: `Unknown surface: ${surface}` };
|
|
685
|
+
}
|
|
686
|
+
} catch (err) {
|
|
687
|
+
return {
|
|
688
|
+
surface,
|
|
689
|
+
result: "skipped",
|
|
690
|
+
note: `Error: ${err instanceof Error ? err.message : String(err)}`,
|
|
691
|
+
};
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
export function removePluginHooks(surface, pluginName, opts = {}) {
|
|
696
|
+
try {
|
|
697
|
+
switch (surface) {
|
|
698
|
+
case "claude": {
|
|
699
|
+
const home = opts.claudeHome ?? (opts.local ? join(opts.cwd ?? process.cwd(), ".claude") : defaultHome("claude"));
|
|
700
|
+
const settingsPath = join(home, "settings.json");
|
|
701
|
+
const result = removeJsonPluginHooks(settingsPath, pluginName);
|
|
702
|
+
return { surface, result, location: settingsPath };
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
case "cursor": {
|
|
706
|
+
// Cursor reads hooks from the plugin bundle — nothing to remove globally.
|
|
707
|
+
return {
|
|
708
|
+
surface,
|
|
709
|
+
result: "skipped",
|
|
710
|
+
note: "Cursor reads hooks from the plugin bundle (hooks/hooks.json installed via installCursorPluginBundle). No global hook file to remove from.",
|
|
711
|
+
};
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
case "codex": {
|
|
715
|
+
const home = opts.codexHome ?? (opts.local ? join(opts.cwd ?? process.cwd(), ".codex") : defaultHome("codex"));
|
|
716
|
+
const configPath = join(home, "config.toml");
|
|
717
|
+
const result = removeTomlPluginHooks(configPath, pluginName);
|
|
718
|
+
return { surface, result, location: configPath };
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
case "enact": {
|
|
722
|
+
const home = opts.enactHome ?? (opts.local ? join(opts.cwd ?? process.cwd(), ".enact", "agent") : defaultHome("enact"));
|
|
723
|
+
const configPath = join(home, "config.toml");
|
|
724
|
+
const result = removeTomlPluginHooks(configPath, pluginName);
|
|
725
|
+
return { surface, result, location: configPath };
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
default:
|
|
729
|
+
return { surface, result: "skipped", note: `Unknown surface: ${surface}` };
|
|
730
|
+
}
|
|
731
|
+
} catch (err) {
|
|
732
|
+
return {
|
|
733
|
+
surface,
|
|
734
|
+
result: "skipped",
|
|
735
|
+
note: `Error: ${err instanceof Error ? err.message : String(err)}`,
|
|
736
|
+
};
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
// The 4 supported surfaces.
|
|
741
|
+
export const SUPPORTED_SURFACES = ["claude", "codex", "cursor", "enact"];
|