@amsterdamdatalabs/enact-extensions 0.1.0 → 0.1.1

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.
Files changed (152) hide show
  1. package/README.md +94 -20
  2. package/dist/index.d.ts +3 -3
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +2 -2
  5. package/dist/index.js.map +1 -1
  6. package/dist/install.d.ts +89 -0
  7. package/dist/install.d.ts.map +1 -1
  8. package/dist/install.js +219 -18
  9. package/dist/install.js.map +1 -1
  10. package/dist/validate/index.d.ts +21 -0
  11. package/dist/validate/index.d.ts.map +1 -1
  12. package/dist/validate/index.js +77 -0
  13. package/dist/validate/index.js.map +1 -1
  14. package/extensions/cmux/.agents/plugin.json +37 -0
  15. package/extensions/cmux/skills/cmux/SKILL.md +82 -0
  16. package/extensions/cmux/skills/cmux/agents/openai.yaml +4 -0
  17. package/extensions/cmux/skills/cmux/references/handles-and-identify.md +35 -0
  18. package/extensions/cmux/skills/cmux/references/panes-surfaces.md +37 -0
  19. package/extensions/cmux/skills/cmux/references/trigger-flash-and-health.md +23 -0
  20. package/extensions/cmux/skills/cmux/references/windows-workspaces.md +31 -0
  21. package/extensions/cmux/skills/cmux-vm-monitor/SKILL.md +122 -0
  22. package/extensions/cmux/skills/cmux-vm-monitor/agents/openai.yaml +4 -0
  23. package/extensions/cmux/skills/cmux-vm-monitor/references/cmux-commands.md +66 -0
  24. package/extensions/cmux/skills/cmux-vm-monitor/scripts/codex_vm_monitor.sh +45 -0
  25. package/extensions/cmux/skills/cmux-workspace/SKILL.md +93 -0
  26. package/extensions/dev-state/.agents/plugin.json +35 -0
  27. package/extensions/dev-state/skills/dev-state-plan-graduation/SKILL.md +194 -0
  28. package/extensions/dev-state/skills/dev-state-plan-graduation/agents/openai.yaml +4 -0
  29. package/extensions/dev-state/skills/dev-state-plan-graduation/references/reference.md +130 -0
  30. package/extensions/devops/.agents/plugin.json +36 -0
  31. package/extensions/devops/skills/azure-devops-cli/SKILL.md +431 -0
  32. package/extensions/devops/skills/azure-devops-cli/agents/openai.yaml +4 -0
  33. package/extensions/devops/skills/ci-pipeline-strategy/SKILL.md +217 -0
  34. package/extensions/devops/skills/ci-pipeline-strategy/agents/openai.yaml +4 -0
  35. package/{plugins/net-revenue-management/.codex-plugin → extensions/net-revenue-management/.agents}/plugin.json +10 -6
  36. package/extensions/plugin-dev/.agents/plugin.json +42 -0
  37. package/extensions/plugin-dev/.mcp.json +3 -0
  38. package/extensions/plugin-dev/agents/agent-creator.md +199 -0
  39. package/extensions/plugin-dev/agents/plugin-validator.md +91 -0
  40. package/extensions/plugin-dev/agents/skill-reviewer.md +212 -0
  41. package/extensions/plugin-dev/commands/_archive/create-marketplace.md +427 -0
  42. package/extensions/plugin-dev/commands/_archive/plugin-dev-guide.md +12 -0
  43. package/extensions/plugin-dev/commands/create-plugin.md +498 -0
  44. package/extensions/plugin-dev/commands/start.md +81 -0
  45. package/extensions/plugin-dev/hooks/hooks.json +3 -0
  46. package/extensions/plugin-dev/skills/agent-development/SKILL.md +641 -0
  47. package/extensions/plugin-dev/skills/agent-development/examples/agent-creation-prompt.md +250 -0
  48. package/extensions/plugin-dev/skills/agent-development/examples/complete-agent-examples.md +461 -0
  49. package/extensions/plugin-dev/skills/agent-development/references/advanced-agent-fields.md +246 -0
  50. package/extensions/plugin-dev/skills/agent-development/references/agent-creation-system-prompt.md +216 -0
  51. package/extensions/plugin-dev/skills/agent-development/references/permission-modes-rules.md +226 -0
  52. package/extensions/plugin-dev/skills/agent-development/references/system-prompt-design.md +464 -0
  53. package/extensions/plugin-dev/skills/agent-development/references/triggering-examples.md +474 -0
  54. package/extensions/plugin-dev/skills/agent-development/scripts/create-agent-skeleton.sh +176 -0
  55. package/extensions/plugin-dev/skills/agent-development/scripts/test-agent-trigger.sh +227 -0
  56. package/extensions/plugin-dev/skills/agent-development/scripts/validate-agent.sh +227 -0
  57. package/extensions/plugin-dev/skills/command-development/SKILL.md +763 -0
  58. package/extensions/plugin-dev/skills/command-development/examples/plugin-commands.md +612 -0
  59. package/extensions/plugin-dev/skills/command-development/examples/simple-commands.md +527 -0
  60. package/extensions/plugin-dev/skills/command-development/references/advanced-workflows.md +762 -0
  61. package/extensions/plugin-dev/skills/command-development/references/documentation-patterns.md +769 -0
  62. package/extensions/plugin-dev/skills/command-development/references/frontmatter-reference.md +508 -0
  63. package/extensions/plugin-dev/skills/command-development/references/interactive-commands.md +966 -0
  64. package/extensions/plugin-dev/skills/command-development/references/marketplace-considerations.md +943 -0
  65. package/extensions/plugin-dev/skills/command-development/references/plugin-features-reference.md +637 -0
  66. package/extensions/plugin-dev/skills/command-development/references/plugin-integration.md +191 -0
  67. package/extensions/plugin-dev/skills/command-development/references/skill-tool.md +447 -0
  68. package/extensions/plugin-dev/skills/command-development/references/testing-strategies.md +723 -0
  69. package/extensions/plugin-dev/skills/command-development/scripts/check-frontmatter.sh +234 -0
  70. package/extensions/plugin-dev/skills/command-development/scripts/validate-command.sh +160 -0
  71. package/extensions/plugin-dev/skills/hook-development/SKILL.md +861 -0
  72. package/extensions/plugin-dev/skills/hook-development/examples/load-context.sh +55 -0
  73. package/extensions/plugin-dev/skills/hook-development/examples/validate-bash.sh +57 -0
  74. package/extensions/plugin-dev/skills/hook-development/examples/validate-write.sh +48 -0
  75. package/extensions/plugin-dev/skills/hook-development/references/advanced.md +871 -0
  76. package/extensions/plugin-dev/skills/hook-development/references/hook-input-schemas.md +145 -0
  77. package/extensions/plugin-dev/skills/hook-development/references/migration.md +392 -0
  78. package/extensions/plugin-dev/skills/hook-development/references/patterns.md +430 -0
  79. package/extensions/plugin-dev/skills/hook-development/scripts/README.md +181 -0
  80. package/extensions/plugin-dev/skills/hook-development/scripts/hook-linter.sh +153 -0
  81. package/extensions/plugin-dev/skills/hook-development/scripts/test-hook.sh +276 -0
  82. package/extensions/plugin-dev/skills/hook-development/scripts/validate-hook-schema.sh +159 -0
  83. package/extensions/plugin-dev/skills/mcp-integration/SKILL.md +775 -0
  84. package/extensions/plugin-dev/skills/mcp-integration/examples/http-server.json +20 -0
  85. package/extensions/plugin-dev/skills/mcp-integration/examples/sse-server.json +19 -0
  86. package/extensions/plugin-dev/skills/mcp-integration/examples/stdio-server.json +38 -0
  87. package/extensions/plugin-dev/skills/mcp-integration/examples/ws-server.json +26 -0
  88. package/extensions/plugin-dev/skills/mcp-integration/references/authentication.md +601 -0
  89. package/extensions/plugin-dev/skills/mcp-integration/references/server-discovery.md +190 -0
  90. package/extensions/plugin-dev/skills/mcp-integration/references/server-types.md +572 -0
  91. package/extensions/plugin-dev/skills/mcp-integration/references/tool-usage.md +623 -0
  92. package/extensions/plugin-dev/skills/plugin-dev-guide/SKILL.md +222 -0
  93. package/extensions/plugin-dev/skills/plugin-structure/SKILL.md +705 -0
  94. package/extensions/plugin-dev/skills/plugin-structure/examples/advanced-plugin.md +774 -0
  95. package/extensions/plugin-dev/skills/plugin-structure/examples/minimal-plugin.md +83 -0
  96. package/extensions/plugin-dev/skills/plugin-structure/examples/standard-plugin.md +611 -0
  97. package/extensions/plugin-dev/skills/plugin-structure/references/advanced-topics.md +289 -0
  98. package/extensions/plugin-dev/skills/plugin-structure/references/component-patterns.md +592 -0
  99. package/extensions/plugin-dev/skills/plugin-structure/references/github-actions.md +233 -0
  100. package/extensions/plugin-dev/skills/plugin-structure/references/headless-ci-mode.md +193 -0
  101. package/extensions/plugin-dev/skills/plugin-structure/references/manifest-reference.md +625 -0
  102. package/extensions/plugin-dev/skills/plugin-structure/references/output-styles.md +116 -0
  103. package/extensions/plugin-dev/skills/skill-development/SKILL.md +564 -0
  104. package/extensions/plugin-dev/skills/skill-development/examples/complete-skill.md +465 -0
  105. package/extensions/plugin-dev/skills/skill-development/examples/frontmatter-templates.md +167 -0
  106. package/extensions/plugin-dev/skills/skill-development/examples/minimal-skill.md +111 -0
  107. package/extensions/plugin-dev/skills/skill-development/references/advanced-frontmatter.md +225 -0
  108. package/extensions/plugin-dev/skills/skill-development/references/commands-vs-skills.md +39 -0
  109. package/extensions/plugin-dev/skills/skill-development/references/skill-creation-workflow.md +379 -0
  110. package/extensions/plugin-dev/skills/skill-development/references/skill-creator-original.md +210 -0
  111. package/package.json +8 -11
  112. package/scripts/enact-extensions.mjs +751 -16
  113. package/scripts/hooks/session-start-drift-check.mjs +58 -0
  114. package/scripts/lib/build-index.mjs +50 -0
  115. package/scripts/lib/bundle-hash.mjs +137 -0
  116. package/scripts/lib/hooks.mjs +389 -0
  117. package/scripts/lib/ledger.mjs +162 -0
  118. package/scripts/lib/list-bundles.mjs +70 -0
  119. package/scripts/lib/outdated.mjs +144 -0
  120. package/scripts/lib/provision-mcp.mjs +369 -0
  121. package/scripts/lib/resolve-bundle.mjs +121 -0
  122. package/scripts/lib/run-install.mjs +321 -39
  123. package/scripts/lib/run-uninstall.mjs +220 -0
  124. package/scripts/lib/run-update.mjs +152 -0
  125. package/scripts/lib/run-validate.mjs +12 -18
  126. package/scripts/lib/serve.mjs +454 -0
  127. package/scripts/postinstall.mjs +63 -0
  128. package/scripts/setup-enact-context.sh +2 -2
  129. package/spec/index.json +59 -0
  130. package/web/assets/README.md +111 -0
  131. package/web/assets/logo-full.png +0 -0
  132. package/web/assets/logo-slim.png +0 -0
  133. package/web/assets/tokens/base.css +45 -0
  134. package/web/assets/tokens/colors.css +248 -0
  135. package/web/assets/tokens/effects.css +24 -0
  136. package/web/assets/tokens/fonts.css +8 -0
  137. package/web/assets/tokens/index.css +18 -0
  138. package/web/assets/tokens/spacing.css +50 -0
  139. package/web/index.html +1188 -0
  140. package/.agents/plugins/marketplace.json +0 -20
  141. package/catalog/enact-context.json +0 -9
  142. package/catalog/enact-factory.json +0 -7
  143. package/catalog/enact-operator.json +0 -7
  144. package/catalog/enact-wiki.json +0 -7
  145. package/catalog/net-revenue-management.json +0 -8
  146. package/scripts/rename-supervisor-to-operator.pl +0 -66
  147. package/scripts/sync-manifests.mjs +0 -23
  148. package/scripts/validate-catalog.mjs +0 -37
  149. package/scripts/validate-plugin.mjs +0 -10
  150. /package/{plugins → extensions}/net-revenue-management/.mcp.json +0 -0
  151. /package/{plugins → extensions}/net-revenue-management/skills/net-revenue-risks/SKILL.md +0 -0
  152. /package/{plugins → extensions}/net-revenue-management/skills/net-revenue-scenario/SKILL.md +0 -0
