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.
- package/README.md +77 -0
- package/bin/install.mjs +351 -0
- 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.
|
package/bin/install.mjs
ADDED
|
@@ -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
|
+
}
|