@amsterdamdatalabs/enact-extensions 0.1.1 → 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 +4 -3
- 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 +82 -1
- package/dist/install.d.ts.map +1 -1
- package/dist/install.js +187 -35
- 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 +23 -0
- package/dist/validate/index.d.ts.map +1 -1
- package/dist/validate/index.js +80 -0
- package/dist/validate/index.js.map +1 -1
- 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/package.json +1 -1
- package/scripts/enact-extensions.mjs +79 -12
- package/scripts/lib/hooks.mjs +352 -0
- package/scripts/lib/ledger.mjs +4 -3
- package/scripts/lib/provision-mcp.mjs +12 -365
- package/scripts/lib/run-install.mjs +87 -5
- package/scripts/lib/run-prune.mjs +73 -0
- package/scripts/lib/run-sync.mjs +9 -1
- package/scripts/lib/run-uninstall.mjs +26 -2
- package/scripts/lib/run-validate.mjs +10 -1
- package/scripts/lib/serve.mjs +19 -1
- package/scripts/version-bump.sh +463 -0
- package/spec/codex.json +1 -11
|
@@ -7,10 +7,11 @@ import { runValidate } from "./lib/run-validate.mjs";
|
|
|
7
7
|
import { runSync } from "./lib/run-sync.mjs";
|
|
8
8
|
import { runInstall } from "./lib/run-install.mjs";
|
|
9
9
|
import { runUpdate } from "./lib/run-update.mjs";
|
|
10
|
+
import { runPrune } from "./lib/run-prune.mjs";
|
|
10
11
|
import { runUninstall } from "./lib/run-uninstall.mjs";
|
|
11
12
|
import { readLedger } from "./lib/ledger.mjs";
|
|
12
13
|
import { computeOutdated } from "./lib/outdated.mjs";
|
|
13
|
-
import { startServer } from "./lib/serve.mjs";
|
|
14
|
+
import { startServer, resolveServePort } from "./lib/serve.mjs";
|
|
14
15
|
import { registerHook, removeHook, SUPPORTED_SURFACES } from "./lib/hooks.mjs";
|
|
15
16
|
import { dirname, join, resolve } from "node:path";
|
|
16
17
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
@@ -21,8 +22,12 @@ import { Ajv } from "ajv";
|
|
|
21
22
|
import addFormatsModule from "ajv-formats";
|
|
22
23
|
|
|
23
24
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
25
|
+
const PACKAGE_ROOT = resolve(__dirname, "..");
|
|
26
|
+
const PACKAGE_JSON = JSON.parse(readFileSync(join(PACKAGE_ROOT, "package.json"), "utf8"));
|
|
27
|
+
const VERSION = PACKAGE_JSON.version ?? "unknown";
|
|
24
28
|
|
|
25
29
|
const HELP = `enact-extensions — Enact multi-platform plugin manifests
|
|
30
|
+
Version: ${VERSION}
|
|
26
31
|
|
|
27
32
|
Usage:
|
|
28
33
|
enact-extensions list [dir] List available plugin bundles (default: bundled + cwd/extensions/)
|
|
@@ -35,6 +40,9 @@ Usage:
|
|
|
35
40
|
enact-extensions update --all Refresh EVERY outdated plugin/surface
|
|
36
41
|
enact-extensions update <name> --dry-run Show what would be refreshed; change nothing
|
|
37
42
|
enact-extensions update --all --dry-run Show every outdated surface that would be refreshed
|
|
43
|
+
enact-extensions prune --platform all Remove ledger-installed plugin surfaces whose local source bundle is gone
|
|
44
|
+
enact-extensions prune --platform shared Remove orphaned shared skill drops (explicit only)
|
|
45
|
+
enact-extensions prune --dry-run Show orphaned surfaces that would be removed
|
|
38
46
|
enact-extensions index [--out <path>] Generate a discovery index of all bundles (generated artifact, never committed)
|
|
39
47
|
enact-extensions index --out - Print the discovery index JSON to stdout
|
|
40
48
|
enact-extensions index --stdout Alias for --out -
|
|
@@ -47,7 +55,7 @@ Usage:
|
|
|
47
55
|
enact-extensions install [path|name] --platform enact Install to Enact (Codex fork)
|
|
48
56
|
enact-extensions install [path|name] --platform codex Install to Codex (explicit)
|
|
49
57
|
enact-extensions install [path|name] --platform shared Install skills to host-neutral .agents/skills/
|
|
50
|
-
enact-extensions install [path|name] --platform all Install to every platform (codex, claude, cursor, enact
|
|
58
|
+
enact-extensions install [path|name] --platform all Install to every plugin platform (codex, claude, cursor, enact)
|
|
51
59
|
enact-extensions install [path|name] --platform claude,cursor Install to a subset of platforms
|
|
52
60
|
enact-extensions install [path|name] --global Install into the default agent home (default)
|
|
53
61
|
enact-extensions install [path|name] --local Install into a project home under cwd (./.codex, ...)
|
|
@@ -57,9 +65,9 @@ Usage:
|
|
|
57
65
|
enact-extensions uninstall [path|name] --platform enact Uninstall from Enact (Codex fork)
|
|
58
66
|
enact-extensions uninstall [path|name] --platform codex Uninstall from Codex (explicit)
|
|
59
67
|
enact-extensions uninstall [path|name] --platform shared Remove skills from host-neutral .agents/skills/
|
|
60
|
-
enact-extensions uninstall [path|name] --platform all Uninstall from every platform
|
|
68
|
+
enact-extensions uninstall [path|name] --platform all Uninstall from every plugin platform
|
|
61
69
|
enact-extensions uninstall [path|name] --platform claude,cursor Uninstall from a subset of platforms
|
|
62
|
-
enact-extensions serve [--port
|
|
70
|
+
enact-extensions serve [--port N] [--prod] [--host 127.0.0.1] [--open] Start the localhost management UI + API (dev 43217 / prod 53217)
|
|
63
71
|
enact-extensions hooks [setup] [--surfaces <list>|--all] [--remove] [--local]
|
|
64
72
|
Register (or remove) the session-start drift-check hook
|
|
65
73
|
for each named surface (claude, codex, cursor, enact).
|
|
@@ -71,6 +79,7 @@ Hooks command:
|
|
|
71
79
|
enact-extensions hooks setup --all --remove Remove the drift hook from all surfaces
|
|
72
80
|
enact-extensions hooks setup --surfaces claude --local Register using project-local home (under cwd)
|
|
73
81
|
enact-extensions hooks setup Interactive surface picker (TTY only)
|
|
82
|
+
enact-extensions -v | --version Print version and exit
|
|
74
83
|
|
|
75
84
|
Options:
|
|
76
85
|
path|name Plugin root path, or a bare plugin name resolved from bundled extensions.
|
|
@@ -80,12 +89,12 @@ Options:
|
|
|
80
89
|
--stdout (index only) Alias for --out -.
|
|
81
90
|
--json (list only) Emit a JSON array to stdout instead of a human table.
|
|
82
91
|
--platform <name> Target platform: codex (default), claude, cursor, enact, shared.
|
|
83
|
-
Use "all" to install to every platform, or a comma-separated list
|
|
92
|
+
Use "all" to install to every plugin platform, or a comma-separated list
|
|
84
93
|
(e.g. "claude,cursor") to install to a named subset.
|
|
85
94
|
--global Install into the agent's default home (default)
|
|
86
95
|
--local Install into a project-scoped home under the current dir
|
|
87
96
|
--codex-home <path> Install only into the given Codex-compatible home
|
|
88
|
-
--enact-home <path> Target a specific local Enact home (default: ~/.enact)
|
|
97
|
+
--enact-home <path> Target a specific local Enact home (default: ~/.enact/agent)
|
|
89
98
|
--claude-home <path> Target a specific local Claude home (default: ~/.claude)
|
|
90
99
|
--cursor-home <path> Target a specific local Cursor home (default: ~/.cursor)
|
|
91
100
|
--shared-home <path> Base dir for shared install (skills land at <path>/.agents/skills/)
|
|
@@ -105,7 +114,7 @@ Name resolution (bare plugin names):
|
|
|
105
114
|
3. <npm-global-root>/@amsterdamdatalabs/enact-extensions/extensions/<name>
|
|
106
115
|
|
|
107
116
|
Multi-platform notes:
|
|
108
|
-
- Platforms are installed sequentially in deterministic order: codex, claude, cursor, enact
|
|
117
|
+
- Platforms are installed sequentially in deterministic order: codex, claude, cursor, enact.
|
|
109
118
|
- Per-platform --<platform>-home flags apply to their respective platform in a multi-target run.
|
|
110
119
|
- If any platform fails, the others still proceed; a non-zero exit is returned and the summary
|
|
111
120
|
of succeeded/failed platforms is printed to stderr.
|
|
@@ -118,13 +127,13 @@ Examples:
|
|
|
118
127
|
enact-extensions index --out - Print discovery index JSON to stdout (pipe-friendly)
|
|
119
128
|
enact-extensions index --stdout Same as --out -
|
|
120
129
|
enact-extensions index --out /tmp/my-index.json
|
|
121
|
-
cd
|
|
130
|
+
cd extensions/enact-factory && enact-extensions validate
|
|
122
131
|
enact-extensions sync . --name my-plugin
|
|
123
132
|
enact-extensions install net-revenue-management --platform enact --global
|
|
124
133
|
enact-extensions install net-revenue-management --platform enact --local
|
|
125
134
|
enact-extensions install extensions/net-revenue-management --platform enact --global
|
|
126
|
-
enact-extensions install
|
|
127
|
-
enact-extensions install
|
|
135
|
+
enact-extensions install enact-factory --platform claude
|
|
136
|
+
enact-extensions install enact-factory --platform cursor
|
|
128
137
|
enact-extensions install net-revenue-management --platform shared
|
|
129
138
|
enact-extensions install net-revenue-management --platform shared --local
|
|
130
139
|
enact-extensions install net-revenue-management --platform all
|
|
@@ -182,6 +191,8 @@ function parseArgs(argv) {
|
|
|
182
191
|
options.host = argv[++i];
|
|
183
192
|
} else if (arg === "--open") {
|
|
184
193
|
options.open = true;
|
|
194
|
+
} else if (arg === "--prod") {
|
|
195
|
+
options.prod = true;
|
|
185
196
|
} else if (arg === "--surfaces" && argv[i + 1]) {
|
|
186
197
|
// Comma-separated list of surfaces: claude,codex,cursor,enact
|
|
187
198
|
options.surfaces = argv[++i].split(",").map((s) => s.trim()).filter(Boolean);
|
|
@@ -197,7 +208,13 @@ function parseArgs(argv) {
|
|
|
197
208
|
return { command: positional[0], path: positional[1], options };
|
|
198
209
|
}
|
|
199
210
|
|
|
200
|
-
const
|
|
211
|
+
const rawArgv = process.argv.slice(2);
|
|
212
|
+
if (rawArgv.length === 1 && (rawArgv[0] === "-v" || rawArgv[0] === "--version")) {
|
|
213
|
+
console.log(`enact-extensions ${VERSION}`);
|
|
214
|
+
process.exit(0);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const { command, path, options } = parseArgs(rawArgv);
|
|
201
218
|
|
|
202
219
|
if (!command || command === "help") {
|
|
203
220
|
console.log(HELP);
|
|
@@ -667,13 +684,63 @@ if (command === "update") {
|
|
|
667
684
|
process.exit(summary.failed.length > 0 ? 1 : 0);
|
|
668
685
|
}
|
|
669
686
|
|
|
687
|
+
// ---------------------------------------------------------------------------
|
|
688
|
+
// `prune` command — remove orphaned installed surfaces, driven by the ledger.
|
|
689
|
+
//
|
|
690
|
+
// A surface is pruned only when:
|
|
691
|
+
// - the ledger still says it is installed,
|
|
692
|
+
// - it belongs to the requested marketplace,
|
|
693
|
+
// - its platform is in scope, and
|
|
694
|
+
// - no local source bundle exists for that plugin name.
|
|
695
|
+
// ---------------------------------------------------------------------------
|
|
696
|
+
if (command === "prune") {
|
|
697
|
+
let summary;
|
|
698
|
+
try {
|
|
699
|
+
summary = runPrune({
|
|
700
|
+
platform: options.platform ?? "all",
|
|
701
|
+
dryRun: options.dryRun,
|
|
702
|
+
ledgerHome: homedir(),
|
|
703
|
+
cwd: processCwd(),
|
|
704
|
+
marketplaceName: options.marketplaceName,
|
|
705
|
+
});
|
|
706
|
+
} catch (err) {
|
|
707
|
+
process.stderr.write(
|
|
708
|
+
`[enact-extensions prune] ${err instanceof Error ? err.message : String(err)}\n`,
|
|
709
|
+
);
|
|
710
|
+
process.exit(1);
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
if (options.json) {
|
|
714
|
+
process.stdout.write(JSON.stringify(summary, null, 2) + "\n");
|
|
715
|
+
process.exit(summary.failed.length > 0 ? 1 : 0);
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
const rows = summary.dryRun ? summary.candidates : summary.pruned;
|
|
719
|
+
const verb = summary.dryRun ? "would prune" : "pruned";
|
|
720
|
+
if (rows.length === 0) {
|
|
721
|
+
process.stderr.write("Nothing to prune — no orphaned installed surfaces in scope.\n");
|
|
722
|
+
}
|
|
723
|
+
for (const entry of rows) {
|
|
724
|
+
console.log(`${verb}: ${entry.name} [${entry.platform}] @ ${entry.home}`);
|
|
725
|
+
}
|
|
726
|
+
for (const f of summary.failed) {
|
|
727
|
+
process.stderr.write(
|
|
728
|
+
`failed: ${f.entry.name} [${f.entry.platform}] @ ${f.entry.home}: ${f.error.message}\n`,
|
|
729
|
+
);
|
|
730
|
+
}
|
|
731
|
+
process.stderr.write(
|
|
732
|
+
`${rows.length} ${summary.dryRun ? "would be pruned" : "pruned"}, ${summary.failed.length} failed.\n`,
|
|
733
|
+
);
|
|
734
|
+
process.exit(summary.failed.length > 0 ? 1 : 0);
|
|
735
|
+
}
|
|
736
|
+
|
|
670
737
|
// ---------------------------------------------------------------------------
|
|
671
738
|
// `serve` command — localhost API + static-file server (no pluginRoot needed).
|
|
672
739
|
// Handled here (before resolveRoot) because it is async + long-running.
|
|
673
740
|
// ---------------------------------------------------------------------------
|
|
674
741
|
if (command === "serve") {
|
|
675
742
|
const PACKAGE_ROOT = resolve(__dirname, "..");
|
|
676
|
-
const port = options
|
|
743
|
+
const port = resolveServePort(options);
|
|
677
744
|
const host = options.host ?? "127.0.0.1";
|
|
678
745
|
|
|
679
746
|
if (host !== "127.0.0.1" && host !== "localhost" && host !== "::1") {
|
package/scripts/lib/hooks.mjs
CHANGED
|
@@ -195,6 +195,264 @@ command = "node ${scriptPath}"
|
|
|
195
195
|
const TOML_BEGIN_MARKER = `# [${TOML_HOOK_MARKER}] BEGIN`;
|
|
196
196
|
const TOML_END_MARKER = `# [${TOML_HOOK_MARKER}] END`;
|
|
197
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
|
+
|
|
198
456
|
/**
|
|
199
457
|
* Read config.toml content. Returns empty string on missing.
|
|
200
458
|
*/
|
|
@@ -385,5 +643,99 @@ export function removeHook(surface, opts = {}) {
|
|
|
385
643
|
}
|
|
386
644
|
}
|
|
387
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
|
+
|
|
388
740
|
// The 4 supported surfaces.
|
|
389
741
|
export const SUPPORTED_SURFACES = ["claude", "codex", "cursor", "enact"];
|
package/scripts/lib/ledger.mjs
CHANGED
|
@@ -13,7 +13,7 @@ import { dirname, join } from "node:path";
|
|
|
13
13
|
* to this single global ledger. The `home` override exists so tests never touch
|
|
14
14
|
* the real ~/.enact.
|
|
15
15
|
*
|
|
16
|
-
* Entry shape: { ts, action, name, version, platform, scope, home, path }
|
|
16
|
+
* Entry shape: { ts, action, name, version, platform, scope, home, path, marketplaceName }
|
|
17
17
|
*
|
|
18
18
|
* All file I/O is best-effort: failures throw a catchable error so callers
|
|
19
19
|
* (the install path) can swallow + warn rather than aborting an install.
|
|
@@ -127,13 +127,13 @@ export function findInstalled(name, { home } = {}) {
|
|
|
127
127
|
* still-installed targets across all plugin names.
|
|
128
128
|
*
|
|
129
129
|
* Each returned entry carries the full ledger fields for the installed surface:
|
|
130
|
-
* { name, version, platform, scope, home, path, hash }
|
|
130
|
+
* { name, version, platform, scope, home, path, hash, marketplaceName }
|
|
131
131
|
*
|
|
132
132
|
* A target is keyed by name+platform+scope+home+path so the same plugin installed
|
|
133
133
|
* to multiple platforms/homes/names is tracked independently.
|
|
134
134
|
*
|
|
135
135
|
* @param {object} [opts] - { home }
|
|
136
|
-
* @returns {{ name: string, version: string, platform: string, scope: string, home: string, path: string, hash: string|null }[]}
|
|
136
|
+
* @returns {{ name: string, version: string, platform: string, scope: string, home: string, path: string, hash: string|null, marketplaceName: string|null }[]}
|
|
137
137
|
*/
|
|
138
138
|
export function listInstalled({ home } = {}) {
|
|
139
139
|
const all = readLedger({ home });
|
|
@@ -153,6 +153,7 @@ export function listInstalled({ home } = {}) {
|
|
|
153
153
|
home: e.home,
|
|
154
154
|
path: e.path,
|
|
155
155
|
hash: e.hash ?? null,
|
|
156
|
+
marketplaceName: e.marketplaceName ?? null,
|
|
156
157
|
});
|
|
157
158
|
} else if (e.action === "uninstall") {
|
|
158
159
|
state.delete(key);
|