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.
@@ -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 (&lt; $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 &amp; 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 &lt;provider&gt;</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>"