agent-gate-installer 1.0.0

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 (3) hide show
  1. package/README.md +77 -0
  2. package/bin/install.mjs +351 -0
  3. package/package.json +23 -0
package/README.md ADDED
@@ -0,0 +1,77 @@
1
+ # agent-gate-installer
2
+
3
+ One-command setup for the [agent-gate](https://github.com/jl-cmd/agent-gate) prompt evaluation gate for Claude Code.
4
+
5
+ ## Prerequisites
6
+
7
+ - Node.js 18+
8
+ - Python 3.12+
9
+ - git
10
+ - A GitHub personal access token with access to `jl-cmd/agent-gate`
11
+
12
+ ## Install
13
+
14
+ ```bash
15
+ GH_TOKEN=github_pat_XXXXX npx agent-gate-installer
16
+ ```
17
+
18
+ Or set `GH_TOKEN` in your environment first:
19
+
20
+ ```bash
21
+ export GH_TOKEN=github_pat_XXXXX
22
+ npx agent-gate-installer
23
+ ```
24
+
25
+ Without `GH_TOKEN`, the installer prompts interactively.
26
+
27
+ ### What it does
28
+
29
+ 1. Detects Python 3.12+ (tries `python3`, `python`, `py -3`)
30
+ 2. Clones `jl-cmd/agent-gate` to `~/.claude/agent-gate/`
31
+ 3. Creates a Python venv at `~/.claude/agent-gate/.venv/`
32
+ 4. Installs all agent-gate sub-packages in editable mode
33
+ 5. Merges 3 hooks into `~/.claude/settings.json` (idempotent)
34
+ 6. Registers the MCP server in `~/.claude/settings.json`
35
+ 7. Verifies the installation
36
+
37
+ Restart Claude Code after installing.
38
+
39
+ ## Update
40
+
41
+ ```bash
42
+ npx agent-gate-installer --update
43
+ ```
44
+
45
+ Pulls the latest code and reinstalls packages.
46
+
47
+ ## Uninstall
48
+
49
+ ```bash
50
+ npx agent-gate-installer --uninstall
51
+ ```
52
+
53
+ Removes `~/.claude/agent-gate/`, the hooks, and the MCP server entry from settings.json.
54
+
55
+ ## Flags
56
+
57
+ | Flag | Description |
58
+ |------|-------------|
59
+ | `--verbose` | Show detailed output from each step |
60
+ | `--non-interactive` | Fail if GH_TOKEN is missing (for CI/automation) |
61
+ | `--update` | Pull latest and reinstall packages |
62
+ | `--uninstall` | Remove agent-gate completely |
63
+ | `--help` | Show usage information |
64
+
65
+ ## Cloud setup script
66
+
67
+ For Claude Code on the web environments:
68
+
69
+ ```bash
70
+ GH_TOKEN="${GH_TOKEN}" npx agent-gate-installer --non-interactive --verbose
71
+ ```
72
+
73
+ Set `GH_TOKEN` as an environment variable in your cloud environment settings.
74
+
75
+ ## Cross-platform
76
+
77
+ Works on Linux, macOS, and Windows. Python detection and venv paths adapt automatically.
@@ -0,0 +1,351 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { execSync } from "node:child_process";
4
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, rmSync } from "node:fs";
5
+ import { createInterface } from "node:readline";
6
+ import { join } from "node:path";
7
+ import { homedir, platform } from "node:os";
8
+
9
+ const CLAUDE_HOME = join(homedir(), ".claude");
10
+ const INSTALL_DIR = join(CLAUDE_HOME, "agent-gate");
11
+ const SETTINGS_PATH = join(CLAUDE_HOME, "settings.json");
12
+ const REPO_URL = "https://github.com/jl-cmd/agent-gate.git";
13
+ const MINIMUM_PYTHON_VERSION = [3, 12];
14
+ const IS_WINDOWS = platform() === "win32";
15
+
16
+ const HOOK_FILENAMES = ["gate_enforcer.py", "gate_trigger.py", "session_cleanup.py"];
17
+ const HOOK_EVENTS = [
18
+ { event: "PreToolUse", matcher: "Read|Write|Edit|Bash|Glob|Grep|Agent|Task", filename: "gate_enforcer.py" },
19
+ { event: "UserPromptSubmit", matcher: "", filename: "gate_trigger.py" },
20
+ { event: "SessionStart", matcher: null, filename: "session_cleanup.py" },
21
+ ];
22
+
23
+ const flags = new Set(process.argv.slice(2));
24
+ const verbose = flags.has("--verbose");
25
+ const nonInteractive = flags.has("--non-interactive");
26
+
27
+ function log(message) {
28
+ console.log(message);
29
+ }
30
+
31
+ function logVerbose(message) {
32
+ if (verbose) console.log(message);
33
+ }
34
+
35
+ function run(command, options = {}) {
36
+ const stdio = verbose ? "inherit" : "pipe";
37
+ return execSync(command, { encoding: "utf8", stdio, ...options });
38
+ }
39
+
40
+ function detectPython() {
41
+ const candidates = IS_WINDOWS
42
+ ? ["python", "python3", "py -3"]
43
+ : ["python3", "python", "py -3"];
44
+
45
+ for (const candidate of candidates) {
46
+ try {
47
+ const output = execSync(`${candidate} --version`, { encoding: "utf8", stdio: "pipe" });
48
+ if (!output.includes("Python 3")) continue;
49
+ const match = output.match(/Python (\d+)\.(\d+)/);
50
+ if (!match) continue;
51
+ const major = parseInt(match[1], 10);
52
+ const minor = parseInt(match[2], 10);
53
+ if (major < MINIMUM_PYTHON_VERSION[0]) continue;
54
+ if (major === MINIMUM_PYTHON_VERSION[0] && minor < MINIMUM_PYTHON_VERSION[1]) continue;
55
+ return { command: candidate.split(" ")[0], args: candidate.includes(" ") ? candidate.split(" ").slice(1) : [], version: `${major}.${minor}`, full: candidate };
56
+ } catch {
57
+ continue;
58
+ }
59
+ }
60
+ log("Error: Python 3.12+ is required but not found.");
61
+ log("Install from https://www.python.org/downloads/");
62
+ process.exit(1);
63
+ }
64
+
65
+ function getVenvPython(venvDirectory) {
66
+ return IS_WINDOWS
67
+ ? join(venvDirectory, "Scripts", "python.exe")
68
+ : join(venvDirectory, "bin", "python");
69
+ }
70
+
71
+ function readSettings() {
72
+ if (!existsSync(SETTINGS_PATH)) return {};
73
+ const raw = readFileSync(SETTINGS_PATH, "utf8");
74
+ try {
75
+ return JSON.parse(raw);
76
+ } catch {
77
+ log(`Error: ${SETTINGS_PATH} contains malformed JSON. Fix it manually before running the installer.`);
78
+ process.exit(1);
79
+ }
80
+ }
81
+
82
+ function writeSettings(settings) {
83
+ writeFileSync(SETTINGS_PATH, JSON.stringify(settings, null, 4) + "\n", "utf8");
84
+ }
85
+
86
+ function hookCommandContains(hookEntry, filename) {
87
+ return hookEntry.hooks?.some((each_hook) => each_hook.command?.includes(filename));
88
+ }
89
+
90
+ function mergeHooks(settings, venvPython) {
91
+ settings.hooks = settings.hooks || {};
92
+ let updatedCount = 0;
93
+ let addedCount = 0;
94
+
95
+ for (const { event, matcher, filename } of HOOK_EVENTS) {
96
+ const hookCommand = `"${venvPython}" "${join(INSTALL_DIR, "hooks", filename)}"`;
97
+ const newEntry = { type: "command", command: hookCommand, timeout: 10000 };
98
+ const newGroup = matcher !== null
99
+ ? { matcher, hooks: [newEntry] }
100
+ : { hooks: [newEntry] };
101
+
102
+ settings.hooks[event] = settings.hooks[event] || [];
103
+ const existingIndex = settings.hooks[event].findIndex((group) => hookCommandContains(group, filename));
104
+
105
+ if (existingIndex >= 0) {
106
+ const existingGroup = settings.hooks[event][existingIndex];
107
+ const hookIndex = existingGroup.hooks.findIndex((each_hook) => each_hook.command?.includes(filename));
108
+ if (hookIndex >= 0) {
109
+ existingGroup.hooks[hookIndex] = newEntry;
110
+ }
111
+ updatedCount++;
112
+ } else {
113
+ settings.hooks[event].push(newGroup);
114
+ addedCount++;
115
+ }
116
+ }
117
+
118
+ if (updatedCount > 0 && addedCount === 0) {
119
+ log(" ~ Updated 3 existing hooks in settings.json");
120
+ } else {
121
+ log(` + Merged ${addedCount + updatedCount} hooks into settings.json`);
122
+ }
123
+ }
124
+
125
+ function registerMcpServer(settings, venvPython) {
126
+ settings.mcpServers = settings.mcpServers || {};
127
+ settings.mcpServers["agent-gate"] = {
128
+ type: "stdio",
129
+ command: venvPython,
130
+ args: [join(INSTALL_DIR, "src", "agent_gate", "server.py")],
131
+ };
132
+ log(" + Registered agent-gate MCP server");
133
+ }
134
+
135
+ async function promptForToken() {
136
+ const readline = createInterface({ input: process.stdin, output: process.stdout });
137
+ return new Promise((resolve) => {
138
+ readline.question("Enter your GitHub token (GH_TOKEN): ", (answer) => {
139
+ readline.close();
140
+ resolve(answer.trim());
141
+ });
142
+ });
143
+ }
144
+
145
+ async function install() {
146
+ log("Installing agent-gate...\n");
147
+
148
+ let token = process.env.GH_TOKEN || "";
149
+ if (!token) {
150
+ if (nonInteractive) {
151
+ log("Error: GH_TOKEN environment variable is required in --non-interactive mode.");
152
+ log("Create a fine-grained PAT at: https://github.com/settings/personal-access-tokens/new");
153
+ process.exit(1);
154
+ }
155
+ token = await promptForToken();
156
+ if (!token) {
157
+ log("Error: No token provided.");
158
+ process.exit(1);
159
+ }
160
+ }
161
+
162
+ const python = detectPython();
163
+ logVerbose(` Detected Python: ${python.full} (${python.version})`);
164
+
165
+ mkdirSync(CLAUDE_HOME, { recursive: true });
166
+
167
+ if (existsSync(join(INSTALL_DIR, ".git"))) {
168
+ log(" ~ agent-gate already cloned, pulling latest...");
169
+ try {
170
+ run(`git -C "${INSTALL_DIR}" pull`);
171
+ } catch {
172
+ log(" ! git pull failed (continuing with existing checkout)");
173
+ }
174
+ } else {
175
+ log(" + Cloning agent-gate...");
176
+ const authenticatedUrl = `https://${token}@github.com/jl-cmd/agent-gate.git`;
177
+ try {
178
+ run(`git clone "${authenticatedUrl}" "${INSTALL_DIR}"`);
179
+ log(" + Cloned agent-gate to ~/.claude/agent-gate/");
180
+ } catch {
181
+ log("Error: git clone failed. Is GH_TOKEN set correctly?");
182
+ log("Create a fine-grained PAT at: https://github.com/settings/personal-access-tokens/new");
183
+ process.exit(1);
184
+ }
185
+ }
186
+
187
+ const venvDirectory = join(INSTALL_DIR, ".venv");
188
+ const venvPython = getVenvPython(venvDirectory);
189
+
190
+ if (existsSync(venvDirectory)) {
191
+ log(" ~ venv already exists");
192
+ } else {
193
+ log(" + Creating Python venv...");
194
+ try {
195
+ run(`${python.full} -m venv "${venvDirectory}"`);
196
+ log(" + Created Python venv");
197
+ } catch (error) {
198
+ log(`Error: Failed to create venv. ${error.message}`);
199
+ process.exit(1);
200
+ }
201
+ }
202
+
203
+ log(" + Installing agent-gate packages...");
204
+ const packagePaths = [
205
+ join(INSTALL_DIR, "packages", "agent-gate-core"),
206
+ join(INSTALL_DIR, "packages", "agent-gate-claude"),
207
+ join(INSTALL_DIR, "packages", "agent-gate-prompt-refinement"),
208
+ INSTALL_DIR,
209
+ ];
210
+ const editableArgs = packagePaths.map((each_path) => `-e "${each_path}"`).join(" ");
211
+ try {
212
+ run(`"${venvPython}" -m pip install ${editableArgs}`);
213
+ log(" + Installed agent-gate packages");
214
+ } catch {
215
+ log("Error: pip install failed. Check Python version and network connectivity.");
216
+ process.exit(1);
217
+ }
218
+
219
+ const settings = readSettings();
220
+ mergeHooks(settings, venvPython);
221
+ registerMcpServer(settings, venvPython);
222
+ writeSettings(settings);
223
+
224
+ log(" + Verifying installation...");
225
+ try {
226
+ run(`"${venvPython}" -c "from agent_gate.server import create_mcp; print('OK')"`);
227
+ log(" + Verification passed");
228
+ } catch {
229
+ log(" ! Verification failed (install may still work after restart)");
230
+ }
231
+
232
+ log(`
233
+ agent-gate installed successfully
234
+
235
+ Location: ~/.claude/agent-gate/
236
+ Python: ${python.full} (${python.version})
237
+ Venv: ~/.claude/agent-gate/.venv/
238
+ Hooks: 3 merged into settings.json
239
+ MCP: agent-gate registered
240
+
241
+ Restart Claude Code to activate.
242
+ `);
243
+ }
244
+
245
+ function uninstall() {
246
+ log("Uninstalling agent-gate...\n");
247
+
248
+ if (existsSync(INSTALL_DIR)) {
249
+ if (!nonInteractive) {
250
+ log(` This will delete ${INSTALL_DIR}`);
251
+ log(" Run with --non-interactive to skip this confirmation.");
252
+ }
253
+ rmSync(INSTALL_DIR, { recursive: true, force: true });
254
+ log(" - Removed ~/.claude/agent-gate/");
255
+ } else {
256
+ log(" ~ agent-gate directory not found (already removed?)");
257
+ }
258
+
259
+ const settings = readSettings();
260
+
261
+ if (settings.hooks) {
262
+ for (const eventName of Object.keys(settings.hooks)) {
263
+ settings.hooks[eventName] = settings.hooks[eventName].filter(
264
+ (group) => !HOOK_FILENAMES.some((filename) => hookCommandContains(group, filename))
265
+ );
266
+ if (settings.hooks[eventName].length === 0) {
267
+ delete settings.hooks[eventName];
268
+ }
269
+ }
270
+ log(" - Removed agent-gate hooks from settings.json");
271
+ }
272
+
273
+ if (settings.mcpServers?.["agent-gate"]) {
274
+ delete settings.mcpServers["agent-gate"];
275
+ log(" - Removed agent-gate MCP server from settings.json");
276
+ }
277
+
278
+ writeSettings(settings);
279
+ log("\nagent-gate uninstalled. Restart Claude Code.");
280
+ }
281
+
282
+ function update() {
283
+ log("Updating agent-gate...\n");
284
+
285
+ if (!existsSync(join(INSTALL_DIR, ".git"))) {
286
+ log("Error: agent-gate is not installed. Run the installer first.");
287
+ process.exit(1);
288
+ }
289
+
290
+ try {
291
+ run(`git -C "${INSTALL_DIR}" pull`);
292
+ log(" + Pulled latest changes");
293
+ } catch {
294
+ log("Error: git pull failed.");
295
+ process.exit(1);
296
+ }
297
+
298
+ const venvPython = getVenvPython(join(INSTALL_DIR, ".venv"));
299
+ const packagePaths = [
300
+ join(INSTALL_DIR, "packages", "agent-gate-core"),
301
+ join(INSTALL_DIR, "packages", "agent-gate-claude"),
302
+ join(INSTALL_DIR, "packages", "agent-gate-prompt-refinement"),
303
+ INSTALL_DIR,
304
+ ];
305
+ const editableArgs = packagePaths.map((each_path) => `-e "${each_path}"`).join(" ");
306
+ try {
307
+ run(`"${venvPython}" -m pip install ${editableArgs}`);
308
+ log(" + Reinstalled packages");
309
+ } catch {
310
+ log("Error: pip install failed.");
311
+ process.exit(1);
312
+ }
313
+
314
+ log("\nagent-gate updated. Restart Claude Code.");
315
+ }
316
+
317
+ function printHelp() {
318
+ log(`
319
+ agent-gate-installer
320
+
321
+ One-command setup for the agent-gate prompt evaluation gate.
322
+
323
+ Usage:
324
+ npx agent-gate-installer Install agent-gate
325
+ npx agent-gate-installer --update Pull latest and reinstall
326
+ npx agent-gate-installer --uninstall Remove agent-gate
327
+ npx agent-gate-installer --help Show this help
328
+
329
+ Flags:
330
+ --verbose Show detailed output from each step
331
+ --non-interactive Fail if GH_TOKEN is missing (for automation)
332
+
333
+ Environment:
334
+ GH_TOKEN GitHub personal access token (required for private repo)
335
+
336
+ Prerequisites:
337
+ - Node.js 18+
338
+ - Python 3.12+
339
+ - git
340
+ `);
341
+ }
342
+
343
+ if (flags.has("--help") || flags.has("-h")) {
344
+ printHelp();
345
+ } else if (flags.has("--uninstall")) {
346
+ uninstall();
347
+ } else if (flags.has("--update")) {
348
+ update();
349
+ } else {
350
+ install();
351
+ }
package/package.json ADDED
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "agent-gate-installer",
3
+ "version": "1.0.0",
4
+ "description": "One-command installer for agent-gate prompt evaluation gate",
5
+ "type": "module",
6
+ "bin": {
7
+ "agent-gate-installer": "./bin/install.mjs"
8
+ },
9
+ "files": [
10
+ "bin/"
11
+ ],
12
+ "keywords": [
13
+ "claude-code",
14
+ "agent-gate",
15
+ "prompt-gate",
16
+ "cli"
17
+ ],
18
+ "license": "MIT",
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "git+https://github.com/jl-cmd/agent-gate.git"
22
+ }
23
+ }