forge-delegate 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 +185 -0
- package/cli.mjs +261 -0
- package/docs/DELEGATION.md +61 -0
- package/docs/index.html +407 -0
- package/install.sh +118 -0
- package/lib.mjs +357 -0
- package/package.json +31 -0
- package/server.mjs +260 -0
package/docs/index.html
ADDED
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>forge-delegate — install configurator</title>
|
|
7
|
+
<meta name="description" content="Generate a one-line install command for the forge-delegate MCP server, or a prompt your AI agent can run for you.">
|
|
8
|
+
<style>
|
|
9
|
+
:root {
|
|
10
|
+
--bg: #0f1115; --panel: #171a21; --border: #262b36; --text: #e6e8ee;
|
|
11
|
+
--muted: #9aa3b2; --accent: #4f8cff; --accent-dim: #2f5fb8; --ok: #3fb96b;
|
|
12
|
+
}
|
|
13
|
+
* { box-sizing: border-box; }
|
|
14
|
+
body {
|
|
15
|
+
margin: 0; background: var(--bg); color: var(--text);
|
|
16
|
+
font: 15px/1.55 -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
|
17
|
+
}
|
|
18
|
+
.wrap { max-width: 900px; margin: 0 auto; padding: 40px 20px 80px; }
|
|
19
|
+
h1 { font-size: 26px; margin: 0 0 6px; }
|
|
20
|
+
h1 code { color: var(--accent); }
|
|
21
|
+
.sub { color: var(--muted); margin: 0 0 28px; }
|
|
22
|
+
.card {
|
|
23
|
+
background: var(--panel); border: 1px solid var(--border); border-radius: 10px;
|
|
24
|
+
padding: 20px; margin-bottom: 20px;
|
|
25
|
+
}
|
|
26
|
+
.card h2 { font-size: 15px; margin: 0 0 14px; text-transform: uppercase; letter-spacing: .06em; color: var(--muted); }
|
|
27
|
+
.field { margin-bottom: 14px; }
|
|
28
|
+
.field label { display: block; font-size: 13px; color: var(--muted); margin-bottom: 5px; }
|
|
29
|
+
input[type=text], input[type=url], select {
|
|
30
|
+
width: 100%; padding: 8px 10px; background: #0d0f13; color: var(--text);
|
|
31
|
+
border: 1px solid var(--border); border-radius: 6px; font: 13px/1.4 ui-monospace, Menlo, monospace;
|
|
32
|
+
}
|
|
33
|
+
input:focus, select:focus { outline: none; border-color: var(--accent); }
|
|
34
|
+
.chips { display: flex; gap: 8px; flex-wrap: wrap; }
|
|
35
|
+
.chip {
|
|
36
|
+
border: 1px solid var(--border); background: #0d0f13; color: var(--text);
|
|
37
|
+
border-radius: 999px; padding: 6px 14px; font-size: 13px; cursor: pointer; user-select: none;
|
|
38
|
+
}
|
|
39
|
+
.chip.on { background: var(--accent-dim); border-color: var(--accent); color: #fff; }
|
|
40
|
+
.check { display: inline-flex; align-items: center; gap: 6px; margin-right: 16px; font-size: 14px; cursor: pointer; }
|
|
41
|
+
.row { display: flex; gap: 18px; flex-wrap: wrap; }
|
|
42
|
+
.row .field { flex: 1; min-width: 200px; }
|
|
43
|
+
.radio { display: inline-flex; align-items: center; gap: 6px; margin-right: 16px; font-size: 14px; cursor: pointer; }
|
|
44
|
+
.hidden { display: none; }
|
|
45
|
+
pre {
|
|
46
|
+
background: #0d0f13; border: 1px solid var(--border); border-radius: 6px;
|
|
47
|
+
padding: 12px 14px; overflow-x: auto; font: 13px/1.5 ui-monospace, Menlo, monospace;
|
|
48
|
+
white-space: pre-wrap; word-break: break-all; margin: 0 0 10px;
|
|
49
|
+
}
|
|
50
|
+
.hint { color: var(--muted); font-size: 12.5px; margin: 0 0 12px; }
|
|
51
|
+
.bar { display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px; }
|
|
52
|
+
.bar h2 { margin: 0; }
|
|
53
|
+
button {
|
|
54
|
+
background: var(--accent); color: #fff; border: 0; border-radius: 6px;
|
|
55
|
+
padding: 7px 14px; font-size: 13px; cursor: pointer;
|
|
56
|
+
}
|
|
57
|
+
button:hover { background: #6aa0ff; }
|
|
58
|
+
button.ok { background: var(--ok); }
|
|
59
|
+
.foot { color: var(--muted); font-size: 12.5px; margin-top: 30px; text-align: center; }
|
|
60
|
+
.foot a { color: var(--accent); }
|
|
61
|
+
.note { color: var(--muted); font-size: 12.5px; }
|
|
62
|
+
|
|
63
|
+
/* model picker */
|
|
64
|
+
.picker { position: relative; }
|
|
65
|
+
.picker .status { font-size: 12px; color: var(--muted); margin-top: 6px; min-height: 16px; }
|
|
66
|
+
.picker .filters { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 10px; align-items: center; }
|
|
67
|
+
.picker .filters select { width: auto; padding: 6px 8px; }
|
|
68
|
+
.picker .list {
|
|
69
|
+
position: absolute; top: calc(100% + 4px); left: 0; right: 0; z-index: 20;
|
|
70
|
+
background: #0d0f13; border: 1px solid var(--accent-dim); border-radius: 8px;
|
|
71
|
+
max-height: 320px; overflow-y: auto; box-shadow: 0 12px 32px rgba(0,0,0,.5);
|
|
72
|
+
}
|
|
73
|
+
.picker .list .item {
|
|
74
|
+
display: flex; justify-content: space-between; align-items: center; gap: 12px;
|
|
75
|
+
padding: 7px 12px; cursor: pointer; border-bottom: 1px solid var(--border);
|
|
76
|
+
font: 13px ui-monospace, Menlo, monospace;
|
|
77
|
+
}
|
|
78
|
+
.picker .list .item:hover { background: var(--accent-dim); color: #fff; }
|
|
79
|
+
.picker .list .item .price { color: var(--muted); font-size: 11.5px; white-space: nowrap; }
|
|
80
|
+
.picker .list .item:hover .price { color: #cfe0ff; }
|
|
81
|
+
.picker .list .item .free { color: var(--ok); font-size: 11px; border: 1px solid var(--ok); border-radius: 4px; padding: 0 5px; }
|
|
82
|
+
.picker .list .empty { padding: 12px; color: var(--muted); font-size: 13px; }
|
|
83
|
+
.picker .list .count { padding: 6px 12px; color: var(--muted); font-size: 11.5px; position: sticky; top: 0; background: #0d0f13; }
|
|
84
|
+
</style>
|
|
85
|
+
</head>
|
|
86
|
+
<body>
|
|
87
|
+
<div class="wrap">
|
|
88
|
+
<h1>forge-delegate <code>→</code> install configurator</h1>
|
|
89
|
+
<p class="sub">
|
|
90
|
+
MCP server that lets your primary agent (Claude Code / Codex / opencode) delegate grunt work to
|
|
91
|
+
cheaper models. Pick your options, then copy the one-line install command — or let your AI run it.
|
|
92
|
+
</p>
|
|
93
|
+
|
|
94
|
+
<div class="card">
|
|
95
|
+
<h2>1 · Configure</h2>
|
|
96
|
+
|
|
97
|
+
<div class="field picker">
|
|
98
|
+
<label for="model">Default model (provider/model)</label>
|
|
99
|
+
<input type="text" id="model" placeholder="type to search, e.g. opencode/mimo-v2.5-free" autocomplete="off">
|
|
100
|
+
<div class="filters">
|
|
101
|
+
<select id="provider" title="Provider">
|
|
102
|
+
<option value="">all providers</option>
|
|
103
|
+
</select>
|
|
104
|
+
<label class="check"><input type="checkbox" id="freeOnly"> free only</label>
|
|
105
|
+
<label class="check"><input type="checkbox" id="cheapOnly"> cheap (< $0.50 in)</label>
|
|
106
|
+
</div>
|
|
107
|
+
<div class="status" id="modelStatus">Loading model catalog…</div>
|
|
108
|
+
<div class="list hidden" id="modelList"></div>
|
|
109
|
+
<p class="note">
|
|
110
|
+
Model list & prices come from <a href="https://models.dev" target="_blank" rel="noopener">models.dev</a>.
|
|
111
|
+
The free <code>opencode/*</code> models it lists change fast and can be outdated
|
|
112
|
+
(e.g. <code>opencode/minimax-m3-free</code> is no longer served); providers you auth
|
|
113
|
+
with a key (deepseek, openai, …) are usually in sync. Either way, run
|
|
114
|
+
<code>opencode models</code> before picking and copy a model from <em>that</em> list —
|
|
115
|
+
it's exactly what your opencode accepts today, so the install command will work.
|
|
116
|
+
</p>
|
|
117
|
+
<p class="note">
|
|
118
|
+
Free <code>opencode/*</code> models need <em>no</em> auth — zero setup. For any other
|
|
119
|
+
provider (deepseek, openai, google, …), <strong>you</strong> must log it into your own
|
|
120
|
+
opencode first: <code>opencode auth login <provider></code>. forge-delegate reuses
|
|
121
|
+
your opencode's credentials and never stores API keys.
|
|
122
|
+
</p>
|
|
123
|
+
</div>
|
|
124
|
+
|
|
125
|
+
<div class="field">
|
|
126
|
+
<label>Register with</label>
|
|
127
|
+
<div class="chips" id="targets">
|
|
128
|
+
<span class="chip on" data-target="claude">Claude Code</span>
|
|
129
|
+
<span class="chip on" data-target="codex">Codex</span>
|
|
130
|
+
<span class="chip on" data-target="opencode">opencode</span>
|
|
131
|
+
</div>
|
|
132
|
+
</div>
|
|
133
|
+
|
|
134
|
+
<div class="field">
|
|
135
|
+
<label>Scope</label>
|
|
136
|
+
<label class="radio"><input type="radio" name="scope" value="user" checked> user (all projects)</label>
|
|
137
|
+
<label class="radio"><input type="radio" name="scope" value="project"> project</label>
|
|
138
|
+
</div>
|
|
139
|
+
|
|
140
|
+
<div class="field hidden" id="projectDirField">
|
|
141
|
+
<label for="projectDir">Project directory (for project scope)</label>
|
|
142
|
+
<input type="text" id="projectDir" placeholder="/path/to/project">
|
|
143
|
+
</div>
|
|
144
|
+
</div>
|
|
145
|
+
|
|
146
|
+
<div class="card">
|
|
147
|
+
<div class="bar"><h2>2 · One-line install</h2><button id="copyCmd">Copy command</button></div>
|
|
148
|
+
<p class="hint">npx pulls the latest package and registers it with your agents — works on Windows (cmd/PowerShell), macOS, and Linux, no bash needed.</p>
|
|
149
|
+
<pre id="cmd"></pre>
|
|
150
|
+
</div>
|
|
151
|
+
|
|
152
|
+
<div class="card">
|
|
153
|
+
<div class="bar"><h2>3 · Let your AI do it</h2><button id="copyPrompt">Copy prompt</button></div>
|
|
154
|
+
<p class="hint">Paste this into Claude Code, Codex, or opencode — the agent runs the command, checks it, and fixes problems itself.</p>
|
|
155
|
+
<pre id="prompt"></pre>
|
|
156
|
+
</div>
|
|
157
|
+
|
|
158
|
+
<p class="foot">
|
|
159
|
+
Repo: <a href="https://github.com/MuhamadBarzani/forge-delegate" target="_blank" rel="noopener">github.com/MuhamadBarzani/forge-delegate</a>
|
|
160
|
+
· Docs: <a href="./DELEGATION.md">delegation guide</a>
|
|
161
|
+
</p>
|
|
162
|
+
</div>
|
|
163
|
+
|
|
164
|
+
<script>
|
|
165
|
+
(function () {
|
|
166
|
+
var CATALOG_URL = "https://models.dev/api.json";
|
|
167
|
+
var CHEAP_MAX = 0.5;
|
|
168
|
+
var CACHE_KEY = "fd-models-cache-v1";
|
|
169
|
+
var CACHE_TTL = 7 * 24 * 3600 * 1000;
|
|
170
|
+
var COMMON = ["opencode", "deepseek", "openai", "google", "anthropic", "ollama", "mistral", "meta", "qwen", "xai", "groq", "moonshotai", "zhipuai", "nvidia", "amazon"];
|
|
171
|
+
|
|
172
|
+
var modelEl = document.getElementById("model");
|
|
173
|
+
var providerEl = document.getElementById("provider");
|
|
174
|
+
var freeEl = document.getElementById("freeOnly");
|
|
175
|
+
var cheapEl = document.getElementById("cheapOnly");
|
|
176
|
+
var statusEl = document.getElementById("modelStatus");
|
|
177
|
+
var listEl = document.getElementById("modelList");
|
|
178
|
+
var targetEls = document.querySelectorAll("#targets .chip");
|
|
179
|
+
var scopeEls = document.querySelectorAll('input[name="scope"]');
|
|
180
|
+
var projectDirField = document.getElementById("projectDirField");
|
|
181
|
+
var projectDirEl = document.getElementById("projectDir");
|
|
182
|
+
var cmdEl = document.getElementById("cmd");
|
|
183
|
+
var promptEl = document.getElementById("prompt");
|
|
184
|
+
|
|
185
|
+
var models = []; // flat: {id, provider, input, free}
|
|
186
|
+
|
|
187
|
+
// ---- model catalog ------------------------------------------------------
|
|
188
|
+
function loadCatalog() {
|
|
189
|
+
try {
|
|
190
|
+
var cached = JSON.parse(localStorage.getItem(CACHE_KEY) || "null");
|
|
191
|
+
if (cached && Date.now() - cached.ts < CACHE_TTL && cached.models.length) {
|
|
192
|
+
setCatalog(cached.models, "Loaded " + cached.models.length + " models from cache");
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
} catch (e) {}
|
|
196
|
+
fetch(CATALOG_URL)
|
|
197
|
+
.then(function (r) { if (!r.ok) throw new Error("HTTP " + r.status); return r.json(); })
|
|
198
|
+
.then(function (data) {
|
|
199
|
+
var flat = [];
|
|
200
|
+
Object.keys(data).forEach(function (pid) {
|
|
201
|
+
var p = data[pid];
|
|
202
|
+
Object.keys(p.models || {}).forEach(function (mid) {
|
|
203
|
+
var m = p.models[mid];
|
|
204
|
+
var cost = m.cost || {};
|
|
205
|
+
flat.push({
|
|
206
|
+
id: pid + "/" + mid,
|
|
207
|
+
provider: pid,
|
|
208
|
+
input: cost.input || 0,
|
|
209
|
+
free: (cost.input || 0) === 0 && (cost.output || 0) === 0
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
try { localStorage.setItem(CACHE_KEY, JSON.stringify({ ts: Date.now(), models: flat })); } catch (e) {}
|
|
214
|
+
setCatalog(flat, "Loaded " + flat.length + " models from models.dev (live)");
|
|
215
|
+
})
|
|
216
|
+
.catch(function (e) {
|
|
217
|
+
statusEl.textContent = "Couldn't load catalog (" + e.message + ") — type a model manually.";
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function setCatalog(flat, msg) {
|
|
222
|
+
models = flat;
|
|
223
|
+
statusEl.textContent = msg;
|
|
224
|
+
var known = {};
|
|
225
|
+
COMMON.concat(models.map(function (m) { return m.provider; }))
|
|
226
|
+
.forEach(function (p) { known[p] = true; });
|
|
227
|
+
Object.keys(known).sort().forEach(function (pid) {
|
|
228
|
+
var opt = document.createElement("option");
|
|
229
|
+
opt.value = pid; opt.textContent = pid;
|
|
230
|
+
providerEl.appendChild(opt);
|
|
231
|
+
});
|
|
232
|
+
renderList();
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function matches(m) {
|
|
236
|
+
var q = modelEl.value.trim().toLowerCase();
|
|
237
|
+
var prov = providerEl.value;
|
|
238
|
+
if (prov && m.provider !== prov) return false;
|
|
239
|
+
if (freeEl.checked && !m.free) return false;
|
|
240
|
+
if (cheapEl.checked && m.input >= CHEAP_MAX) return false;
|
|
241
|
+
if (q) {
|
|
242
|
+
var i = m.id.toLowerCase();
|
|
243
|
+
var p = i.indexOf("/" + q);
|
|
244
|
+
if (i.indexOf(q) === -1 && p === -1) return false;
|
|
245
|
+
}
|
|
246
|
+
return true;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function renderList() {
|
|
250
|
+
if (!models.length) return;
|
|
251
|
+
var hits = models.filter(matches)
|
|
252
|
+
.sort(function (a, b) {
|
|
253
|
+
if (a.free !== b.free) return a.free ? -1 : 1;
|
|
254
|
+
if (a.input !== b.input) return a.input - b.input;
|
|
255
|
+
return a.id.localeCompare(b.id);
|
|
256
|
+
})
|
|
257
|
+
.slice(0, 60);
|
|
258
|
+
listEl.innerHTML = "";
|
|
259
|
+
var count = document.createElement("div");
|
|
260
|
+
count.className = "count";
|
|
261
|
+
count.textContent = hits.length + (hits.length === 60 ? "+" : "") + " of " + models.length + " models";
|
|
262
|
+
listEl.appendChild(count);
|
|
263
|
+
if (!hits.length) {
|
|
264
|
+
var e = document.createElement("div");
|
|
265
|
+
e.className = "empty";
|
|
266
|
+
e.textContent = "no matches — type a model id manually";
|
|
267
|
+
listEl.appendChild(e);
|
|
268
|
+
}
|
|
269
|
+
hits.forEach(function (m) {
|
|
270
|
+
var row = document.createElement("div");
|
|
271
|
+
row.className = "item";
|
|
272
|
+
var left = document.createElement("span");
|
|
273
|
+
left.textContent = m.id;
|
|
274
|
+
if (m.free) {
|
|
275
|
+
var b = document.createElement("span");
|
|
276
|
+
b.className = "free"; b.textContent = "free";
|
|
277
|
+
left.appendChild(document.createTextNode(" ")); left.appendChild(b);
|
|
278
|
+
}
|
|
279
|
+
var price = document.createElement("span");
|
|
280
|
+
price.className = "price";
|
|
281
|
+
price.textContent = m.free ? "$0" : "$" + m.input.toFixed(2) + "/M in";
|
|
282
|
+
row.appendChild(left); row.appendChild(price);
|
|
283
|
+
row.addEventListener("click", function () {
|
|
284
|
+
modelEl.value = m.id;
|
|
285
|
+
hideList();
|
|
286
|
+
render();
|
|
287
|
+
});
|
|
288
|
+
listEl.appendChild(row);
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function showList() { if (models.length) { listEl.classList.remove("hidden"); renderList(); } }
|
|
293
|
+
function hideList() { listEl.classList.add("hidden"); }
|
|
294
|
+
|
|
295
|
+
// ---- command / prompt ---------------------------------------------------
|
|
296
|
+
function getTargets() {
|
|
297
|
+
var on = [];
|
|
298
|
+
targetEls.forEach(function (c) { if (c.classList.contains("on")) on.push(c.dataset.target); });
|
|
299
|
+
return on;
|
|
300
|
+
}
|
|
301
|
+
function getScope() {
|
|
302
|
+
for (var i = 0; i < scopeEls.length; i++) if (scopeEls[i].checked) return scopeEls[i].value;
|
|
303
|
+
return "user";
|
|
304
|
+
}
|
|
305
|
+
function shellQuote(s) { return '"' + s.replace(/"/g, '\\"') + '"'; }
|
|
306
|
+
|
|
307
|
+
function setupFlags() {
|
|
308
|
+
var parts = [];
|
|
309
|
+
var model = modelEl.value.trim();
|
|
310
|
+
if (model) parts.push("--model", shellQuote(model));
|
|
311
|
+
var targets = getTargets();
|
|
312
|
+
if (targets.length) parts.push("--targets", shellQuote(targets.join(",")));
|
|
313
|
+
var scope = getScope();
|
|
314
|
+
if (scope === "project") {
|
|
315
|
+
parts.push("--scope", "project");
|
|
316
|
+
var dir = projectDirEl.value.trim();
|
|
317
|
+
if (dir) parts.push("--project-dir", shellQuote(dir));
|
|
318
|
+
}
|
|
319
|
+
return parts;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function command() {
|
|
323
|
+
return ["npx", "-y", "forge-delegate", "setup"].concat(setupFlags()).join(" ");
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function prompt() {
|
|
327
|
+
var model = modelEl.value.trim();
|
|
328
|
+
var targets = getTargets().join(", ") || "(whichever you find installed)";
|
|
329
|
+
var scope = getScope();
|
|
330
|
+
var cmd = command();
|
|
331
|
+
var modelLine = model
|
|
332
|
+
? 'Use "' + model + '" as the default model unless I say otherwise.'
|
|
333
|
+
: 'Use a cheap/free default model (e.g. opencode/mimo-v2.5-free) unless I say otherwise.';
|
|
334
|
+
return [
|
|
335
|
+
"Install and register the forge-delegate MCP server for me. " + modelLine +
|
|
336
|
+
" Register it with " + targets + ", and use \"" + scope + "\" scope.",
|
|
337
|
+
"",
|
|
338
|
+
"Run:",
|
|
339
|
+
"",
|
|
340
|
+
cmd,
|
|
341
|
+
"",
|
|
342
|
+
"Then show me the output, flag any warnings (missing opencode, unauthenticated provider, PATH issues), " +
|
|
343
|
+
"and confirm it's registered (claude mcp list and/or codex mcp). " +
|
|
344
|
+
"If something fails, diagnose and fix it rather than just reporting it."
|
|
345
|
+
].join("\n");
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function render() {
|
|
349
|
+
cmdEl.textContent = command();
|
|
350
|
+
promptEl.textContent = prompt();
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// ---- copy ---------------------------------------------------------------
|
|
354
|
+
function copy(el, btn, doneText) {
|
|
355
|
+
var text = el.textContent;
|
|
356
|
+
function done() {
|
|
357
|
+
var old = btn.textContent;
|
|
358
|
+
btn.textContent = doneText;
|
|
359
|
+
btn.classList.add("ok");
|
|
360
|
+
setTimeout(function () { btn.textContent = old; btn.classList.remove("ok"); }, 1600);
|
|
361
|
+
}
|
|
362
|
+
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
363
|
+
navigator.clipboard.writeText(text).then(done, function () { fallbackCopy(text); done(); });
|
|
364
|
+
} else { fallbackCopy(text); done(); }
|
|
365
|
+
}
|
|
366
|
+
function fallbackCopy(text) {
|
|
367
|
+
var ta = document.createElement("textarea");
|
|
368
|
+
ta.value = text; document.body.appendChild(ta); ta.select();
|
|
369
|
+
try { document.execCommand("copy"); } catch (e) {}
|
|
370
|
+
document.body.removeChild(ta);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// ---- events -------------------------------------------------------------
|
|
374
|
+
targetEls.forEach(function (c) {
|
|
375
|
+
c.addEventListener("click", function () {
|
|
376
|
+
c.classList.toggle("on");
|
|
377
|
+
if (!getTargets().length) c.classList.add("on");
|
|
378
|
+
render();
|
|
379
|
+
});
|
|
380
|
+
});
|
|
381
|
+
scopeEls.forEach(function (r) {
|
|
382
|
+
r.addEventListener("change", function () {
|
|
383
|
+
projectDirField.classList.toggle("hidden", r.value !== "project");
|
|
384
|
+
render();
|
|
385
|
+
});
|
|
386
|
+
});
|
|
387
|
+
[projectDirEl].forEach(function (el) { el.addEventListener("input", render); });
|
|
388
|
+
|
|
389
|
+
modelEl.addEventListener("input", function () { render(); renderList(); });
|
|
390
|
+
modelEl.addEventListener("focus", showList);
|
|
391
|
+
modelEl.addEventListener("keydown", function (ev) { if (ev.key === "Escape") hideList(); });
|
|
392
|
+
providerEl.addEventListener("change", renderList);
|
|
393
|
+
freeEl.addEventListener("change", renderList);
|
|
394
|
+
cheapEl.addEventListener("change", renderList);
|
|
395
|
+
document.addEventListener("click", function (ev) {
|
|
396
|
+
if (!ev.target.closest(".picker")) hideList();
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
document.getElementById("copyCmd").addEventListener("click", function (e) { copy(cmdEl, e.currentTarget, "Copied!"); });
|
|
400
|
+
document.getElementById("copyPrompt").addEventListener("click", function (e) { copy(promptEl, e.currentTarget, "Copied!"); });
|
|
401
|
+
|
|
402
|
+
loadCatalog();
|
|
403
|
+
render();
|
|
404
|
+
})();
|
|
405
|
+
</script>
|
|
406
|
+
</body>
|
|
407
|
+
</html>
|
package/install.sh
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# forge-delegate installer — register the MCP server with Claude Code, Codex, and/or opencode.
|
|
3
|
+
#
|
|
4
|
+
# Preferred route: the published npm package (works everywhere, always latest):
|
|
5
|
+
# npx -y forge-delegate setup --model ... --targets ... --scope ...
|
|
6
|
+
# If that fails (e.g. package not published yet / offline), this script falls back to a
|
|
7
|
+
# repo checkout or clone. From a checkout: bash install.sh [options...]
|
|
8
|
+
# From a pipe: curl -fsSL <raw-url> | bash -s -- [options...]
|
|
9
|
+
#
|
|
10
|
+
# Options:
|
|
11
|
+
# --model provider/model default model so calls can omit it
|
|
12
|
+
# --targets a,b,c hosts to register with: claude,codex,opencode (default all)
|
|
13
|
+
# --scope user|project registration scope (default user)
|
|
14
|
+
# --project-dir <path> project for --scope project (default current dir)
|
|
15
|
+
# --no-opencode skip auto-installing opencode if missing
|
|
16
|
+
# --no-npx skip the npx route, force clone/checkout
|
|
17
|
+
# --help
|
|
18
|
+
set -euo pipefail
|
|
19
|
+
|
|
20
|
+
MODEL=""
|
|
21
|
+
TARGETS="claude,codex,opencode"
|
|
22
|
+
SCOPE="user"
|
|
23
|
+
PROJECT_DIR=""
|
|
24
|
+
NO_OPENCODE=0
|
|
25
|
+
NO_NPX=0
|
|
26
|
+
|
|
27
|
+
while [[ $# -gt 0 ]]; do
|
|
28
|
+
case "$1" in
|
|
29
|
+
--model) MODEL="${2:-}"; shift 2 ;;
|
|
30
|
+
--model=*) MODEL="${1#*=}"; shift ;;
|
|
31
|
+
--targets) TARGETS="${2:-}"; shift 2 ;;
|
|
32
|
+
--targets=*) TARGETS="${1#*=}"; shift ;;
|
|
33
|
+
--scope) SCOPE="${2:-}"; shift 2 ;;
|
|
34
|
+
--scope=*) SCOPE="${1#*=}"; shift ;;
|
|
35
|
+
--project-dir) PROJECT_DIR="${2:-}"; shift 2 ;;
|
|
36
|
+
--project-dir=*) PROJECT_DIR="${1#*=}"; shift ;;
|
|
37
|
+
--no-opencode) NO_OPENCODE=1; shift ;;
|
|
38
|
+
--no-npx) NO_NPX=1; shift ;;
|
|
39
|
+
--help|-h)
|
|
40
|
+
sed -n '3,18p' "$0"
|
|
41
|
+
exit 0 ;;
|
|
42
|
+
*) echo "unknown option: $1"; exit 1 ;;
|
|
43
|
+
esac
|
|
44
|
+
done
|
|
45
|
+
|
|
46
|
+
if [[ -n "$PROJECT_DIR" ]]; then
|
|
47
|
+
PROJECT_DIR="$(cd "$PROJECT_DIR" && pwd)"
|
|
48
|
+
else
|
|
49
|
+
PROJECT_DIR="$PWD"
|
|
50
|
+
fi
|
|
51
|
+
|
|
52
|
+
echo "=== forge-delegate installer ==="
|
|
53
|
+
|
|
54
|
+
# 1. Requirements
|
|
55
|
+
command -v node >/dev/null 2>&1 || { echo "✗ node is required (>=18)"; exit 1; }
|
|
56
|
+
if [[ "$NO_OPENCODE" -eq 0 ]] && ! command -v opencode >/dev/null 2>&1; then
|
|
57
|
+
echo "· opencode not found — installing it..."
|
|
58
|
+
if command -v curl >/dev/null 2>&1; then
|
|
59
|
+
curl -fsSL https://opencode.ai/install | bash || true
|
|
60
|
+
fi
|
|
61
|
+
if ! command -v opencode >/dev/null 2>&1 && [[ -x "$HOME/.opencode/bin/opencode" ]]; then
|
|
62
|
+
export PATH="$HOME/.opencode/bin:$PATH"
|
|
63
|
+
fi
|
|
64
|
+
command -v opencode >/dev/null 2>&1 || echo "· opencode still not on PATH — install it from https://opencode.ai and re-run. (Delegations need it.)"
|
|
65
|
+
fi
|
|
66
|
+
|
|
67
|
+
# 2. Build the setup args once, used by both routes
|
|
68
|
+
ARGS=(--targets "$TARGETS" --scope "$SCOPE")
|
|
69
|
+
if [[ -n "$MODEL" ]]; then ARGS+=(--model "$MODEL"); fi
|
|
70
|
+
if [[ "$SCOPE" == "project" ]]; then ARGS+=(--project-dir "$PROJECT_DIR"); fi
|
|
71
|
+
|
|
72
|
+
# 3. Preferred route: published npm package (works on Windows, always latest)
|
|
73
|
+
if [[ "$NO_NPX" -eq 0 ]] && command -v npx >/dev/null 2>&1; then
|
|
74
|
+
echo "· installing via npx (published package)..."
|
|
75
|
+
if npx -y forge-delegate setup "${ARGS[@]}"; then
|
|
76
|
+
echo
|
|
77
|
+
echo "Done. Restart your agent, then confirm the server is connected:"
|
|
78
|
+
echo " Claude Code : /mcp"
|
|
79
|
+
echo " Codex : codex mcp"
|
|
80
|
+
echo " opencode : opencode"
|
|
81
|
+
echo "Change defaults anytime: forge-delegate config set --model <m>"
|
|
82
|
+
exit 0
|
|
83
|
+
fi
|
|
84
|
+
echo "· npx route unavailable (not published yet?) — falling back to a repo copy..."
|
|
85
|
+
fi
|
|
86
|
+
|
|
87
|
+
# 4. Fallback: local checkout, or clone into place (pipe mode)
|
|
88
|
+
SCRIPT_DIR="$(dirname "${BASH_SOURCE[0]}")"
|
|
89
|
+
if [[ -f "$SCRIPT_DIR/cli.mjs" ]]; then
|
|
90
|
+
APP_DIR="$SCRIPT_DIR"
|
|
91
|
+
echo "· using local checkout: $APP_DIR"
|
|
92
|
+
else
|
|
93
|
+
APP_DIR="${FORGE_DELEGATE_DIR:-$HOME/.local/share/forge-delegate}"
|
|
94
|
+
REPO="${FORGE_DELEGATE_REPO:-https://github.com/MuhamadBarzani/forge-delegate.git}"
|
|
95
|
+
command -v git >/dev/null 2>&1 || { echo "✗ git is required for remote install"; exit 1; }
|
|
96
|
+
echo "· fetching forge-delegate into $APP_DIR"
|
|
97
|
+
if [[ -f "$APP_DIR/cli.mjs" ]]; then
|
|
98
|
+
(cd "$APP_DIR" && git pull --ff-only) || echo "· pull failed — continuing with existing copy"
|
|
99
|
+
else
|
|
100
|
+
mkdir -p "$APP_DIR"
|
|
101
|
+
git clone --depth 1 "$REPO" "$APP_DIR"
|
|
102
|
+
fi
|
|
103
|
+
fi
|
|
104
|
+
|
|
105
|
+
# 5. Install JS deps if not present
|
|
106
|
+
if [[ ! -d "$APP_DIR/node_modules" ]]; then
|
|
107
|
+
echo "· installing dependencies..."
|
|
108
|
+
(cd "$APP_DIR" && npm install --silent)
|
|
109
|
+
fi
|
|
110
|
+
|
|
111
|
+
node "$APP_DIR/cli.mjs" setup "${ARGS[@]}"
|
|
112
|
+
|
|
113
|
+
echo
|
|
114
|
+
echo "Done. Restart your agent, then confirm the server is connected:"
|
|
115
|
+
echo " Claude Code : /mcp"
|
|
116
|
+
echo " Codex : codex mcp"
|
|
117
|
+
echo " opencode : opencode"
|
|
118
|
+
echo "Change defaults anytime: forge-delegate config set --model <m>"
|