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/README.md
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
# forge-delegate
|
|
2
|
+
|
|
3
|
+
MCP server that delegates coding work from your primary agent (**Claude Code, Codex, or opencode**)
|
|
4
|
+
to other — usually cheaper or free — AI models, so your main agent's context stays small and your
|
|
5
|
+
token bill stays low. Direct API calls for quick questions; full agentic delegation via
|
|
6
|
+
[opencode](https://opencode.ai) for real work.
|
|
7
|
+
|
|
8
|
+
Your main agent (the expensive one) keeps architecture, critical logic, and hard decisions.
|
|
9
|
+
The cheap model grinds through the high-volume, low-risk work (boilerplate, repetitive edits,
|
|
10
|
+
tests, lint/type fixes) and reports back.
|
|
11
|
+
|
|
12
|
+
```
|
|
13
|
+
┌──────────────────┐ MCP (stdio) ┌──────────────────┐ opencode run ┌──────────────────┐
|
|
14
|
+
│ Claude / Codex / │ ──────────────► │ forge-delegate │ ───────────────► │ opencode + cheap │
|
|
15
|
+
│ opencode (primary)│ │ (this server) │ --model cheap │ model │
|
|
16
|
+
└──────────────────┘ ◄────────────── └──────────────────┘ ◄─────────────── └──────────────────┘
|
|
17
|
+
result: text + session + token usage
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Quick start
|
|
21
|
+
|
|
22
|
+
**Web configurator** — pick model/targets/scope and get a ready-to-paste command
|
|
23
|
+
(https://muhamadbarzani.github.io/forge-delegate/). The model picker is backed by the live
|
|
24
|
+
[models.dev](https://models.dev) catalog — searchable, with free/cheap filters and real prices,
|
|
25
|
+
so you're picking from actual available models rather than guessing.
|
|
26
|
+
|
|
27
|
+
**npx (recommended — works everywhere, no bash needed):**
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
npx -y forge-delegate setup --model "opencode/mimo-v2.5-free" --targets "claude,codex,opencode"
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
That's it — npx pulls the latest package and `setup` registers the MCP server with the agents
|
|
34
|
+
you chose and sets your default model. Works on Windows (cmd/PowerShell), macOS, and Linux —
|
|
35
|
+
no bash needed, and always runs the latest version.
|
|
36
|
+
|
|
37
|
+
Run `setup` **without flags** and it walks you through the choices interactively (defaults in
|
|
38
|
+
`[brackets]`, Enter accepts); pass any of the flags to skip that prompt. In an automated/non-terminal
|
|
39
|
+
run it just uses sensible defaults.
|
|
40
|
+
|
|
41
|
+
> GitHub Pages: enable it with *Settings → Pages → Deploy from a branch → `main` / `/docs`*.
|
|
42
|
+
> The configurator lives at `docs/index.html`. `install.sh`'s `FORGE_DELEGATE_REPO` default
|
|
43
|
+
> (and the raw URL inside it) matters only if you self-host the fallback installer.
|
|
44
|
+
>
|
|
45
|
+
> Working on a checkout yourself? `node cli.mjs setup` does the same thing.
|
|
46
|
+
|
|
47
|
+
`setup` will:
|
|
48
|
+
1. Register the MCP server with each agent you chose (`--targets claude,codex,opencode`).
|
|
49
|
+
2. Set your default model (`--model`) so calls can omit `model`.
|
|
50
|
+
|
|
51
|
+
Routing and cost-policy guidance is injected by the MCP server itself into the agent's context
|
|
52
|
+
via MCP `instructions` — no memory-file (`CLAUDE.md`/`AGENTS.md`) editing needed.
|
|
53
|
+
|
|
54
|
+
Then restart your agent and confirm the server is connected: `/mcp` in Claude Code,
|
|
55
|
+
`codex mcp`, or `opencode`.
|
|
56
|
+
|
|
57
|
+
### What you need
|
|
58
|
+
|
|
59
|
+
- **Node.js ≥ 18** and **opencode** installed (the installer can fetch opencode for you).
|
|
60
|
+
- **Free `opencode/*` models** (e.g. `opencode/mimo-v2.5-free`) need no auth — zero setup.
|
|
61
|
+
- **Any other provider** (deepseek, openai, google, ollama, …) must be logged into **your**
|
|
62
|
+
opencode first — that's a step *you* do in your own opencode, not something forge-delegate
|
|
63
|
+
handles: `opencode auth login <provider>`. forge-delegate reuses your opencode's credentials
|
|
64
|
+
and never stores API keys. (Ollama needs no key, just the Ollama app running.)
|
|
65
|
+
|
|
66
|
+
### Windows
|
|
67
|
+
|
|
68
|
+
Use the `npx` route — no bash needed (works in cmd/PowerShell). Drop `codex` from `--targets`
|
|
69
|
+
(Codex CLI isn't natively supported on Windows; use `--targets "claude,opencode"`).
|
|
70
|
+
|
|
71
|
+
### Scope
|
|
72
|
+
|
|
73
|
+
| `--scope` | Claude Code | Codex | opencode |
|
|
74
|
+
|---|---|---|---|
|
|
75
|
+
| `user` (default) | `~/.claude.json` — all projects | `~/.codex/config.toml` — all projects | `~/.config/opencode/opencode.json` — all projects |
|
|
76
|
+
| `project` | `.mcp.json` in the project (needs one-time approval) | `.codex/config.toml` in the project (loads for trusted projects only) | `opencode.json` in the project |
|
|
77
|
+
|
|
78
|
+
### Try it free — zero signup
|
|
79
|
+
|
|
80
|
+
opencode ships free no-auth models under `opencode/`:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
npx -y forge-delegate setup --model "opencode/mimo-v2.5-free" --targets "claude,codex"
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Other free options: `opencode/hy3-free`, `opencode/nemotron-3.5-lightning-free`. Free tiers are
|
|
87
|
+
rate-limited; switch anytime with `forge-delegate config set --model <m>`.
|
|
88
|
+
|
|
89
|
+
## Tools
|
|
90
|
+
|
|
91
|
+
| Tool | What it does | Context cost |
|
|
92
|
+
|---|---|---|
|
|
93
|
+
| `ask_model` | One-shot question to any model (direct API, no file access) | Small |
|
|
94
|
+
| `delegate_task` | Send a task + optional files to a model agent running via opencode; returns result + session id | Medium |
|
|
95
|
+
| `delegate_agent` | Model works **autonomously** in the **background** — returns a job id instantly; poll `check_delegation` | Tiny |
|
|
96
|
+
| `check_delegation` | Poll a background job (or list all with no id) | Tiny |
|
|
97
|
+
| `list_models` | List local/keyless, direct-API, and agentic models | Small |
|
|
98
|
+
| `get_delegate_config` | Read current configuration (default model, directory, autoApprove, profiles) | Tiny |
|
|
99
|
+
| `set_delegate_config` | Change configuration at runtime — no reinstall | Tiny |
|
|
100
|
+
|
|
101
|
+
`delegate_agent` is the token-saver: the external model gets its own tool loop executed by
|
|
102
|
+
opencode locally. File contents never touch your main agent's context. The delegation runs as a
|
|
103
|
+
detached background process, so the MCP call returns instantly.
|
|
104
|
+
|
|
105
|
+
## When to delegate
|
|
106
|
+
|
|
107
|
+
| ✅ Delegate — high volume, low risk | ⛔ Keep on the primary agent |
|
|
108
|
+
|---|---|
|
|
109
|
+
| Tests for well-specified behavior | Architecture, system design, choosing abstractions |
|
|
110
|
+
| Boilerplate & scaffolding (CRUD, DTOs, fixtures, mocks) | Security-sensitive code (auth, crypto, secrets, permissions) |
|
|
111
|
+
| Mechanical edits across many files (renames, prop propagation, imports) | Concurrency, performance-critical paths, subtle correctness |
|
|
112
|
+
| Lint / formatting / type-error fixes | Ambiguous / underspecified requirements needing judgment |
|
|
113
|
+
| Docstrings, comments, README / changelog sections | Public API / interface design, breaking changes |
|
|
114
|
+
| Straightforward data transforms / migrations with a clear spec | Debugging unknown root causes |
|
|
115
|
+
| Obvious glue code / format conversions | Anything costly or hard to detect if wrong |
|
|
116
|
+
|
|
117
|
+
Full guide: [docs/DELEGATION.md](docs/DELEGATION.md)
|
|
118
|
+
|
|
119
|
+
## Configuration
|
|
120
|
+
|
|
121
|
+
Stored in `~/.forge-delegate/config.json`, read fresh on every call — change it anytime without
|
|
122
|
+
reinstalling. Four ways:
|
|
123
|
+
|
|
124
|
+
1. **From your agent** via the MCP tools `get_delegate_config` / `set_delegate_config`.
|
|
125
|
+
2. **Terminal CLI**: `forge-delegate config get`, `forge-delegate config set --model <m> --timeout <s> --dir <path> --autoApprove false`, `forge-delegate config path`.
|
|
126
|
+
3. **`setup` flags**: `--model`, `--targets`, `--scope`, `--project-dir`.
|
|
127
|
+
4. **Edit the file** directly.
|
|
128
|
+
|
|
129
|
+
| Field | Description | Default |
|
|
130
|
+
|---|---|---|
|
|
131
|
+
| `defaultModel` | provider/model used when a call omits `model` | unset |
|
|
132
|
+
| `defaultAgent` | opencode agent used for delegations | unset |
|
|
133
|
+
| `defaultVariant` | default reasoning-effort variant | unset |
|
|
134
|
+
| `autoApprove` | auto-approve permissions so headless delegations don't block | `true` |
|
|
135
|
+
| `timeoutMs` | per-delegation timeout | `900000` (env `FORGE_OPENCODE_TIMEOUT`) |
|
|
136
|
+
| `defaultDirectory` | default working directory for delegations | unset (server cwd) |
|
|
137
|
+
| `profiles` | named presets `{ name: { model, agent, variant, autoApprove, directory } }` | `{}` |
|
|
138
|
+
|
|
139
|
+
Every delegation tool also accepts per-call `model`, `directory`, `agent`, `autoApprove`, and
|
|
140
|
+
`profile` overrides. Per-call values beat profile values beat config defaults.
|
|
141
|
+
|
|
142
|
+
## Models
|
|
143
|
+
|
|
144
|
+
- **opencode provider** — free no-auth models (`opencode/mimo-v2.5-free`, ...) plus
|
|
145
|
+
anything you have logged in via `opencode auth login <provider>`.
|
|
146
|
+
- **Direct API from opencode logins** — `provider/model` for OpenAI-compatible providers.
|
|
147
|
+
- **Custom providers** — OpenAI-compatible providers defined in `opencode.json`
|
|
148
|
+
(`@ai-sdk/openai-compatible` + `baseURL`), callable as `provider/model`.
|
|
149
|
+
- **Ollama (local)** — `ollama:llama3`, `local:qwen2.5-coder`, no key needed.
|
|
150
|
+
|
|
151
|
+
## Safety notes
|
|
152
|
+
|
|
153
|
+
- `autoApprove: true` runs delegations non-interactively with tool permissions granted — point
|
|
154
|
+
them at repositories you trust and review the diffs. Set `autoApprove: false` to require
|
|
155
|
+
manual approval.
|
|
156
|
+
- This server never stores API keys — provider auth lives in opencode's own auth store.
|
|
157
|
+
- Usage is logged to `~/.forge-delegate/usage.jsonl`; see `forge-delegate stats`.
|
|
158
|
+
|
|
159
|
+
## CLI
|
|
160
|
+
|
|
161
|
+
```text
|
|
162
|
+
forge-delegate setup Register with Claude Code / Codex / opencode
|
|
163
|
+
forge-delegate serve Start the MCP server (used internally)
|
|
164
|
+
forge-delegate config get | set | path
|
|
165
|
+
forge-delegate status Show opencode logins and usage summary
|
|
166
|
+
forge-delegate stats Show detailed usage statistics
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
## Publishing (npm)
|
|
170
|
+
|
|
171
|
+
The primary install is `npx -y forge-delegate setup`, so the package must be on npm. Checklist:
|
|
172
|
+
|
|
173
|
+
1. `npm run prepublishOnly` — syntax-checks all entry files (runs automatically on publish).
|
|
174
|
+
2. `npm pack --dry-run` — confirm the tarball contains only `cli.mjs`, `server.mjs`, `lib.mjs`,
|
|
175
|
+
`install.sh`, `docs/`, `README.md` (via the `files` field), not `node_modules`.
|
|
176
|
+
3. Bump `version` in `package.json` (and the root `version` in `package-lock.json`).
|
|
177
|
+
4. `npm login && npm publish` — the `bin` field makes `npx forge-delegate …` work.
|
|
178
|
+
|
|
179
|
+
Also make sure the GitHub repo exists at `MuhamadBarzani/forge-delegate` with Pages enabled on
|
|
180
|
+
`/docs` (configurator) — the `raw.githubusercontent.com` install URL and the configurator's
|
|
181
|
+
hardcoded URL point there.
|
|
182
|
+
|
|
183
|
+
## License
|
|
184
|
+
|
|
185
|
+
MIT
|
package/cli.mjs
ADDED
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createInterface } from "node:readline/promises";
|
|
3
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
4
|
+
import { join, dirname } from "node:path";
|
|
5
|
+
import { homedir } from "node:os";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { execSync } from "node:child_process";
|
|
8
|
+
import { loadConfig, saveConfig, configPath } from "./lib.mjs";
|
|
9
|
+
|
|
10
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
11
|
+
|
|
12
|
+
function parseArgs(argv) {
|
|
13
|
+
const args = {};
|
|
14
|
+
for (let i = 0; i < argv.length; i++) {
|
|
15
|
+
const a = argv[i];
|
|
16
|
+
if (!a.startsWith("--")) continue;
|
|
17
|
+
const eq = a.indexOf("=");
|
|
18
|
+
const key = eq >= 0 ? a.slice(2, eq) : a.slice(2);
|
|
19
|
+
if (eq >= 0) args[key] = a.slice(eq + 1);
|
|
20
|
+
else if (i + 1 < argv.length && !argv[i + 1].startsWith("--")) {
|
|
21
|
+
args[key] = argv[i + 1];
|
|
22
|
+
i++;
|
|
23
|
+
} else args[key] = true;
|
|
24
|
+
}
|
|
25
|
+
return args;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function launchCommand() {
|
|
29
|
+
const isPublished = here.includes(join("node_modules", "forge-delegate")) || here.includes("/.npm/_npx/");
|
|
30
|
+
return isPublished ? "npx -y forge-delegate serve" : `node ${join(here, "cli.mjs")} serve`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function launchArray() {
|
|
34
|
+
const isPublished = here.includes(join("node_modules", "forge-delegate")) || here.includes("/.npm/_npx/");
|
|
35
|
+
return isPublished ? ["npx", "-y", "forge-delegate", "serve"] : [process.execPath, join(here, "cli.mjs"), "serve"];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function registerClaude(launch, scope) {
|
|
39
|
+
try {
|
|
40
|
+
execSync(`claude mcp remove --scope ${scope} forge-delegate`, { stdio: "ignore" });
|
|
41
|
+
} catch {}
|
|
42
|
+
try {
|
|
43
|
+
execSync(`claude mcp remove forge-delegate`, { stdio: "ignore" });
|
|
44
|
+
} catch {}
|
|
45
|
+
execSync(`claude mcp add --scope ${scope} forge-delegate -- ${launch}`, { stdio: "inherit" });
|
|
46
|
+
console.log(` ✓ Claude Code (scope=${scope})`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function codexConfigPath(scope, projectDir) {
|
|
50
|
+
return scope === "user" ? join(homedir(), ".codex", "config.toml") : join(projectDir, ".codex", "config.toml");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function registerCodex(launchArr, scope, projectDir) {
|
|
54
|
+
const path = codexConfigPath(scope, projectDir);
|
|
55
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
56
|
+
let toml = "";
|
|
57
|
+
try {
|
|
58
|
+
toml = readFileSync(path, "utf8");
|
|
59
|
+
} catch {}
|
|
60
|
+
const section = `[mcp_servers.forge-delegate]`;
|
|
61
|
+
const block = `${section}\ncommand = ${JSON.stringify(launchArr[0])}\nargs = ${JSON.stringify(launchArr.slice(1))}\n`;
|
|
62
|
+
const escaped = section.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
63
|
+
if (toml.includes(section)) {
|
|
64
|
+
toml = toml.replace(new RegExp(`${escaped}[\\s\\S]*?(?=\\n\\[|$)`), block.trimEnd());
|
|
65
|
+
} else {
|
|
66
|
+
toml += (toml.endsWith("\n") || toml === "" ? "" : "\n") + "\n" + block;
|
|
67
|
+
}
|
|
68
|
+
writeFileSync(path, toml);
|
|
69
|
+
console.log(` ✓ Codex (${path})`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function opencodeConfigPath(scope, projectDir) {
|
|
73
|
+
return scope === "user" ? join(homedir(), ".config", "opencode", "opencode.json") : join(projectDir, "opencode.json");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function registerOpencode(launchArr, scope, projectDir) {
|
|
77
|
+
const path = opencodeConfigPath(scope, projectDir);
|
|
78
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
79
|
+
let cfg = {};
|
|
80
|
+
try {
|
|
81
|
+
cfg = JSON.parse(readFileSync(path, "utf8"));
|
|
82
|
+
} catch {}
|
|
83
|
+
cfg.mcp = cfg.mcp ?? {};
|
|
84
|
+
cfg.mcp["forge-delegate"] = { type: "local", command: launchArr, enabled: true };
|
|
85
|
+
writeFileSync(path, JSON.stringify(cfg, null, 2) + "\n");
|
|
86
|
+
console.log(` ✓ opencode (${path})`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function setup() {
|
|
90
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
91
|
+
const ask = (q) => rl.question(q);
|
|
92
|
+
const flags = parseArgs(process.argv.slice(3));
|
|
93
|
+
const current = loadConfig();
|
|
94
|
+
|
|
95
|
+
// Flags win; anything omitted is asked interactively (Enter accepts the [default]).
|
|
96
|
+
// In a non-terminal (piped/automated) run, prompts are skipped and defaults are used.
|
|
97
|
+
const interactive = !!process.stdin.isTTY;
|
|
98
|
+
let model = flags.model;
|
|
99
|
+
let targets = flags.targets;
|
|
100
|
+
let scope = flags.scope;
|
|
101
|
+
let projectDir = flags["project-dir"];
|
|
102
|
+
|
|
103
|
+
console.log("\n=== forge-delegate setup ===\n");
|
|
104
|
+
if (interactive) console.log("Press Enter to accept the suggested [default], or type a value.\n");
|
|
105
|
+
|
|
106
|
+
if (!model) {
|
|
107
|
+
const def = current.defaultModel ?? "opencode/mimo-v2.5-free";
|
|
108
|
+
model = interactive ? ((await ask(`Default model [${def}]: `)) ?? "").trim() || def : def;
|
|
109
|
+
}
|
|
110
|
+
if (!targets) targets = "claude,codex,opencode";
|
|
111
|
+
if (!scope) scope = "user";
|
|
112
|
+
if (scope === "project" && !projectDir) projectDir = process.cwd();
|
|
113
|
+
projectDir = projectDir ?? process.cwd();
|
|
114
|
+
targets = targets.split(",").map((s) => s.trim().toLowerCase());
|
|
115
|
+
|
|
116
|
+
const launch = launchCommand();
|
|
117
|
+
const launchArr = launchArray();
|
|
118
|
+
|
|
119
|
+
// 1. Set the default model first so it's saved even if registration hits an issue
|
|
120
|
+
if (model && model !== current.defaultModel) {
|
|
121
|
+
saveConfig({ defaultModel: model });
|
|
122
|
+
console.log(` ✓ default model set: ${model}`);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// 2. Register with chosen hosts (claude / codex / opencode)
|
|
126
|
+
for (const target of targets) {
|
|
127
|
+
try {
|
|
128
|
+
if (target === "claude") registerClaude(launch, scope);
|
|
129
|
+
else if (target === "codex") registerCodex(launchArr, scope, projectDir);
|
|
130
|
+
else if (target === "opencode") registerOpencode(launchArr, scope, projectDir);
|
|
131
|
+
else console.log(` ✗ unknown target '${target}' (use claude,codex,opencode)`);
|
|
132
|
+
} catch (e) {
|
|
133
|
+
console.log(` ✗ ${target}: ${e.message}`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// 3. Check opencode logins
|
|
138
|
+
let authCount = 0;
|
|
139
|
+
try {
|
|
140
|
+
const authPath = join(homedir(), ".local/share/opencode/auth.json");
|
|
141
|
+
authCount = Object.keys(JSON.parse(readFileSync(authPath, "utf8"))).length;
|
|
142
|
+
} catch {}
|
|
143
|
+
if (authCount > 0) {
|
|
144
|
+
console.log(`Found ${authCount} opencode provider login(s) — ready to use.`);
|
|
145
|
+
} else {
|
|
146
|
+
console.log("\nNo opencode logins found. To enable models, run:");
|
|
147
|
+
console.log(" opencode auth login <provider> (e.g. opencode auth login deepseek)");
|
|
148
|
+
if (interactive) await ask("Press Enter when done (or Ctrl+C to skip): ");
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
console.log(`
|
|
152
|
+
Done! Restart your agent, then run /mcp (Claude Code) or \`codex mcp\` / \`opencode\` to confirm "forge-delegate" is connected.
|
|
153
|
+
Use list_models inside the agent to see everything available.
|
|
154
|
+
`);
|
|
155
|
+
rl.close();
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function configCmd(argv) {
|
|
159
|
+
const sub = argv[0];
|
|
160
|
+
if (sub === "path") {
|
|
161
|
+
console.log(configPath());
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
const flags = parseArgs(argv);
|
|
165
|
+
if (sub === "set") {
|
|
166
|
+
const patch = {};
|
|
167
|
+
if (flags.model) patch.defaultModel = flags.model;
|
|
168
|
+
if (flags.agent) patch.defaultAgent = flags.agent;
|
|
169
|
+
if (flags.variant) patch.defaultVariant = flags.variant;
|
|
170
|
+
if (flags.timeout) patch.timeoutMs = Number(flags.timeout) * 1000;
|
|
171
|
+
if (flags.dir) patch.defaultDirectory = flags.dir;
|
|
172
|
+
if (flags["default-dir"]) patch.defaultDirectory = flags["default-dir"];
|
|
173
|
+
if (flags.autoApprove !== undefined) patch.autoApprove = flags.autoApprove === "true" || flags.autoApprove === "1" || flags.autoApprove === true;
|
|
174
|
+
saveConfig(patch);
|
|
175
|
+
console.log(`updated ${configPath()}`);
|
|
176
|
+
}
|
|
177
|
+
const cfg = loadConfig();
|
|
178
|
+
console.log(JSON.stringify(cfg, null, 2));
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async function status() {
|
|
182
|
+
let providers = [];
|
|
183
|
+
try {
|
|
184
|
+
const authPath = join(homedir(), ".local/share/opencode/auth.json");
|
|
185
|
+
const { listAuthDirectProviders } = await import("./lib.mjs");
|
|
186
|
+
const auth = JSON.parse(readFileSync(authPath, "utf8"));
|
|
187
|
+
providers = Object.keys(auth);
|
|
188
|
+
} catch {}
|
|
189
|
+
console.log(`\nopencode logins: ${providers.length ? providers.join(", ") : "none (run 'opencode auth login <provider>')"}\n`);
|
|
190
|
+
try {
|
|
191
|
+
const usagePath = join(homedir(), ".forge-delegate", "usage.jsonl");
|
|
192
|
+
if (existsSync(usagePath)) {
|
|
193
|
+
const lines = readFileSync(usagePath, "utf8").trim().split("\n").length;
|
|
194
|
+
console.log(`recorded delegations: ${lines} (see 'forge-delegate stats')`);
|
|
195
|
+
}
|
|
196
|
+
} catch {}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function stats() {
|
|
200
|
+
const usagePath = join(homedir(), ".forge-delegate", "usage.jsonl");
|
|
201
|
+
if (!existsSync(usagePath)) {
|
|
202
|
+
console.log("no usage recorded yet");
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
const byKey = new Map();
|
|
206
|
+
for (const line of readFileSync(usagePath, "utf8").split("\n")) {
|
|
207
|
+
if (!line.trim()) continue;
|
|
208
|
+
try {
|
|
209
|
+
const u = JSON.parse(line);
|
|
210
|
+
const key = `${u.tool} · ${u.model}`;
|
|
211
|
+
const s = byKey.get(key) ?? { calls: 0, ok: 0, ms: 0, tokens: 0 };
|
|
212
|
+
s.calls++;
|
|
213
|
+
if (u.ok) s.ok++;
|
|
214
|
+
s.ms += u.ms ?? 0;
|
|
215
|
+
s.tokens += u.tokens ?? 0;
|
|
216
|
+
byKey.set(key, s);
|
|
217
|
+
} catch {}
|
|
218
|
+
}
|
|
219
|
+
console.log("\nforge-delegate usage\n");
|
|
220
|
+
console.log("calls ok% avg(s) tokens tool · model");
|
|
221
|
+
for (const [key, s] of [...byKey.entries()].sort((a, b) => b[1].calls - a[1].calls)) {
|
|
222
|
+
console.log(
|
|
223
|
+
String(s.calls).padStart(5),
|
|
224
|
+
Math.round((s.ok / s.calls) * 100).toString().padStart(4) + "%",
|
|
225
|
+
(s.ms / s.calls / 1000).toFixed(1).padStart(7),
|
|
226
|
+
s.tokens ? String(s.tokens).padStart(8) : " —",
|
|
227
|
+
" ", key
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
console.log(`\nlog: ${usagePath}\n`);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function help() {
|
|
234
|
+
console.log(`
|
|
235
|
+
forge-delegate — delegate coding work from Claude Code/Codex/opencode to other AI models
|
|
236
|
+
|
|
237
|
+
Usage:
|
|
238
|
+
forge-delegate setup [--model provider/model] [--targets claude,codex,opencode]
|
|
239
|
+
[--scope user|project] [--project-dir <path>]
|
|
240
|
+
Interactive setup; flags optional (run bare to be prompted)
|
|
241
|
+
forge-delegate serve Start the MCP server (used internally by agents)
|
|
242
|
+
forge-delegate config get Show current configuration
|
|
243
|
+
forge-delegate config set --model <m> [--agent <a>] [--variant <v>]
|
|
244
|
+
[--timeout <s>] [--dir <path>] [--autoApprove true|false]
|
|
245
|
+
forge-delegate config path Print config file path
|
|
246
|
+
forge-delegate status Show opencode logins and usage summary
|
|
247
|
+
forge-delegate stats Show detailed usage statistics
|
|
248
|
+
|
|
249
|
+
Models come from your opencode installation:
|
|
250
|
+
opencode auth login <provider> add a provider
|
|
251
|
+
opencode models list available models
|
|
252
|
+
`);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const cmd = process.argv[2] ?? "help";
|
|
256
|
+
if (cmd === "setup") await setup();
|
|
257
|
+
else if (cmd === "serve") await import("./server.mjs");
|
|
258
|
+
else if (cmd === "config") configCmd(process.argv.slice(3));
|
|
259
|
+
else if (cmd === "status") await status();
|
|
260
|
+
else if (cmd === "stats") stats();
|
|
261
|
+
else help();
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# Delegation guide
|
|
2
|
+
|
|
3
|
+
forge-delegate exists so your primary (expensive) agent stays focused on the decisions that
|
|
4
|
+
need its judgment, while a cheap model grinds through the mechanical majority of coding work.
|
|
5
|
+
|
|
6
|
+
## When does the primary agent delegate?
|
|
7
|
+
|
|
8
|
+
Installing the tools doesn't force an agent to use them — it decides, guided by the
|
|
9
|
+
`instructions` that forge-delegate injects into its context over MCP. Those instructions
|
|
10
|
+
contain the cost policy, the delegate-vs-keep rules below, and the routing rules for which
|
|
11
|
+
tool to call. Edit them by editing the `INSTRUCTIONS` string in `server.mjs`.
|
|
12
|
+
|
|
13
|
+
## What to delegate vs. keep
|
|
14
|
+
|
|
15
|
+
| ✅ Delegate — high volume, low risk | ⛔ Keep on the primary agent |
|
|
16
|
+
|---|---|
|
|
17
|
+
| Tests for well-specified behavior | Architecture, system design, choosing abstractions |
|
|
18
|
+
| Boilerplate & scaffolding (CRUD, DTOs, fixtures, mocks) | Security-sensitive code (auth, crypto, secrets, permissions) |
|
|
19
|
+
| Mechanical edits across many files (renames, prop propagation, imports) | Concurrency, performance-critical paths, subtle correctness |
|
|
20
|
+
| Lint / formatting / type-error fixes | Ambiguous / underspecified requirements needing judgment |
|
|
21
|
+
| Docstrings, comments, README / changelog sections | Public API / interface design, breaking changes |
|
|
22
|
+
| Straightforward data transforms / migrations with a clear spec | Debugging unknown root causes |
|
|
23
|
+
| Obvious glue code / format conversions | Anything costly or hard to detect if wrong |
|
|
24
|
+
|
|
25
|
+
**Rule of thumb:** high volume + low risk → delegate. Low volume + high risk → keep it.
|
|
26
|
+
Unsure → keep it.
|
|
27
|
+
|
|
28
|
+
## Which tool?
|
|
29
|
+
|
|
30
|
+
| Need | Tool |
|
|
31
|
+
|---|---|
|
|
32
|
+
| Quick one-shot question, no file access needed | `ask_model` |
|
|
33
|
+
| A self-contained task with concrete code output | `delegate_task` |
|
|
34
|
+
| Autonomous multi-file work, don't want to block | `delegate_agent` + `check_delegation` |
|
|
35
|
+
| See what's available | `list_models` |
|
|
36
|
+
|
|
37
|
+
## Cost policy (non-negotiable)
|
|
38
|
+
|
|
39
|
+
The user pays per delegated call on a budget plan. **Always pick cheap or free models.**
|
|
40
|
+
Never pick top-tier/premium/frontier models — not even "just this once" or for hard tasks.
|
|
41
|
+
If a task seems too hard for a cheap model, split it into smaller delegated steps instead of
|
|
42
|
+
upgrading the model.
|
|
43
|
+
|
|
44
|
+
## Preconditions for a good delegation
|
|
45
|
+
|
|
46
|
+
1. **Self-contained prompt.** State the goal, constraints, and acceptance criteria. Don't
|
|
47
|
+
assume the cheap model saw the surrounding conversation.
|
|
48
|
+
2. **Right working directory.** Pass `directory:` (or set `defaultDirectory`) — don't rely on
|
|
49
|
+
the server's inherited cwd.
|
|
50
|
+
3. **Bound the blast radius.** Auto-approve is on by default, so point delegations at
|
|
51
|
+
repositories you trust and review the diffs afterward.
|
|
52
|
+
4. **Follow up cheaply.** Keep the returned `session` id and pass it as `continue_session` for
|
|
53
|
+
follow-ups — the agent keeps its context instead of re-exploring.
|
|
54
|
+
|
|
55
|
+
## Tuning
|
|
56
|
+
|
|
57
|
+
- Change the default model / directory / auto-approve anytime: `set_delegate_config`, or
|
|
58
|
+
`forge-delegate config set`.
|
|
59
|
+
- Save named presets in `profiles` (e.g. `tests`, `quick`) and reference them per call with
|
|
60
|
+
`profile: "name"`.
|
|
61
|
+
- Raise/lower the per-delegation timeout with `timeoutMs` (env default `FORGE_OPENCODE_TIMEOUT`).
|