@@ -0,0 +1,58 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * session-start-drift-check.mjs — Enact Extensions session-start drift hook.
4
+ *
5
+ * Registered as a session-start hook for Claude, Codex, Cursor, and Enact.
6
+ * Checks whether any installed enact-extensions plugins have drifted from
7
+ * their canonical source (i.e. are outdated) and prints a concise advisory
8
+ * if so.
9
+ *
10
+ * Design constraints:
11
+ * - FAIL-SILENT: wrap everything in try/catch; never throw; always exit 0.
12
+ * - FAST: imports computeOutdated directly (no subprocess) for speed.
13
+ * - READ-ONLY: never writes any state.
14
+ * - NO OUTPUT on success (or error): only print when there are outdated entries.
15
+ * - DEPENDENCY-FREE: only node builtins + enact-extensions own modules.
16
+ */
17
+
18
+ import { dirname, resolve, join } from "node:path";
19
+ import { fileURLToPath } from "node:url";
20
+
21
+ const __dirname = dirname(fileURLToPath(import.meta.url));
22
+ // Navigate from scripts/hooks/ up to the repo root so we can import lib modules.
23
+ const REPO_ROOT = resolve(__dirname, "..", "..");
24
+
25
+ try {
26
+ // Dynamically import so any module-load failure is caught by the outer try/catch.
27
+ const { computeOutdated } = await import(join(REPO_ROOT, "scripts", "lib", "outdated.mjs"));
28
+
29
+ let entries;
30
+ try {
31
+ entries = computeOutdated();
32
+ } catch {
33
+ // computeOutdated threw (ledger missing, I/O error, etc.) — stay silent.
34
+ process.exit(0);
35
+ }
36
+
37
+ // Filter to only outdated entries (not fresh, not orphaned).
38
+ const outdated = (entries ?? []).filter((e) => e && e.status === "outdated");
39
+
40
+ if (outdated.length === 0) {
41
+ // Nothing to report.
42
+ process.exit(0);
43
+ }
44
+
45
+ // Build a concise list of (plugin, surface) pairs.
46
+ const pairs = outdated
47
+ .map((e) => `${e.name}/${e.platform}`)
48
+ .join(", ");
49
+
50
+ // Single advisory line to stdout.
51
+ process.stdout.write(
52
+ `[enact-extensions] ${outdated.length} plugin surface${outdated.length === 1 ? "" : "s"} outdated: ${pairs}. Run: enact-extensions update --all\n`,
53
+ );
54
+ } catch {
55
+ // Any top-level error (import failure, parse error, etc.) — silent exit 0.
56
+ }
57
+
58
+ process.exit(0);
@@ -0,0 +1,50 @@
1
+ import { relative } from "node:path";
2
+ import { listBundles } from "./list-bundles.mjs";
3
+
4
+ /**
5
+ * Build a discovery index of all plugin bundles.
6
+ *
7
+ * The index is a generated artifact — it is NEVER committed. The schema
8
+ * contract lives in spec/index.json.
9
+ *
10
+ * @param {string[]} roots - Absolute paths to scan for plugin bundles
11
+ * (passed directly to listBundles).
12
+ * @param {object} [opts]
13
+ * @param {string|null} [opts.now] - ISO timestamp to stamp on generatedAt.
14
+ * Pass a fixed value for deterministic tests.
15
+ * Defaults to null when omitted — this lib is
16
+ * pure and never calls new Date(). The CLI
17
+ * boundary computes the live timestamp and
18
+ * passes it in as `now`.
19
+ * @param {string} [opts.packageRoot] - Absolute package root used to compute
20
+ * repo-relative paths. Defaults to the
21
+ * directory two levels above this file.
22
+ * @returns {{ generatedAt: string|null, count: number, plugins: object[] }}
23
+ */
24
+ export function buildIndex(roots, opts = {}) {
25
+ const { now = null, packageRoot } = opts;
26
+
27
+ // Resolve package root: two levels up from this file (scripts/lib/ → repo root)
28
+ const pkgRoot =
29
+ packageRoot ??
30
+ new URL("../../", import.meta.url).pathname.replace(/\/$/, "");
31
+
32
+ // Source bundles via the canonical list-bundles helper (deduplicated, resilient).
33
+ const entries = listBundles(roots);
34
+
35
+ const plugins = entries.map((entry) => ({
36
+ name: entry.name,
37
+ version: entry.version,
38
+ category: entry.category,
39
+ description: entry.description,
40
+ targets: entry.targets,
41
+ // Make path repo-relative so the artifact is portable.
42
+ path: relative(pkgRoot, entry.path),
43
+ }));
44
+
45
+ return {
46
+ generatedAt: now,
47
+ count: plugins.length,
48
+ plugins,
49
+ };
50
+ }
@@ -0,0 +1,137 @@
1
+ /**
2
+ * bundle-hash.mjs — deterministic content hash for a plugin bundle.
3
+ *
4
+ * bundleHash(pluginRoot) → sha256 hex string
5
+ *
6
+ * The hash covers the CANONICAL bundle content only:
7
+ * - .agents/plugin.json (the manifest itself)
8
+ * - Component dirs/files referenced in the manifest:
9
+ * skills, mcpServers, apps, commands, hooks, agents, _agents, rules
10
+ * - .mcp.json (if present at pluginRoot root)
11
+ *
12
+ * EXCLUDED (generated / derived surfaces, never present in canonical source):
13
+ * - .claude-plugin/
14
+ * - .codex-plugin/
15
+ * - .cursor-plugin/
16
+ *
17
+ * Algorithm:
18
+ * 1. Collect the set of paths to include (manifest + referenced component paths).
19
+ * 2. Walk each path recursively; collect all (relativePath, bytes) pairs.
20
+ * 3. Sort by relative path (order-independence).
21
+ * 4. Feed path + bytes for each file into a single sha256 digest.
22
+ * 5. Return the hex digest.
23
+ *
24
+ * Pure, no writes, synchronous.
25
+ */
26
+
27
+ import { createHash } from "node:crypto";
28
+ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
29
+ import { join, relative } from "node:path";
30
+
31
+ // ---------------------------------------------------------------------------
32
+ // Canonical component field names (mirrors pathFieldsFromManifest in install.ts)
33
+ // ---------------------------------------------------------------------------
34
+ const COMPONENT_FIELDS = [
35
+ "skills",
36
+ "mcpServers",
37
+ "apps",
38
+ "commands",
39
+ "hooks",
40
+ "agents",
41
+ "_agents",
42
+ "rules",
43
+ ];
44
+
45
+ // Generated platform dirs — always excluded from the canonical hash.
46
+ const EXCLUDED_DIRS = new Set([".claude-plugin", ".codex-plugin", ".cursor-plugin"]);
47
+
48
+ /**
49
+ * Recursively collect all file entries under `root`.
50
+ * Returns an array of { rel: string, abs: string } sorted by rel.
51
+ * Silently skips non-file, non-directory entries.
52
+ *
53
+ * @param {string} abs - absolute path to walk
54
+ * @param {string} base - the pluginRoot (for computing relative paths)
55
+ * @param {string[]} acc - accumulator
56
+ */
57
+ function walkFiles(abs, base, acc) {
58
+ if (!existsSync(abs)) return;
59
+ const stat = statSync(abs);
60
+ if (stat.isFile()) {
61
+ acc.push(relative(base, abs));
62
+ return;
63
+ }
64
+ if (!stat.isDirectory()) return;
65
+ const entries = readdirSync(abs).sort(); // sort for determinism
66
+ for (const name of entries) {
67
+ walkFiles(join(abs, name), base, acc);
68
+ }
69
+ }
70
+
71
+ /**
72
+ * Collect the canonical set of relative paths to include in the hash.
73
+ *
74
+ * @param {string} pluginRoot
75
+ * @param {object} manifest - parsed .agents/plugin.json
76
+ * @returns {string[]} sorted relative paths (relative to pluginRoot)
77
+ */
78
+ function canonicalPaths(pluginRoot, manifest) {
79
+ // Always include the manifest itself.
80
+ const collected = [];
81
+
82
+ // 1. Always walk .agents/plugin.json (the manifest).
83
+ walkFiles(join(pluginRoot, ".agents", "plugin.json"), pluginRoot, collected);
84
+
85
+ // 2. Walk each component path that exists.
86
+ for (const field of COMPONENT_FIELDS) {
87
+ const raw = manifest[field];
88
+ if (typeof raw !== "string" || !raw.trim()) continue;
89
+ // Normalise: strip leading ./ and trailing /
90
+ const cleaned = raw.replace(/^\.\//, "").replace(/\/+$/, "");
91
+ if (!cleaned || cleaned.startsWith("../")) continue;
92
+ // Refuse to hash generated platform dirs even if the manifest references them.
93
+ const topSegment = cleaned.split("/")[0];
94
+ if (EXCLUDED_DIRS.has(topSegment)) continue;
95
+ walkFiles(join(pluginRoot, cleaned), pluginRoot, collected);
96
+ }
97
+
98
+ // 3. .mcp.json at pluginRoot root (included if present, regardless of manifest field)
99
+ walkFiles(join(pluginRoot, ".mcp.json"), pluginRoot, collected);
100
+
101
+ // Sort for order-independence, then deduplicate.
102
+ const unique = [...new Set(collected)].sort();
103
+ return unique;
104
+ }
105
+
106
+ /**
107
+ * Compute a deterministic sha256 hex hash of the canonical plugin bundle.
108
+ *
109
+ * @param {string} pluginRoot - absolute path to the bundle root
110
+ * @returns {string} sha256 hex digest (64 lowercase hex chars)
111
+ * @throws {Error} if the manifest cannot be read (ENOENT, parse error, etc.)
112
+ */
113
+ export function bundleHash(pluginRoot) {
114
+ // Read and parse the manifest first — this is the anchor; if it's missing
115
+ // we cannot know which component paths to include, so we throw.
116
+ const manifestPath = join(pluginRoot, ".agents", "plugin.json");
117
+ if (!existsSync(manifestPath)) {
118
+ throw new Error(`Cannot compute bundle hash: manifest not found at ${manifestPath}`);
119
+ }
120
+ const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
121
+
122
+ // Collect the sorted list of relative paths.
123
+ const paths = canonicalPaths(pluginRoot, manifest);
124
+
125
+ // Hash all paths and their contents in sorted order.
126
+ const hash = createHash("sha256");
127
+ for (const rel of paths) {
128
+ const abs = join(pluginRoot, rel);
129
+ // Feed the relative path as a NUL-terminated string to make path changes
130
+ // visible even when content is identical.
131
+ hash.update(rel + "\0", "utf8");
132
+ // Feed the file bytes.
133
+ hash.update(readFileSync(abs));
134
+ }
135
+
136
+ return hash.digest("hex");
137
+ }
@@ -0,0 +1,389 @@
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
+ /**
199
+ * Read config.toml content. Returns empty string on missing.
200
+ */
201
+ function readToml(configPath) {
202
+ if (!existsSync(configPath)) return "";
203
+ try {
204
+ return readFileSync(configPath, "utf8");
205
+ } catch {
206
+ return "";
207
+ }
208
+ }
209
+
210
+ /**
211
+ * Write config.toml (creates parent dir).
212
+ */
213
+ function writeToml(configPath, content) {
214
+ mkdirSync(dirname(configPath), { recursive: true });
215
+ writeFileSync(configPath, content, "utf8");
216
+ }
217
+
218
+ /**
219
+ * Register the drift-check hook in a codex/enact config.toml.
220
+ * Idempotent: checks for the BEGIN marker before appending.
221
+ *
222
+ * @param {string} configPath
223
+ * @returns {"registered"|"already_registered"} result
224
+ */
225
+ function registerTomlHook(configPath) {
226
+ const existing = readToml(configPath);
227
+ if (existing.includes(TOML_BEGIN_MARKER)) return "already_registered";
228
+
229
+ const block = TOML_HOOK_BLOCK(DRIFT_CHECK_SCRIPT);
230
+ writeToml(configPath, existing + block);
231
+ return "registered";
232
+ }
233
+
234
+ /**
235
+ * Remove the drift-check hook block from config.toml.
236
+ * Removes the content between BEGIN and END markers (inclusive).
237
+ *
238
+ * @param {string} configPath
239
+ * @returns {"removed"|"not_found"} result
240
+ */
241
+ function removeTomlHook(configPath) {
242
+ const existing = readToml(configPath);
243
+ if (!existing.includes(TOML_BEGIN_MARKER)) return "not_found";
244
+
245
+ // Remove the marked block (BEGIN line through END line, inclusive).
246
+ const beginIdx = existing.indexOf(TOML_BEGIN_MARKER);
247
+ const endIdx = existing.indexOf(TOML_END_MARKER);
248
+ if (beginIdx === -1 || endIdx === -1) return "not_found";
249
+
250
+ // Include the newline before BEGIN and after END.
251
+ const beforeBegin = existing.slice(0, beginIdx).replace(/\n$/, "");
252
+ const afterEnd = existing.slice(endIdx + TOML_END_MARKER.length).replace(/^\n/, "");
253
+ const newContent = beforeBegin + (afterEnd ? "\n" + afterEnd : "");
254
+
255
+ writeToml(configPath, newContent);
256
+ return "removed";
257
+ }
258
+
259
+ // ---------------------------------------------------------------------------
260
+ // Public API: registerHook / removeHook for each surface
261
+ // ---------------------------------------------------------------------------
262
+
263
+ /**
264
+ * Register the session-start drift-check hook for a given surface.
265
+ *
266
+ * @param {string} surface — "claude" | "codex" | "cursor" | "enact"
267
+ * @param {object} [opts]
268
+ * @param {string} [opts.claudeHome]
269
+ * @param {string} [opts.codexHome]
270
+ * @param {string} [opts.cursorHome]
271
+ * @param {string} [opts.enactHome]
272
+ * @param {boolean} [opts.local] — if true, use project-local home under cwd
273
+ * @param {string} [opts.cwd] — cwd for --local resolution
274
+ * @returns {{ surface: string, result: "registered"|"already_registered"|"skipped", note?: string }}
275
+ */
276
+ export function registerHook(surface, opts = {}) {
277
+ try {
278
+ switch (surface) {
279
+ case "claude": {
280
+ const home = opts.claudeHome ?? (opts.local ? join(opts.cwd ?? process.cwd(), ".claude") : defaultHome("claude"));
281
+ const settingsPath = join(home, "settings.json");
282
+ const result = registerClaudeHook(settingsPath);
283
+ return { surface, result, location: settingsPath };
284
+ }
285
+
286
+ case "cursor": {
287
+ // Cursor mirrors Claude's settings.json approach.
288
+ // NOTE: Cursor's actual hook schema may differ — this uses the same
289
+ // SessionStart format as Claude. Reverse with --remove if unwanted.
290
+ const home = opts.cursorHome ?? (opts.local ? join(opts.cwd ?? process.cwd(), ".cursor") : defaultHome("cursor"));
291
+ const settingsPath = join(home, "settings.json");
292
+ const result = registerClaudeHook(settingsPath);
293
+ return {
294
+ surface,
295
+ result,
296
+ location: settingsPath,
297
+ note: "Assumption: Cursor uses settings.json SessionStart hooks (same as Claude Code). Verify if Cursor's actual hook schema differs.",
298
+ };
299
+ }
300
+
301
+ case "codex": {
302
+ const home = opts.codexHome ?? (opts.local ? join(opts.cwd ?? process.cwd(), ".codex") : defaultHome("codex"));
303
+ const configPath = join(home, "config.toml");
304
+ const result = registerTomlHook(configPath);
305
+ return {
306
+ surface,
307
+ result,
308
+ location: configPath,
309
+ note: "Assumption: codex config.toml supports [[hooks.session_start]] with a command field.",
310
+ };
311
+ }
312
+
313
+ case "enact": {
314
+ const home = opts.enactHome ?? (opts.local ? join(opts.cwd ?? process.cwd(), ".enact", "agent") : defaultHome("enact"));
315
+ const configPath = join(home, "config.toml");
316
+ const result = registerTomlHook(configPath);
317
+ return {
318
+ surface,
319
+ result,
320
+ location: configPath,
321
+ note: "Assumption: enact-agent config.toml supports [[hooks.session_start]] with a command field.",
322
+ };
323
+ }
324
+
325
+ default:
326
+ return { surface, result: "skipped", note: `Unknown surface: ${surface}` };
327
+ }
328
+ } catch (err) {
329
+ // Never crash — return skipped with the error note.
330
+ return {
331
+ surface,
332
+ result: "skipped",
333
+ note: `Error: ${err instanceof Error ? err.message : String(err)}`,
334
+ };
335
+ }
336
+ }
337
+
338
+ /**
339
+ * Remove the session-start drift-check hook for a given surface.
340
+ *
341
+ * @param {string} surface
342
+ * @param {object} [opts] — same as registerHook
343
+ * @returns {{ surface: string, result: "removed"|"not_found"|"skipped", note?: string }}
344
+ */
345
+ export function removeHook(surface, opts = {}) {
346
+ try {
347
+ switch (surface) {
348
+ case "claude": {
349
+ const home = opts.claudeHome ?? (opts.local ? join(opts.cwd ?? process.cwd(), ".claude") : defaultHome("claude"));
350
+ const settingsPath = join(home, "settings.json");
351
+ const result = removeClaudeHook(settingsPath);
352
+ return { surface, result, location: settingsPath };
353
+ }
354
+
355
+ case "cursor": {
356
+ const home = opts.cursorHome ?? (opts.local ? join(opts.cwd ?? process.cwd(), ".cursor") : defaultHome("cursor"));
357
+ const settingsPath = join(home, "settings.json");
358
+ const result = removeClaudeHook(settingsPath);
359
+ return { surface, result, location: settingsPath };
360
+ }
361
+
362
+ case "codex": {
363
+ const home = opts.codexHome ?? (opts.local ? join(opts.cwd ?? process.cwd(), ".codex") : defaultHome("codex"));
364
+ const configPath = join(home, "config.toml");
365
+ const result = removeTomlHook(configPath);
366
+ return { surface, result, location: configPath };
367
+ }
368
+
369
+ case "enact": {
370
+ const home = opts.enactHome ?? (opts.local ? join(opts.cwd ?? process.cwd(), ".enact", "agent") : defaultHome("enact"));
371
+ const configPath = join(home, "config.toml");
372
+ const result = removeTomlHook(configPath);
373
+ return { surface, result, location: configPath };
374
+ }
375
+
376
+ default:
377
+ return { surface, result: "skipped", note: `Unknown surface: ${surface}` };
378
+ }
379
+ } catch (err) {
380
+ return {
381
+ surface,
382
+ result: "skipped",
383
+ note: `Error: ${err instanceof Error ? err.message : String(err)}`,
384
+ };
385
+ }
386
+ }
387
+
388
+ // The 4 supported surfaces.
389
+ export const SUPPORTED_SURFACES = ["claude", "codex", "cursor", "enact"];