claude-overnight 1.13.1 → 1.16.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 +40 -5
- package/dist/bin.d.ts +2 -0
- package/dist/bin.js +27 -0
- package/dist/cli.js +1 -1
- package/dist/index.js +216 -22
- package/dist/planner-query.d.ts +1 -0
- package/dist/planner-query.js +11 -0
- package/dist/providers.d.ts +61 -0
- package/dist/providers.js +243 -0
- package/dist/render.js +22 -14
- package/dist/run.d.ts +5 -0
- package/dist/run.js +19 -5
- package/dist/swarm.d.ts +14 -1
- package/dist/swarm.js +80 -7
- package/dist/types.d.ts +5 -0
- package/dist/types.js +4 -0
- package/dist/ui.d.ts +2 -0
- package/dist/ui.js +22 -2
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@ Run 10, 100, or 1000 Claude agents overnight. Come back to shipped work.
|
|
|
4
4
|
|
|
5
5
|
Describe what to build. Set a budget. The tool plans, explores your codebase, breaks the objective into tasks, launches parallel agents in isolated git worktrees, iterates toward quality, and handles rate limits automatically. You press Run once, then go to sleep.
|
|
6
6
|
|
|
7
|
-
Built on the [Claude Agent SDK](https://www.npmjs.com/package/@anthropic-ai/claude-agent-sdk). Works with Claude Opus, Sonnet, and Haiku.
|
|
7
|
+
Built on the [Claude Agent SDK](https://www.npmjs.com/package/@anthropic-ai/claude-agent-sdk). Works with Claude Opus, Sonnet, and Haiku — or route executors to Qwen / OpenRouter / any Anthropic-compatible endpoint via the `Other…` picker.
|
|
8
8
|
|
|
9
9
|
## Install
|
|
10
10
|
|
|
@@ -38,14 +38,19 @@ claude-overnight
|
|
|
38
38
|
|
|
39
39
|
② Budget [10]: 200
|
|
40
40
|
|
|
41
|
-
|
|
41
|
+
④ Planner model (thinking, steering — use your strongest):
|
|
42
|
+
● Opus — Opus 4.6 · Most capable
|
|
43
|
+
○ Sonnet — Sonnet 4.6 · Best for everyday tasks
|
|
44
|
+
|
|
45
|
+
⑤ Executor model (what runs the tasks — Qwen/OpenRouter/etc via Other…):
|
|
42
46
|
● Sonnet — Sonnet 4.6 · Best for everyday tasks
|
|
43
47
|
○ Opus — Opus 4.6 · Most capable
|
|
48
|
+
○ Other… · custom OpenAI/Anthropic-compatible endpoint
|
|
44
49
|
|
|
45
|
-
|
|
50
|
+
⑥ Usage cap:
|
|
46
51
|
● 90% · leave 10% for other work
|
|
47
52
|
|
|
48
|
-
|
|
53
|
+
⑦ Allow extra usage (billed separately):
|
|
49
54
|
● No · stop when plan limits are reached
|
|
50
55
|
|
|
51
56
|
╭──────────────────────────────────────────────────╮
|
|
@@ -188,7 +193,7 @@ claude-overnight "fix auth bug in src/auth.ts" "add tests for user model"
|
|
|
188
193
|
|---|---|---|
|
|
189
194
|
| `--budget=N` | `10` | Total agent sessions |
|
|
190
195
|
| `--concurrency=N` | `5` | Parallel agents |
|
|
191
|
-
| `--model=NAME` | prompted | Worker model
|
|
196
|
+
| `--model=NAME` | prompted | Worker model — interactive picks planner + executor separately; `Other…` adds Qwen / OpenRouter / any Anthropic-compat endpoint. In non-interactive mode, a saved provider's model id is auto-resolved to the provider. |
|
|
192
197
|
| `--usage-cap=N` | unlimited | Stop at N% utilization |
|
|
193
198
|
| `--allow-extra-usage` | off | Allow extra/overage usage (billed separately) |
|
|
194
199
|
| `--extra-usage-budget=N` | — | Max $ for extra usage (implies --allow-extra-usage) |
|
|
@@ -210,6 +215,36 @@ claude-overnight "fix auth bug in src/auth.ts" "add tests for user model"
|
|
|
210
215
|
| `mergeStrategy` | `"yolo" \| "branch"` | `"yolo"` | Merge into HEAD or new branch |
|
|
211
216
|
| `usageCap` | `number (0-100)` | unlimited | Stop at N% utilization |
|
|
212
217
|
|
|
218
|
+
## Custom providers (Qwen, OpenRouter, anything Anthropic-compatible)
|
|
219
|
+
|
|
220
|
+
Planner and executor are picked separately — pair Opus-on-Anthropic for the planner/thinker with a cheaper model on another provider for the bulk of execution.
|
|
221
|
+
|
|
222
|
+
From the interactive picker, choose `Other…` on the planner or executor step:
|
|
223
|
+
|
|
224
|
+
```
|
|
225
|
+
⑤ Executor model (what runs the tasks — Qwen/OpenRouter/etc via Other…):
|
|
226
|
+
○ Sonnet
|
|
227
|
+
○ Opus
|
|
228
|
+
● Other…
|
|
229
|
+
|
|
230
|
+
Name: Qwen Coder
|
|
231
|
+
Base URL: https://dashscope-intl.aliyuncs.com/api/v2/apps/claude-code-proxy
|
|
232
|
+
Model id: qwen3-coder-plus
|
|
233
|
+
API key source:
|
|
234
|
+
● Paste key now · stored plaintext in ~/.claude/claude-overnight/providers.json (0600)
|
|
235
|
+
○ Read from env var · nothing written to disk
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
Saved providers live user-level at `~/.claude/claude-overnight/providers.json` (mode 0600) and show up automatically in every repo. No per-project config.
|
|
239
|
+
|
|
240
|
+
**How routing works.** Each `query()` gets its own env override (`ANTHROPIC_BASE_URL` + `ANTHROPIC_AUTH_TOKEN`) — planner queries use the planner provider, executor queries use the executor provider. No global shell env, no proxy daemon, no `process.env` pollution between calls.
|
|
241
|
+
|
|
242
|
+
**Pre-flight.** Before the swarm starts, each custom provider is pinged with a 1-turn auth check. Bad keys fail fast with `✗ executor preflight failed: ...` instead of N scattered mid-run errors.
|
|
243
|
+
|
|
244
|
+
**Resume.** Provider ids are persisted in `run.json` and rehydrated on resume. If you deleted a provider between runs, resume refuses to start and tells you exactly which id is missing.
|
|
245
|
+
|
|
246
|
+
**Non-interactive / CI.** `claude-overnight --model=qwen3-coder-plus` auto-resolves the model id to a saved provider — no separate `--provider` flag.
|
|
247
|
+
|
|
213
248
|
## Usage controls
|
|
214
249
|
|
|
215
250
|
### Extra usage protection
|
package/dist/bin.d.ts
ADDED
package/dist/bin.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Tiny launcher: prints a splash the instant node is ready, then dynamically
|
|
3
|
+
// imports the real entrypoint. Loading `@anthropic-ai/claude-agent-sdk` and the
|
|
4
|
+
// rest of the module graph takes several seconds on a cold cache — without
|
|
5
|
+
// this, the terminal sits black that whole time. index.ts stops the splash
|
|
6
|
+
// via `globalThis.__coStopSplash` as soon as its header is about to print.
|
|
7
|
+
const argv = process.argv.slice(2);
|
|
8
|
+
const quiet = argv.includes("-h") || argv.includes("--help") || argv.includes("-v") || argv.includes("--version");
|
|
9
|
+
if (!quiet && process.stdout.isTTY) {
|
|
10
|
+
const frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
11
|
+
let i = 0;
|
|
12
|
+
const render = () => process.stdout.write(`\r\x1b[2K 🌙 \x1b[1mclaude-overnight\x1b[0m \x1b[2m${frames[i++ % frames.length]} starting…\x1b[0m`);
|
|
13
|
+
render();
|
|
14
|
+
const timer = setInterval(render, 120);
|
|
15
|
+
let stopped = false;
|
|
16
|
+
const stop = () => {
|
|
17
|
+
if (stopped)
|
|
18
|
+
return;
|
|
19
|
+
stopped = true;
|
|
20
|
+
clearInterval(timer);
|
|
21
|
+
process.stdout.write("\r\x1b[2K");
|
|
22
|
+
};
|
|
23
|
+
globalThis.__coStopSplash = stop;
|
|
24
|
+
process.once("exit", stop);
|
|
25
|
+
}
|
|
26
|
+
await import("./index.js");
|
|
27
|
+
export {};
|
package/dist/cli.js
CHANGED
|
@@ -55,7 +55,7 @@ export async function fetchModels(timeoutMs = 10_000) {
|
|
|
55
55
|
clearTimeout(timer);
|
|
56
56
|
q?.close();
|
|
57
57
|
if (err.message === "model_fetch_timeout") {
|
|
58
|
-
|
|
58
|
+
// Silent: callers fall back to a text prompt with the current value as default.
|
|
59
59
|
}
|
|
60
60
|
else if (isAuthError(err)) {
|
|
61
61
|
console.error(chalk.red("\n Authentication failed — check your API key or run: claude auth\n"));
|
package/dist/index.js
CHANGED
|
@@ -6,7 +6,8 @@ import chalk from "chalk";
|
|
|
6
6
|
import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
7
7
|
import { Swarm } from "./swarm.js";
|
|
8
8
|
import { planTasks, refinePlan, identifyThemes, buildThinkingTasks, orchestrate, salvageFromFile } from "./planner.js";
|
|
9
|
-
import { detectModelTier } from "./planner-query.js";
|
|
9
|
+
import { detectModelTier, setPlannerEnvResolver } from "./planner-query.js";
|
|
10
|
+
import { pickModel, loadProviders, preflightProvider, buildEnvResolver } from "./providers.js";
|
|
10
11
|
import { RunDisplay } from "./ui.js";
|
|
11
12
|
import { renderSummary } from "./render.js";
|
|
12
13
|
import { executeRun } from "./run.js";
|
|
@@ -21,6 +22,140 @@ function countTasksInFile(path) {
|
|
|
21
22
|
return 0;
|
|
22
23
|
}
|
|
23
24
|
}
|
|
25
|
+
async function promptResumeOverrides(state, cliFlags, argv, noTTY, runDir) {
|
|
26
|
+
// ── Apply CLI flag overrides first ──
|
|
27
|
+
if (cliFlags.model)
|
|
28
|
+
state.workerModel = cliFlags.model;
|
|
29
|
+
if (cliFlags.concurrency) {
|
|
30
|
+
const n = parseInt(cliFlags.concurrency);
|
|
31
|
+
if (n >= 1)
|
|
32
|
+
state.concurrency = n;
|
|
33
|
+
}
|
|
34
|
+
if (cliFlags.budget) {
|
|
35
|
+
const n = parseInt(cliFlags.budget);
|
|
36
|
+
if (n > 0) {
|
|
37
|
+
state.remaining = n;
|
|
38
|
+
state.budget = state.accCompleted + state.accFailed + n;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
if (cliFlags["usage-cap"] != null) {
|
|
42
|
+
const v = parseFloat(cliFlags["usage-cap"]);
|
|
43
|
+
if (!isNaN(v) && v >= 0 && v <= 100)
|
|
44
|
+
state.usageCap = v > 0 ? v / 100 : undefined;
|
|
45
|
+
}
|
|
46
|
+
if (cliFlags["extra-usage-budget"] != null) {
|
|
47
|
+
const v = parseFloat(cliFlags["extra-usage-budget"]);
|
|
48
|
+
if (!isNaN(v) && v > 0) {
|
|
49
|
+
state.extraUsageBudget = v;
|
|
50
|
+
state.allowExtraUsage = true;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if (argv.includes("--allow-extra-usage"))
|
|
54
|
+
state.allowExtraUsage = true;
|
|
55
|
+
if (cliFlags.perm)
|
|
56
|
+
state.permissionMode = cliFlags.perm;
|
|
57
|
+
if (noTTY) {
|
|
58
|
+
try {
|
|
59
|
+
saveRunState(runDir, state);
|
|
60
|
+
}
|
|
61
|
+
catch { }
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
// Kick off model fetch in the background so it's ready if the user picks Edit.
|
|
65
|
+
const modelsPromise = fetchModels(20_000).catch(() => []);
|
|
66
|
+
// ── Interactive review ──
|
|
67
|
+
const fmtSummary = () => {
|
|
68
|
+
const tier = detectModelTier(state.workerModel);
|
|
69
|
+
const remaining = Math.max(1, state.remaining);
|
|
70
|
+
const capStr = state.usageCap != null ? `${Math.round(state.usageCap * 100)}%` : "unlimited";
|
|
71
|
+
const extraStr = state.allowExtraUsage
|
|
72
|
+
? (state.extraUsageBudget ? `$${state.extraUsageBudget}` : "unlimited")
|
|
73
|
+
: "off";
|
|
74
|
+
console.log();
|
|
75
|
+
console.log(` ${chalk.dim("Resume settings")}`);
|
|
76
|
+
console.log(` ${chalk.dim("─".repeat(40))}`);
|
|
77
|
+
console.log(` ${chalk.dim("model ")}${chalk.white(state.workerModel)} ${chalk.dim(`(${tier})`)}`);
|
|
78
|
+
console.log(` ${chalk.dim("remaining ")}${chalk.white(String(remaining))} ${chalk.dim("sessions")}`);
|
|
79
|
+
console.log(` ${chalk.dim("concur ")}${chalk.white(String(state.concurrency))}`);
|
|
80
|
+
console.log(` ${chalk.dim("usage cap ")}${chalk.white(capStr)}`);
|
|
81
|
+
console.log(` ${chalk.dim("extra ")}${chalk.white(extraStr)}`);
|
|
82
|
+
};
|
|
83
|
+
fmtSummary();
|
|
84
|
+
const action = await selectKey("", [
|
|
85
|
+
{ key: "r", desc: "esume" },
|
|
86
|
+
{ key: "e", desc: "dit" },
|
|
87
|
+
{ key: "q", desc: "uit" },
|
|
88
|
+
]);
|
|
89
|
+
if (action === "q")
|
|
90
|
+
process.exit(0);
|
|
91
|
+
if (action === "r")
|
|
92
|
+
return;
|
|
93
|
+
// ── Edit walk ──
|
|
94
|
+
let modelFrame = 0;
|
|
95
|
+
const modelSpinner = setInterval(() => {
|
|
96
|
+
process.stdout.write(`\x1B[2K\r ${chalk.cyan(BRAILLE[modelFrame++ % BRAILLE.length])} ${chalk.dim("loading models...")}`);
|
|
97
|
+
}, 120);
|
|
98
|
+
let models;
|
|
99
|
+
try {
|
|
100
|
+
models = await modelsPromise;
|
|
101
|
+
}
|
|
102
|
+
finally {
|
|
103
|
+
clearInterval(modelSpinner);
|
|
104
|
+
process.stdout.write(`\x1B[2K\r`);
|
|
105
|
+
}
|
|
106
|
+
const pick = await pickModel(`${chalk.cyan("①")} Worker model:`, models, state.workerProviderId ?? state.workerModel);
|
|
107
|
+
state.workerModel = pick.model;
|
|
108
|
+
state.workerProviderId = pick.providerId;
|
|
109
|
+
const remAns = await ask(`\n ${chalk.cyan("②")} Remaining sessions ${chalk.dim(`[${state.remaining}]:`)} `);
|
|
110
|
+
const parsedRem = parseInt(remAns);
|
|
111
|
+
if (!isNaN(parsedRem) && parsedRem > 0) {
|
|
112
|
+
state.remaining = parsedRem;
|
|
113
|
+
state.budget = state.accCompleted + state.accFailed + parsedRem;
|
|
114
|
+
}
|
|
115
|
+
const concAns = await ask(`\n ${chalk.cyan("③")} Concurrency ${chalk.dim(`[${state.concurrency}]:`)} `);
|
|
116
|
+
const parsedConc = parseInt(concAns);
|
|
117
|
+
if (!isNaN(parsedConc) && parsedConc >= 1)
|
|
118
|
+
state.concurrency = parsedConc;
|
|
119
|
+
const currentCap = state.usageCap != null ? String(Math.round(state.usageCap * 100)) : "off";
|
|
120
|
+
const capAns = await ask(`\n ${chalk.cyan("④")} Usage cap % ${chalk.dim(`[${currentCap}]`)} ${chalk.dim("(0 = off):")} `);
|
|
121
|
+
if (capAns.trim()) {
|
|
122
|
+
const v = parseFloat(capAns);
|
|
123
|
+
if (!isNaN(v) && v >= 0 && v <= 100)
|
|
124
|
+
state.usageCap = v > 0 ? v / 100 : undefined;
|
|
125
|
+
}
|
|
126
|
+
const currentExtra = state.allowExtraUsage
|
|
127
|
+
? (state.extraUsageBudget ? `$${state.extraUsageBudget}` : "unlimited")
|
|
128
|
+
: "off";
|
|
129
|
+
const extraChoice = await select(`${chalk.cyan("⑤")} Extra usage ${chalk.dim(`[current: ${currentExtra}]`)}:`, [
|
|
130
|
+
{ name: "Keep current", value: "keep" },
|
|
131
|
+
{ name: "Off", value: "off", hint: "stop at plan limit" },
|
|
132
|
+
{ name: "With $ cap", value: "budget", hint: "set a spending cap" },
|
|
133
|
+
{ name: "Unlimited", value: "unlimited", hint: "no cap, billed as overage" },
|
|
134
|
+
]);
|
|
135
|
+
if (extraChoice === "off") {
|
|
136
|
+
state.allowExtraUsage = false;
|
|
137
|
+
state.extraUsageBudget = undefined;
|
|
138
|
+
}
|
|
139
|
+
else if (extraChoice === "budget") {
|
|
140
|
+
const bAns = await ask(` ${chalk.dim("Max extra $:")} `);
|
|
141
|
+
const bVal = parseFloat(bAns);
|
|
142
|
+
if (!isNaN(bVal) && bVal > 0) {
|
|
143
|
+
state.extraUsageBudget = bVal;
|
|
144
|
+
state.allowExtraUsage = true;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
else if (extraChoice === "unlimited") {
|
|
148
|
+
state.allowExtraUsage = true;
|
|
149
|
+
state.extraUsageBudget = undefined;
|
|
150
|
+
}
|
|
151
|
+
try {
|
|
152
|
+
saveRunState(runDir, state);
|
|
153
|
+
}
|
|
154
|
+
catch { }
|
|
155
|
+
console.log(chalk.green("\n ✓ Settings updated"));
|
|
156
|
+
fmtSummary();
|
|
157
|
+
console.log();
|
|
158
|
+
}
|
|
24
159
|
async function main() {
|
|
25
160
|
const argv = process.argv.slice(2);
|
|
26
161
|
if (argv.includes("-v") || argv.includes("--version")) {
|
|
@@ -45,7 +180,7 @@ async function main() {
|
|
|
45
180
|
--dry-run Show planned tasks without running them
|
|
46
181
|
--budget=N Target number of agent runs ${chalk.dim("(default: 10)")}
|
|
47
182
|
--concurrency=N Max parallel agents ${chalk.dim("(default: 5)")}
|
|
48
|
-
--model=NAME Worker model override ${chalk.dim("(planner
|
|
183
|
+
--model=NAME Worker model override ${chalk.dim("(interactive mode picks planner + executor separately — supports 'Other…' for Qwen / OpenRouter / etc.)")}
|
|
49
184
|
--usage-cap=N Stop at N% utilization ${chalk.dim("(e.g. 90 to save 10% for other work)")}
|
|
50
185
|
--allow-extra-usage Allow extra/overage usage ${chalk.dim("(default: stop when plan limits hit)")}
|
|
51
186
|
--extra-usage-budget=N Max $ for extra usage ${chalk.dim("(implies --allow-extra-usage)")}
|
|
@@ -108,7 +243,9 @@ async function main() {
|
|
|
108
243
|
}
|
|
109
244
|
}
|
|
110
245
|
// ── Mode detection ──
|
|
111
|
-
|
|
246
|
+
// Stop the bin.ts startup splash (if any) before printing our header.
|
|
247
|
+
globalThis.__coStopSplash?.();
|
|
248
|
+
console.log(` ${chalk.bold("🌙 claude-overnight")}`);
|
|
112
249
|
console.log(chalk.dim(` ${"─".repeat(36)}`));
|
|
113
250
|
const noTTY = !process.stdin.isTTY;
|
|
114
251
|
const nonInteractive = noTTY || fileCfg !== undefined || tasks.length > 0;
|
|
@@ -310,11 +447,14 @@ async function main() {
|
|
|
310
447
|
}
|
|
311
448
|
catch { }
|
|
312
449
|
}
|
|
450
|
+
await promptResumeOverrides(resumeState, cliFlags, argv, noTTY, resumeRunDir);
|
|
313
451
|
}
|
|
314
452
|
}
|
|
315
453
|
// ── Config resolution ──
|
|
316
454
|
let workerModel;
|
|
317
455
|
let plannerModel;
|
|
456
|
+
let workerProvider;
|
|
457
|
+
let plannerProvider;
|
|
318
458
|
let budget;
|
|
319
459
|
let concurrency;
|
|
320
460
|
let objective = fileCfg?.objective;
|
|
@@ -327,6 +467,23 @@ async function main() {
|
|
|
327
467
|
if (resuming) {
|
|
328
468
|
workerModel = resumeState.workerModel;
|
|
329
469
|
plannerModel = resumeState.plannerModel;
|
|
470
|
+
const saved = loadProviders();
|
|
471
|
+
if (resumeState.workerProviderId) {
|
|
472
|
+
workerProvider = saved.find(p => p.id === resumeState.workerProviderId);
|
|
473
|
+
if (!workerProvider) {
|
|
474
|
+
console.error(chalk.red(`\n Resume aborted: worker provider "${resumeState.workerProviderId}" is no longer in ~/.claude/claude-overnight/providers.json`));
|
|
475
|
+
console.error(chalk.dim(` Re-add it via a fresh run's "Other…" flow, or start Fresh instead.\n`));
|
|
476
|
+
process.exit(1);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
if (resumeState.plannerProviderId) {
|
|
480
|
+
plannerProvider = saved.find(p => p.id === resumeState.plannerProviderId);
|
|
481
|
+
if (!plannerProvider) {
|
|
482
|
+
console.error(chalk.red(`\n Resume aborted: planner provider "${resumeState.plannerProviderId}" is no longer in ~/.claude/claude-overnight/providers.json`));
|
|
483
|
+
console.error(chalk.dim(` Re-add it via a fresh run's "Other…" flow, or start Fresh instead.\n`));
|
|
484
|
+
process.exit(1);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
330
487
|
budget = resumeState.budget;
|
|
331
488
|
concurrency = resumeState.concurrency;
|
|
332
489
|
objective = resumeState.objective;
|
|
@@ -378,21 +535,19 @@ async function main() {
|
|
|
378
535
|
clearInterval(modelSpinner);
|
|
379
536
|
process.stdout.write(`\x1B[2K\r`);
|
|
380
537
|
}
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
}
|
|
389
|
-
usageCap = await select(`${chalk.cyan("⑤")} Usage cap:`, [
|
|
538
|
+
const plannerPick = await pickModel(`${chalk.cyan("④")} Planner model ${chalk.dim("(thinking, steering — use your strongest)")}:`, models);
|
|
539
|
+
plannerModel = plannerPick.model;
|
|
540
|
+
plannerProvider = plannerPick.provider;
|
|
541
|
+
const workerPick = await pickModel(`${chalk.cyan("⑤")} Executor model ${chalk.dim("(what runs the tasks — Qwen/OpenRouter/etc via Other…)")}:`, models);
|
|
542
|
+
workerModel = workerPick.model;
|
|
543
|
+
workerProvider = workerPick.provider;
|
|
544
|
+
usageCap = await select(`${chalk.cyan("⑥")} Usage cap:`, [
|
|
390
545
|
{ name: "Unlimited", value: undefined, hint: "full capacity, wait through rate limits" },
|
|
391
546
|
{ name: "90%", value: 0.9, hint: "leave 10% for other work" },
|
|
392
547
|
{ name: "75%", value: 0.75, hint: "conservative, plenty of headroom" },
|
|
393
548
|
{ name: "50%", value: 0.5, hint: "use half, keep the rest" },
|
|
394
549
|
]);
|
|
395
|
-
const extraChoice = await select(`${chalk.cyan("
|
|
550
|
+
const extraChoice = await select(`${chalk.cyan("⑦")} Allow extra usage ${chalk.dim("(billed separately)")}:`, [
|
|
396
551
|
{ name: "No", value: "no", hint: "stop when plan limits are reached" },
|
|
397
552
|
{ name: "Yes, with $ limit", value: "budget", hint: "set a spending cap" },
|
|
398
553
|
{ name: "Yes, unlimited", value: "unlimited", hint: "keep going no matter what" },
|
|
@@ -406,7 +561,7 @@ async function main() {
|
|
|
406
561
|
}
|
|
407
562
|
else if (extraChoice === "unlimited")
|
|
408
563
|
allowExtraUsage = true;
|
|
409
|
-
//
|
|
564
|
+
// ⑧ Permission mode (skip if --yolo or --perm set)
|
|
410
565
|
const cliYolo = argv.includes("--yolo");
|
|
411
566
|
if (cliFlags.perm) {
|
|
412
567
|
permissionMode = cliFlags.perm;
|
|
@@ -415,13 +570,13 @@ async function main() {
|
|
|
415
570
|
permissionMode = "bypassPermissions";
|
|
416
571
|
}
|
|
417
572
|
else {
|
|
418
|
-
permissionMode = await select(`${chalk.cyan("
|
|
573
|
+
permissionMode = await select(`${chalk.cyan("⑧")} Permissions:`, [
|
|
419
574
|
{ name: "Auto", value: "auto", hint: "accept low-risk, reject high-risk" },
|
|
420
575
|
{ name: "Bypass all", value: "bypassPermissions", hint: "agents can run anything (yolo)" },
|
|
421
576
|
{ name: "Prompt each", value: "default", hint: "ask for every dangerous op" },
|
|
422
577
|
]);
|
|
423
578
|
}
|
|
424
|
-
//
|
|
579
|
+
// ⑨ Worktrees + merge (skip if --yolo, --worktrees, --no-worktrees, or --merge set)
|
|
425
580
|
const gitRepo = isGitRepo(cwd);
|
|
426
581
|
if (cliYolo || argv.includes("--no-worktrees")) {
|
|
427
582
|
useWorktrees = false;
|
|
@@ -432,7 +587,7 @@ async function main() {
|
|
|
432
587
|
mergeStrategy = cliFlags.merge || "yolo";
|
|
433
588
|
}
|
|
434
589
|
else if (gitRepo) {
|
|
435
|
-
const wtChoice = await select(`${chalk.cyan("
|
|
590
|
+
const wtChoice = await select(`${chalk.cyan("⑨")} Git isolation:`, [
|
|
436
591
|
{ name: "Worktrees + yolo merge", value: "wt-yolo", hint: "isolate agents, merge into current branch" },
|
|
437
592
|
{ name: "Worktrees + new branch", value: "wt-branch", hint: "isolate agents, merge into a new branch" },
|
|
438
593
|
{ name: "No worktrees", value: "no-wt", hint: "all agents share the working directory" },
|
|
@@ -475,6 +630,14 @@ async function main() {
|
|
|
475
630
|
models = await fetchModels(5_000);
|
|
476
631
|
workerModel = cliFlags.model ?? fileCfg?.model ?? (models[0]?.value || "claude-sonnet-4-6");
|
|
477
632
|
plannerModel = models[0]?.value || workerModel;
|
|
633
|
+
// Auto-resolve a saved custom provider if --model matches its id or model id.
|
|
634
|
+
// Lets `claude-overnight --model=qwen3-coder-plus` route correctly without a separate flag.
|
|
635
|
+
const savedForCli = loadProviders();
|
|
636
|
+
const matched = savedForCli.find(p => p.id === workerModel || p.model === workerModel);
|
|
637
|
+
if (matched) {
|
|
638
|
+
workerProvider = matched;
|
|
639
|
+
workerModel = matched.model;
|
|
640
|
+
}
|
|
478
641
|
concurrency = cliFlags.concurrency ? parseInt(cliFlags.concurrency) : (fileCfg?.concurrency ?? 5);
|
|
479
642
|
budget = cliFlags.budget ? parseInt(cliFlags.budget) : undefined;
|
|
480
643
|
if (budget != null && (isNaN(budget) || budget < 1)) {
|
|
@@ -527,6 +690,29 @@ async function main() {
|
|
|
527
690
|
}
|
|
528
691
|
if (useWorktrees)
|
|
529
692
|
validateGitRepo(cwd);
|
|
693
|
+
// Custom-provider routing: build a model→env resolver so planner and worker
|
|
694
|
+
// queries hit the right endpoint without touching process.env globally.
|
|
695
|
+
const envForModel = buildEnvResolver({ plannerModel, plannerProvider, workerModel, workerProvider });
|
|
696
|
+
setPlannerEnvResolver(envForModel);
|
|
697
|
+
// Fail fast if a custom provider is misconfigured — one bad key would
|
|
698
|
+
// otherwise surface as N agent failures scattered across the run.
|
|
699
|
+
if (plannerProvider || workerProvider) {
|
|
700
|
+
const pending = [];
|
|
701
|
+
if (plannerProvider)
|
|
702
|
+
pending.push(["planner", plannerProvider]);
|
|
703
|
+
if (workerProvider && workerProvider.id !== plannerProvider?.id)
|
|
704
|
+
pending.push(["executor", workerProvider]);
|
|
705
|
+
for (const [role, p] of pending) {
|
|
706
|
+
process.stdout.write(` ${chalk.dim(`◆ Pinging ${role} (${p.displayName})...`)}`);
|
|
707
|
+
const r = await preflightProvider(p, cwd);
|
|
708
|
+
if (!r.ok) {
|
|
709
|
+
process.stdout.write(`\x1B[2K\r ${chalk.red(`✗ ${role} preflight failed:`)} ${chalk.dim(r.error)}\n`);
|
|
710
|
+
console.error(chalk.red(`\n Fix the provider at ~/.claude/claude-overnight/providers.json and retry.\n`));
|
|
711
|
+
process.exit(1);
|
|
712
|
+
}
|
|
713
|
+
process.stdout.write(`\x1B[2K\r ${chalk.green(`✓ ${role} ready`)} ${chalk.dim(`· ${p.displayName} · ${p.model}`)}\n`);
|
|
714
|
+
}
|
|
715
|
+
}
|
|
530
716
|
if (nonInteractive) {
|
|
531
717
|
const capStr = usageCap != null ? ` cap=${Math.round(usageCap * 100)}%` : "";
|
|
532
718
|
const extraStr = allowExtraUsage ? (extraUsageBudget ? ` extra=$${extraUsageBudget}` : " extra=∞") : " extra=off";
|
|
@@ -553,7 +739,9 @@ async function main() {
|
|
|
553
739
|
saveRunState(runDir, {
|
|
554
740
|
id: runDir.split(/[/\\]/).pop() ?? "",
|
|
555
741
|
objective, budget: budget ?? 10, remaining: budget ?? 10,
|
|
556
|
-
workerModel, plannerModel,
|
|
742
|
+
workerModel, plannerModel,
|
|
743
|
+
workerProviderId: workerProvider?.id, plannerProviderId: plannerProvider?.id,
|
|
744
|
+
concurrency, permissionMode,
|
|
557
745
|
usageCap, allowExtraUsage, extraUsageBudget,
|
|
558
746
|
flex, useWorktrees, mergeStrategy,
|
|
559
747
|
waveNum: 0, currentTasks: [],
|
|
@@ -612,9 +800,10 @@ async function main() {
|
|
|
612
800
|
process.stdout.write("\x1B[?25l");
|
|
613
801
|
try {
|
|
614
802
|
let answer = "";
|
|
803
|
+
const plannerEnv = envForModel(plannerModel);
|
|
615
804
|
for await (const msg of query({
|
|
616
805
|
prompt: `You're planning work for: "${objective}"\n\nThemes identified:\n${themes.map((t, i) => `${i + 1}. ${t}`).join("\n")}\n\nUser question: ${question}`,
|
|
617
|
-
options: { cwd, model: plannerModel, permissionMode, persistSession: false },
|
|
806
|
+
options: { cwd, model: plannerModel, permissionMode, persistSession: false, ...(plannerEnv && { env: plannerEnv }) },
|
|
618
807
|
})) {
|
|
619
808
|
if (msg.type === "result" && msg.subtype === "success")
|
|
620
809
|
answer = msg.result || "";
|
|
@@ -654,6 +843,7 @@ async function main() {
|
|
|
654
843
|
const thinkingSwarm = new Swarm({
|
|
655
844
|
tasks: thinkingTasks, concurrency, cwd, model: plannerModel, permissionMode,
|
|
656
845
|
useWorktrees: false, mergeStrategy: "yolo", agentTimeoutMs, usageCap, allowExtraUsage, extraUsageBudget,
|
|
846
|
+
envForModel,
|
|
657
847
|
});
|
|
658
848
|
const thinkRunInfo = { accIn: 0, accOut: 0, accCost: 0, accCompleted: 0, accFailed: 0, sessionsBudget: budget ?? 10, waveNum: -1, remaining: budget ?? 10, model: plannerModel, startedAt: Date.now() };
|
|
659
849
|
const thinkDisplay = new RunDisplay(thinkRunInfo, { remaining: 0, usageCap, concurrency, paused: false, dirty: false });
|
|
@@ -680,7 +870,9 @@ async function main() {
|
|
|
680
870
|
saveRunState(runDir, {
|
|
681
871
|
id: runDir.split(/[/\\]/).pop() ?? "",
|
|
682
872
|
objective: objective, budget: budget ?? 10, remaining: (budget ?? 10) - thinkingUsed,
|
|
683
|
-
workerModel, plannerModel,
|
|
873
|
+
workerModel, plannerModel,
|
|
874
|
+
workerProviderId: workerProvider?.id, plannerProviderId: plannerProvider?.id,
|
|
875
|
+
concurrency, permissionMode,
|
|
684
876
|
usageCap, allowExtraUsage, extraUsageBudget,
|
|
685
877
|
flex, useWorktrees, mergeStrategy,
|
|
686
878
|
waveNum: 0, currentTasks: [],
|
|
@@ -755,9 +947,10 @@ async function main() {
|
|
|
755
947
|
process.stdout.write("\x1B[?25l");
|
|
756
948
|
try {
|
|
757
949
|
let answer = "";
|
|
950
|
+
const plannerEnv = envForModel(plannerModel);
|
|
758
951
|
for await (const msg of query({
|
|
759
952
|
prompt: `You planned these tasks for the objective "${objective}":\n${tasks.map((t, i) => `${i + 1}. ${t.prompt}`).join("\n")}\n\nUser question: ${question}`,
|
|
760
|
-
options: { cwd, model: plannerModel, permissionMode, persistSession: false },
|
|
953
|
+
options: { cwd, model: plannerModel, permissionMode, persistSession: false, ...(plannerEnv && { env: plannerEnv }) },
|
|
761
954
|
})) {
|
|
762
955
|
if (msg.type === "result" && msg.subtype === "success")
|
|
763
956
|
answer = msg.result || "";
|
|
@@ -798,7 +991,8 @@ async function main() {
|
|
|
798
991
|
}
|
|
799
992
|
// ── Execute ──
|
|
800
993
|
await executeRun({
|
|
801
|
-
tasks, objective, budget: budget ?? tasks.length, workerModel, plannerModel,
|
|
994
|
+
tasks, objective, budget: budget ?? tasks.length, workerModel, plannerModel,
|
|
995
|
+
workerProvider, plannerProvider, concurrency,
|
|
802
996
|
permissionMode, useWorktrees, mergeStrategy, usageCap, allowExtraUsage, extraUsageBudget,
|
|
803
997
|
flex, agentTimeoutMs, cwd, allowedTools, runDir, previousKnowledge,
|
|
804
998
|
resuming, resumeState: resumeState ?? undefined,
|
package/dist/planner-query.d.ts
CHANGED
|
@@ -24,6 +24,7 @@ export interface PlannerOpts {
|
|
|
24
24
|
schema: Record<string, unknown>;
|
|
25
25
|
};
|
|
26
26
|
}
|
|
27
|
+
export declare function setPlannerEnvResolver(fn: ((model?: string) => Record<string, string> | undefined) | undefined): void;
|
|
27
28
|
export type ModelTier = "opus" | "sonnet" | "haiku" | "unknown";
|
|
28
29
|
export declare function detectModelTier(model: string): ModelTier;
|
|
29
30
|
export declare function modelCapabilityBlock(model: string): string;
|
package/dist/planner-query.js
CHANGED
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
2
2
|
import { readFileSync } from "fs";
|
|
3
3
|
import { NudgeError } from "./types.js";
|
|
4
|
+
// ── Shared env resolver (set once at run start, used by every planner query) ──
|
|
5
|
+
//
|
|
6
|
+
// Swarm and planner calls share a model→env map so a custom provider configured
|
|
7
|
+
// as planner or worker routes its traffic without threading extra params
|
|
8
|
+
// through every planner.ts / steering.ts function.
|
|
9
|
+
let _envResolver;
|
|
10
|
+
export function setPlannerEnvResolver(fn) {
|
|
11
|
+
_envResolver = fn;
|
|
12
|
+
}
|
|
4
13
|
export function detectModelTier(model) {
|
|
5
14
|
const m = model.toLowerCase();
|
|
6
15
|
if (m === "default" || m.includes("opus"))
|
|
@@ -77,6 +86,7 @@ async function runPlannerQueryOnce(prompt, opts, onLog) {
|
|
|
77
86
|
let structuredOutput;
|
|
78
87
|
const startedAt = Date.now();
|
|
79
88
|
const isResume = !!opts.resumeSessionId;
|
|
89
|
+
const envOverride = _envResolver?.(opts.model);
|
|
80
90
|
const pq = query({
|
|
81
91
|
prompt,
|
|
82
92
|
options: {
|
|
@@ -90,6 +100,7 @@ async function runPlannerQueryOnce(prompt, opts, onLog) {
|
|
|
90
100
|
includePartialMessages: true,
|
|
91
101
|
...(isResume && { resume: opts.resumeSessionId }),
|
|
92
102
|
...(opts.outputFormat && { outputFormat: opts.outputFormat }),
|
|
103
|
+
...(envOverride && { env: envOverride }),
|
|
93
104
|
},
|
|
94
105
|
});
|
|
95
106
|
let lastLogText = "";
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { ModelInfo } from "@anthropic-ai/claude-agent-sdk";
|
|
2
|
+
/**
|
|
3
|
+
* A non-Anthropic model provider reachable via an Anthropic-compatible endpoint
|
|
4
|
+
* (e.g. DashScope for Qwen, OpenRouter, a local proxy). Stored user-level so a
|
|
5
|
+
* key configured once works across every repo.
|
|
6
|
+
*/
|
|
7
|
+
export interface ProviderConfig {
|
|
8
|
+
id: string;
|
|
9
|
+
displayName: string;
|
|
10
|
+
baseURL: string;
|
|
11
|
+
model: string;
|
|
12
|
+
/** Env var name holding the key — preferred over inline `key` (nothing on disk). */
|
|
13
|
+
keyEnv?: string;
|
|
14
|
+
/** Inline API key. Stored plaintext in providers.json (mode 0600). */
|
|
15
|
+
key?: string;
|
|
16
|
+
}
|
|
17
|
+
export declare function getStorePath(): string;
|
|
18
|
+
export declare function loadProviders(): ProviderConfig[];
|
|
19
|
+
export declare function saveProvider(p: ProviderConfig): void;
|
|
20
|
+
export declare function deleteProvider(id: string): void;
|
|
21
|
+
export declare function resolveKey(p: ProviderConfig): string | null;
|
|
22
|
+
/**
|
|
23
|
+
* Build the env overrides for a custom provider. Returns a full merged env
|
|
24
|
+
* (including current process.env) because the SDK replaces, not merges, when
|
|
25
|
+
* you pass `options.env`.
|
|
26
|
+
*/
|
|
27
|
+
export declare function envFor(p: ProviderConfig): Record<string, string>;
|
|
28
|
+
export interface ModelPick {
|
|
29
|
+
model: string;
|
|
30
|
+
providerId?: string;
|
|
31
|
+
provider?: ProviderConfig;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Show a unified picker: Anthropic models (from SDK), saved custom providers,
|
|
35
|
+
* and an "Other…" entry that walks the user through adding a new provider.
|
|
36
|
+
* Returns the selected model string and, if it's a custom provider, the id.
|
|
37
|
+
*/
|
|
38
|
+
export declare function pickModel(label: string, anthropicModels: ModelInfo[], currentModelId?: string): Promise<ModelPick>;
|
|
39
|
+
/**
|
|
40
|
+
* Cheap auth check: spawn a 1-turn query against the provider and fail fast
|
|
41
|
+
* if the key is wrong or the endpoint is unreachable. Timeout is aggressive
|
|
42
|
+
* so misconfig doesn't delay the main run.
|
|
43
|
+
*/
|
|
44
|
+
export declare function preflightProvider(p: ProviderConfig, cwd: string, timeoutMs?: number): Promise<{
|
|
45
|
+
ok: true;
|
|
46
|
+
} | {
|
|
47
|
+
ok: false;
|
|
48
|
+
error: string;
|
|
49
|
+
}>;
|
|
50
|
+
export type EnvResolver = (model?: string) => Record<string, string> | undefined;
|
|
51
|
+
/**
|
|
52
|
+
* Build a single resolver that swarm.ts and planner-query.ts share. Maps a
|
|
53
|
+
* model string to the env overrides that should be passed to `query()`.
|
|
54
|
+
* Returns undefined for Anthropic-native models (let the SDK use process.env).
|
|
55
|
+
*/
|
|
56
|
+
export declare function buildEnvResolver(opts: {
|
|
57
|
+
plannerModel: string;
|
|
58
|
+
plannerProvider?: ProviderConfig;
|
|
59
|
+
workerModel: string;
|
|
60
|
+
workerProvider?: ProviderConfig;
|
|
61
|
+
}): EnvResolver;
|