claude-overnight 1.14.0 → 1.16.1
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 +96 -30
- 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 +14 -4
- package/dist/swarm.d.ts +11 -0
- package/dist/swarm.js +68 -7
- package/dist/types.d.ts +5 -0
- package/dist/types.js +4 -0
- package/dist/ui.js +4 -0
- 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";
|
|
@@ -60,6 +61,8 @@ async function promptResumeOverrides(state, cliFlags, argv, noTTY, runDir) {
|
|
|
60
61
|
catch { }
|
|
61
62
|
return;
|
|
62
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(() => []);
|
|
63
66
|
// ── Interactive review ──
|
|
64
67
|
const fmtSummary = () => {
|
|
65
68
|
const tier = detectModelTier(state.workerModel);
|
|
@@ -88,16 +91,21 @@ async function promptResumeOverrides(state, cliFlags, argv, noTTY, runDir) {
|
|
|
88
91
|
if (action === "r")
|
|
89
92
|
return;
|
|
90
93
|
// ── Edit walk ──
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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;
|
|
95
101
|
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
state.workerModel = ans.trim();
|
|
102
|
+
finally {
|
|
103
|
+
clearInterval(modelSpinner);
|
|
104
|
+
process.stdout.write(`\x1B[2K\r`);
|
|
100
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;
|
|
101
109
|
const remAns = await ask(`\n ${chalk.cyan("②")} Remaining sessions ${chalk.dim(`[${state.remaining}]:`)} `);
|
|
102
110
|
const parsedRem = parseInt(remAns);
|
|
103
111
|
if (!isNaN(parsedRem) && parsedRem > 0) {
|
|
@@ -172,7 +180,7 @@ async function main() {
|
|
|
172
180
|
--dry-run Show planned tasks without running them
|
|
173
181
|
--budget=N Target number of agent runs ${chalk.dim("(default: 10)")}
|
|
174
182
|
--concurrency=N Max parallel agents ${chalk.dim("(default: 5)")}
|
|
175
|
-
--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.)")}
|
|
176
184
|
--usage-cap=N Stop at N% utilization ${chalk.dim("(e.g. 90 to save 10% for other work)")}
|
|
177
185
|
--allow-extra-usage Allow extra/overage usage ${chalk.dim("(default: stop when plan limits hit)")}
|
|
178
186
|
--extra-usage-budget=N Max $ for extra usage ${chalk.dim("(implies --allow-extra-usage)")}
|
|
@@ -235,7 +243,9 @@ async function main() {
|
|
|
235
243
|
}
|
|
236
244
|
}
|
|
237
245
|
// ── Mode detection ──
|
|
238
|
-
|
|
246
|
+
// Stop the bin.ts startup splash (if any) before printing our header.
|
|
247
|
+
globalThis.__coStopSplash?.();
|
|
248
|
+
console.log(` ${chalk.bold("🌙 claude-overnight")}`);
|
|
239
249
|
console.log(chalk.dim(` ${"─".repeat(36)}`));
|
|
240
250
|
const noTTY = !process.stdin.isTTY;
|
|
241
251
|
const nonInteractive = noTTY || fileCfg !== undefined || tasks.length > 0;
|
|
@@ -443,6 +453,8 @@ async function main() {
|
|
|
443
453
|
// ── Config resolution ──
|
|
444
454
|
let workerModel;
|
|
445
455
|
let plannerModel;
|
|
456
|
+
let workerProvider;
|
|
457
|
+
let plannerProvider;
|
|
446
458
|
let budget;
|
|
447
459
|
let concurrency;
|
|
448
460
|
let objective = fileCfg?.objective;
|
|
@@ -455,6 +467,23 @@ async function main() {
|
|
|
455
467
|
if (resuming) {
|
|
456
468
|
workerModel = resumeState.workerModel;
|
|
457
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
|
+
}
|
|
458
487
|
budget = resumeState.budget;
|
|
459
488
|
concurrency = resumeState.concurrency;
|
|
460
489
|
objective = resumeState.objective;
|
|
@@ -506,21 +535,19 @@ async function main() {
|
|
|
506
535
|
clearInterval(modelSpinner);
|
|
507
536
|
process.stdout.write(`\x1B[2K\r`);
|
|
508
537
|
}
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
}
|
|
517
|
-
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:`, [
|
|
518
545
|
{ name: "Unlimited", value: undefined, hint: "full capacity, wait through rate limits" },
|
|
519
546
|
{ name: "90%", value: 0.9, hint: "leave 10% for other work" },
|
|
520
547
|
{ name: "75%", value: 0.75, hint: "conservative, plenty of headroom" },
|
|
521
548
|
{ name: "50%", value: 0.5, hint: "use half, keep the rest" },
|
|
522
549
|
]);
|
|
523
|
-
const extraChoice = await select(`${chalk.cyan("
|
|
550
|
+
const extraChoice = await select(`${chalk.cyan("⑦")} Allow extra usage ${chalk.dim("(billed separately)")}:`, [
|
|
524
551
|
{ name: "No", value: "no", hint: "stop when plan limits are reached" },
|
|
525
552
|
{ name: "Yes, with $ limit", value: "budget", hint: "set a spending cap" },
|
|
526
553
|
{ name: "Yes, unlimited", value: "unlimited", hint: "keep going no matter what" },
|
|
@@ -534,7 +561,7 @@ async function main() {
|
|
|
534
561
|
}
|
|
535
562
|
else if (extraChoice === "unlimited")
|
|
536
563
|
allowExtraUsage = true;
|
|
537
|
-
//
|
|
564
|
+
// ⑧ Permission mode (skip if --yolo or --perm set)
|
|
538
565
|
const cliYolo = argv.includes("--yolo");
|
|
539
566
|
if (cliFlags.perm) {
|
|
540
567
|
permissionMode = cliFlags.perm;
|
|
@@ -543,13 +570,13 @@ async function main() {
|
|
|
543
570
|
permissionMode = "bypassPermissions";
|
|
544
571
|
}
|
|
545
572
|
else {
|
|
546
|
-
permissionMode = await select(`${chalk.cyan("
|
|
573
|
+
permissionMode = await select(`${chalk.cyan("⑧")} Permissions:`, [
|
|
547
574
|
{ name: "Auto", value: "auto", hint: "accept low-risk, reject high-risk" },
|
|
548
575
|
{ name: "Bypass all", value: "bypassPermissions", hint: "agents can run anything (yolo)" },
|
|
549
576
|
{ name: "Prompt each", value: "default", hint: "ask for every dangerous op" },
|
|
550
577
|
]);
|
|
551
578
|
}
|
|
552
|
-
//
|
|
579
|
+
// ⑨ Worktrees + merge (skip if --yolo, --worktrees, --no-worktrees, or --merge set)
|
|
553
580
|
const gitRepo = isGitRepo(cwd);
|
|
554
581
|
if (cliYolo || argv.includes("--no-worktrees")) {
|
|
555
582
|
useWorktrees = false;
|
|
@@ -560,7 +587,7 @@ async function main() {
|
|
|
560
587
|
mergeStrategy = cliFlags.merge || "yolo";
|
|
561
588
|
}
|
|
562
589
|
else if (gitRepo) {
|
|
563
|
-
const wtChoice = await select(`${chalk.cyan("
|
|
590
|
+
const wtChoice = await select(`${chalk.cyan("⑨")} Git isolation:`, [
|
|
564
591
|
{ name: "Worktrees + yolo merge", value: "wt-yolo", hint: "isolate agents, merge into current branch" },
|
|
565
592
|
{ name: "Worktrees + new branch", value: "wt-branch", hint: "isolate agents, merge into a new branch" },
|
|
566
593
|
{ name: "No worktrees", value: "no-wt", hint: "all agents share the working directory" },
|
|
@@ -603,6 +630,14 @@ async function main() {
|
|
|
603
630
|
models = await fetchModels(5_000);
|
|
604
631
|
workerModel = cliFlags.model ?? fileCfg?.model ?? (models[0]?.value || "claude-sonnet-4-6");
|
|
605
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
|
+
}
|
|
606
641
|
concurrency = cliFlags.concurrency ? parseInt(cliFlags.concurrency) : (fileCfg?.concurrency ?? 5);
|
|
607
642
|
budget = cliFlags.budget ? parseInt(cliFlags.budget) : undefined;
|
|
608
643
|
if (budget != null && (isNaN(budget) || budget < 1)) {
|
|
@@ -655,6 +690,29 @@ async function main() {
|
|
|
655
690
|
}
|
|
656
691
|
if (useWorktrees)
|
|
657
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
|
+
}
|
|
658
716
|
if (nonInteractive) {
|
|
659
717
|
const capStr = usageCap != null ? ` cap=${Math.round(usageCap * 100)}%` : "";
|
|
660
718
|
const extraStr = allowExtraUsage ? (extraUsageBudget ? ` extra=$${extraUsageBudget}` : " extra=∞") : " extra=off";
|
|
@@ -681,7 +739,9 @@ async function main() {
|
|
|
681
739
|
saveRunState(runDir, {
|
|
682
740
|
id: runDir.split(/[/\\]/).pop() ?? "",
|
|
683
741
|
objective, budget: budget ?? 10, remaining: budget ?? 10,
|
|
684
|
-
workerModel, plannerModel,
|
|
742
|
+
workerModel, plannerModel,
|
|
743
|
+
workerProviderId: workerProvider?.id, plannerProviderId: plannerProvider?.id,
|
|
744
|
+
concurrency, permissionMode,
|
|
685
745
|
usageCap, allowExtraUsage, extraUsageBudget,
|
|
686
746
|
flex, useWorktrees, mergeStrategy,
|
|
687
747
|
waveNum: 0, currentTasks: [],
|
|
@@ -740,9 +800,10 @@ async function main() {
|
|
|
740
800
|
process.stdout.write("\x1B[?25l");
|
|
741
801
|
try {
|
|
742
802
|
let answer = "";
|
|
803
|
+
const plannerEnv = envForModel(plannerModel);
|
|
743
804
|
for await (const msg of query({
|
|
744
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}`,
|
|
745
|
-
options: { cwd, model: plannerModel, permissionMode, persistSession: false },
|
|
806
|
+
options: { cwd, model: plannerModel, permissionMode, persistSession: false, ...(plannerEnv && { env: plannerEnv }) },
|
|
746
807
|
})) {
|
|
747
808
|
if (msg.type === "result" && msg.subtype === "success")
|
|
748
809
|
answer = msg.result || "";
|
|
@@ -782,6 +843,7 @@ async function main() {
|
|
|
782
843
|
const thinkingSwarm = new Swarm({
|
|
783
844
|
tasks: thinkingTasks, concurrency, cwd, model: plannerModel, permissionMode,
|
|
784
845
|
useWorktrees: false, mergeStrategy: "yolo", agentTimeoutMs, usageCap, allowExtraUsage, extraUsageBudget,
|
|
846
|
+
envForModel,
|
|
785
847
|
});
|
|
786
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() };
|
|
787
849
|
const thinkDisplay = new RunDisplay(thinkRunInfo, { remaining: 0, usageCap, concurrency, paused: false, dirty: false });
|
|
@@ -808,7 +870,9 @@ async function main() {
|
|
|
808
870
|
saveRunState(runDir, {
|
|
809
871
|
id: runDir.split(/[/\\]/).pop() ?? "",
|
|
810
872
|
objective: objective, budget: budget ?? 10, remaining: (budget ?? 10) - thinkingUsed,
|
|
811
|
-
workerModel, plannerModel,
|
|
873
|
+
workerModel, plannerModel,
|
|
874
|
+
workerProviderId: workerProvider?.id, plannerProviderId: plannerProvider?.id,
|
|
875
|
+
concurrency, permissionMode,
|
|
812
876
|
usageCap, allowExtraUsage, extraUsageBudget,
|
|
813
877
|
flex, useWorktrees, mergeStrategy,
|
|
814
878
|
waveNum: 0, currentTasks: [],
|
|
@@ -883,9 +947,10 @@ async function main() {
|
|
|
883
947
|
process.stdout.write("\x1B[?25l");
|
|
884
948
|
try {
|
|
885
949
|
let answer = "";
|
|
950
|
+
const plannerEnv = envForModel(plannerModel);
|
|
886
951
|
for await (const msg of query({
|
|
887
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}`,
|
|
888
|
-
options: { cwd, model: plannerModel, permissionMode, persistSession: false },
|
|
953
|
+
options: { cwd, model: plannerModel, permissionMode, persistSession: false, ...(plannerEnv && { env: plannerEnv }) },
|
|
889
954
|
})) {
|
|
890
955
|
if (msg.type === "result" && msg.subtype === "success")
|
|
891
956
|
answer = msg.result || "";
|
|
@@ -926,7 +991,8 @@ async function main() {
|
|
|
926
991
|
}
|
|
927
992
|
// ── Execute ──
|
|
928
993
|
await executeRun({
|
|
929
|
-
tasks, objective, budget: budget ?? tasks.length, workerModel, plannerModel,
|
|
994
|
+
tasks, objective, budget: budget ?? tasks.length, workerModel, plannerModel,
|
|
995
|
+
workerProvider, plannerProvider, concurrency,
|
|
930
996
|
permissionMode, useWorktrees, mergeStrategy, usageCap, allowExtraUsage, extraUsageBudget,
|
|
931
997
|
flex, agentTimeoutMs, cwd, allowedTools, runDir, previousKnowledge,
|
|
932
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;
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync, chmodSync } from "fs";
|
|
2
|
+
import { homedir } from "os";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
import chalk from "chalk";
|
|
5
|
+
import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
6
|
+
import { ask, select } from "./cli.js";
|
|
7
|
+
// ── Store ──
|
|
8
|
+
const STORE_PATH = join(homedir(), ".claude", "claude-overnight", "providers.json");
|
|
9
|
+
export function getStorePath() { return STORE_PATH; }
|
|
10
|
+
export function loadProviders() {
|
|
11
|
+
try {
|
|
12
|
+
const raw = readFileSync(STORE_PATH, "utf-8");
|
|
13
|
+
const parsed = JSON.parse(raw);
|
|
14
|
+
if (Array.isArray(parsed?.providers))
|
|
15
|
+
return parsed.providers.filter(isValidProvider);
|
|
16
|
+
}
|
|
17
|
+
catch { }
|
|
18
|
+
return [];
|
|
19
|
+
}
|
|
20
|
+
export function saveProvider(p) {
|
|
21
|
+
const all = loadProviders().filter(x => x.id !== p.id);
|
|
22
|
+
all.push(p);
|
|
23
|
+
mkdirSync(join(homedir(), ".claude", "claude-overnight"), { recursive: true });
|
|
24
|
+
writeFileSync(STORE_PATH, JSON.stringify({ providers: all }, null, 2), "utf-8");
|
|
25
|
+
try {
|
|
26
|
+
chmodSync(STORE_PATH, 0o600);
|
|
27
|
+
}
|
|
28
|
+
catch { }
|
|
29
|
+
}
|
|
30
|
+
export function deleteProvider(id) {
|
|
31
|
+
const all = loadProviders().filter(x => x.id !== id);
|
|
32
|
+
if (!existsSync(STORE_PATH))
|
|
33
|
+
return;
|
|
34
|
+
writeFileSync(STORE_PATH, JSON.stringify({ providers: all }, null, 2), "utf-8");
|
|
35
|
+
try {
|
|
36
|
+
chmodSync(STORE_PATH, 0o600);
|
|
37
|
+
}
|
|
38
|
+
catch { }
|
|
39
|
+
}
|
|
40
|
+
function isValidProvider(p) {
|
|
41
|
+
return p && typeof p.id === "string" && typeof p.baseURL === "string"
|
|
42
|
+
&& typeof p.model === "string" && typeof p.displayName === "string";
|
|
43
|
+
}
|
|
44
|
+
// ── Key resolution ──
|
|
45
|
+
export function resolveKey(p) {
|
|
46
|
+
if (p.keyEnv) {
|
|
47
|
+
const v = process.env[p.keyEnv];
|
|
48
|
+
return v && v.trim() ? v : null;
|
|
49
|
+
}
|
|
50
|
+
return p.key && p.key.trim() ? p.key : null;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Build the env overrides for a custom provider. Returns a full merged env
|
|
54
|
+
* (including current process.env) because the SDK replaces, not merges, when
|
|
55
|
+
* you pass `options.env`.
|
|
56
|
+
*/
|
|
57
|
+
export function envFor(p) {
|
|
58
|
+
const key = resolveKey(p);
|
|
59
|
+
if (!key)
|
|
60
|
+
throw new Error(`Provider "${p.id}" has no API key (${p.keyEnv ? `env ${p.keyEnv} is empty` : "inline key missing"})`);
|
|
61
|
+
const base = {};
|
|
62
|
+
for (const [k, v] of Object.entries(process.env))
|
|
63
|
+
if (v !== undefined)
|
|
64
|
+
base[k] = v;
|
|
65
|
+
base.ANTHROPIC_BASE_URL = p.baseURL;
|
|
66
|
+
base.ANTHROPIC_AUTH_TOKEN = key;
|
|
67
|
+
delete base.ANTHROPIC_API_KEY;
|
|
68
|
+
return base;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Show a unified picker: Anthropic models (from SDK), saved custom providers,
|
|
72
|
+
* and an "Other…" entry that walks the user through adding a new provider.
|
|
73
|
+
* Returns the selected model string and, if it's a custom provider, the id.
|
|
74
|
+
*/
|
|
75
|
+
export async function pickModel(label, anthropicModels, currentModelId) {
|
|
76
|
+
for (;;) {
|
|
77
|
+
const saved = loadProviders();
|
|
78
|
+
const items = [];
|
|
79
|
+
for (const m of anthropicModels) {
|
|
80
|
+
items.push({ name: m.displayName, value: { kind: "anthropic", model: m }, hint: m.description });
|
|
81
|
+
}
|
|
82
|
+
// Network-failed fallback: ensure the picker always has at least one Anthropic
|
|
83
|
+
// entry so the user isn't trapped if they cancel the Other… form.
|
|
84
|
+
if (anthropicModels.length === 0) {
|
|
85
|
+
items.push({
|
|
86
|
+
name: "claude-sonnet-4-6",
|
|
87
|
+
value: { kind: "anthropic", model: { value: "claude-sonnet-4-6", displayName: "claude-sonnet-4-6", description: "default (model list unavailable)" } },
|
|
88
|
+
hint: "default — Anthropic model list unavailable",
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
for (const p of saved) {
|
|
92
|
+
const keySrc = p.keyEnv ? `env ${p.keyEnv}` : "stored key";
|
|
93
|
+
items.push({ name: `${p.displayName}`, value: { kind: "provider", provider: p }, hint: `${p.model} · ${keySrc}` });
|
|
94
|
+
}
|
|
95
|
+
items.push({ name: chalk.cyan("Other…"), value: { kind: "other" }, hint: "custom OpenAI/Anthropic-compatible endpoint" });
|
|
96
|
+
let defaultIdx = 0;
|
|
97
|
+
if (currentModelId) {
|
|
98
|
+
const i = items.findIndex(it => {
|
|
99
|
+
if (it.value.kind === "anthropic")
|
|
100
|
+
return it.value.model.value === currentModelId;
|
|
101
|
+
if (it.value.kind === "provider")
|
|
102
|
+
return it.value.provider.id === currentModelId || it.value.provider.model === currentModelId;
|
|
103
|
+
return false;
|
|
104
|
+
});
|
|
105
|
+
if (i >= 0)
|
|
106
|
+
defaultIdx = i;
|
|
107
|
+
}
|
|
108
|
+
const picked = await select(label, items, defaultIdx);
|
|
109
|
+
if (picked.kind === "anthropic")
|
|
110
|
+
return { model: picked.model.value };
|
|
111
|
+
if (picked.kind === "provider") {
|
|
112
|
+
return { model: picked.provider.model, providerId: picked.provider.id, provider: picked.provider };
|
|
113
|
+
}
|
|
114
|
+
const added = await promptNewProvider();
|
|
115
|
+
if (added) {
|
|
116
|
+
saveProvider(added);
|
|
117
|
+
return { model: added.model, providerId: added.id, provider: added };
|
|
118
|
+
}
|
|
119
|
+
// user cancelled "Other…" — loop back to picker
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
async function promptNewProvider() {
|
|
123
|
+
console.log(chalk.dim("\n Add a custom provider (Anthropic-compatible endpoint)"));
|
|
124
|
+
console.log(chalk.dim(" Leave blank to cancel.\n"));
|
|
125
|
+
const displayName = await ask(` ${chalk.cyan("Name")} ${chalk.dim("(e.g. 'Qwen Coder'):")} `);
|
|
126
|
+
if (!displayName)
|
|
127
|
+
return null;
|
|
128
|
+
const id = slugify(displayName);
|
|
129
|
+
const baseURLRaw = await ask(`\n ${chalk.cyan("Base URL")} ${chalk.dim("(e.g. https://dashscope-intl.aliyuncs.com/api/v2/apps/claude-code-proxy):")} `);
|
|
130
|
+
if (!baseURLRaw)
|
|
131
|
+
return null;
|
|
132
|
+
const baseURL = normalizeBaseURL(baseURLRaw);
|
|
133
|
+
const model = await ask(`\n ${chalk.cyan("Model id")} ${chalk.dim("(e.g. qwen3-coder-plus):")} `);
|
|
134
|
+
if (!model)
|
|
135
|
+
return null;
|
|
136
|
+
const keyMode = await select(` ${chalk.cyan("API key source")}:`, [
|
|
137
|
+
{ name: "Paste key now", value: "inline", hint: "stored plaintext in ~/.claude/claude-overnight/providers.json (0600)" },
|
|
138
|
+
{ name: "Read from env var", value: "env", hint: "nothing written to disk" },
|
|
139
|
+
]);
|
|
140
|
+
if (keyMode === "env") {
|
|
141
|
+
const envName = await ask(`\n ${chalk.cyan("Env var name")} ${chalk.dim(`(e.g. CO_KEY_${id.toUpperCase()}):`)} `);
|
|
142
|
+
if (!envName)
|
|
143
|
+
return null;
|
|
144
|
+
if (!process.env[envName]) {
|
|
145
|
+
console.log(chalk.yellow(`\n ⚠ ${envName} is not set in the current shell — you'll need to export it before running.`));
|
|
146
|
+
}
|
|
147
|
+
return { id, displayName, baseURL, model, keyEnv: envName };
|
|
148
|
+
}
|
|
149
|
+
const key = await ask(`\n ${chalk.cyan("API key")}: `);
|
|
150
|
+
if (!key)
|
|
151
|
+
return null;
|
|
152
|
+
return { id, displayName, baseURL, model, key };
|
|
153
|
+
}
|
|
154
|
+
function slugify(s) {
|
|
155
|
+
return s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 32) || "provider";
|
|
156
|
+
}
|
|
157
|
+
/** Strip trailing slashes and common endpoint suffixes users paste by mistake. */
|
|
158
|
+
function normalizeBaseURL(raw) {
|
|
159
|
+
let url = raw.trim().replace(/\/+$/, "");
|
|
160
|
+
url = url.replace(/\/v1\/messages$/i, "").replace(/\/messages$/i, "");
|
|
161
|
+
return url;
|
|
162
|
+
}
|
|
163
|
+
// ── Pre-flight validation ──
|
|
164
|
+
/**
|
|
165
|
+
* Cheap auth check: spawn a 1-turn query against the provider and fail fast
|
|
166
|
+
* if the key is wrong or the endpoint is unreachable. Timeout is aggressive
|
|
167
|
+
* so misconfig doesn't delay the main run.
|
|
168
|
+
*/
|
|
169
|
+
export async function preflightProvider(p, cwd, timeoutMs = 20_000) {
|
|
170
|
+
let env;
|
|
171
|
+
try {
|
|
172
|
+
env = envFor(p);
|
|
173
|
+
}
|
|
174
|
+
catch (err) {
|
|
175
|
+
return { ok: false, error: err.message };
|
|
176
|
+
}
|
|
177
|
+
let pq;
|
|
178
|
+
try {
|
|
179
|
+
pq = query({
|
|
180
|
+
prompt: "Reply with exactly the word ok and nothing else.",
|
|
181
|
+
options: {
|
|
182
|
+
cwd,
|
|
183
|
+
model: p.model,
|
|
184
|
+
env,
|
|
185
|
+
permissionMode: "bypassPermissions",
|
|
186
|
+
allowDangerouslySkipPermissions: true,
|
|
187
|
+
maxTurns: 1,
|
|
188
|
+
persistSession: false,
|
|
189
|
+
},
|
|
190
|
+
});
|
|
191
|
+
const stream = pq;
|
|
192
|
+
const consume = (async () => {
|
|
193
|
+
for await (const msg of stream) {
|
|
194
|
+
if (msg.type === "result") {
|
|
195
|
+
if (msg.subtype !== "success") {
|
|
196
|
+
return { ok: false, error: String(msg.result || msg.subtype || "unknown error").slice(0, 200) };
|
|
197
|
+
}
|
|
198
|
+
return { ok: true };
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return { ok: false, error: "no result received" };
|
|
202
|
+
})();
|
|
203
|
+
const timeout = new Promise((resolve) => {
|
|
204
|
+
setTimeout(() => {
|
|
205
|
+
try {
|
|
206
|
+
stream.interrupt().catch(() => stream.close());
|
|
207
|
+
}
|
|
208
|
+
catch { }
|
|
209
|
+
resolve({ ok: false, error: `timeout after ${Math.round(timeoutMs / 1000)}s` });
|
|
210
|
+
}, timeoutMs);
|
|
211
|
+
});
|
|
212
|
+
return await Promise.race([consume, timeout]);
|
|
213
|
+
}
|
|
214
|
+
catch (err) {
|
|
215
|
+
return { ok: false, error: String(err?.message || err).slice(0, 200) };
|
|
216
|
+
}
|
|
217
|
+
finally {
|
|
218
|
+
try {
|
|
219
|
+
pq?.close();
|
|
220
|
+
}
|
|
221
|
+
catch { }
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Build a single resolver that swarm.ts and planner-query.ts share. Maps a
|
|
226
|
+
* model string to the env overrides that should be passed to `query()`.
|
|
227
|
+
* Returns undefined for Anthropic-native models (let the SDK use process.env).
|
|
228
|
+
*/
|
|
229
|
+
export function buildEnvResolver(opts) {
|
|
230
|
+
const byModel = new Map();
|
|
231
|
+
if (opts.plannerProvider)
|
|
232
|
+
byModel.set(opts.plannerModel, opts.plannerProvider);
|
|
233
|
+
if (opts.workerProvider)
|
|
234
|
+
byModel.set(opts.workerModel, opts.workerProvider);
|
|
235
|
+
if (byModel.size === 0)
|
|
236
|
+
return () => undefined;
|
|
237
|
+
return (model) => {
|
|
238
|
+
if (!model)
|
|
239
|
+
return undefined;
|
|
240
|
+
const p = byModel.get(model);
|
|
241
|
+
return p ? envFor(p) : undefined;
|
|
242
|
+
};
|
|
243
|
+
}
|
package/dist/render.js
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
import chalk from "chalk";
|
|
2
|
+
import { RATE_LIMIT_WINDOW_SHORT } from "./types.js";
|
|
2
3
|
const SPINNER = ["|", "/", "-", "\\"];
|
|
3
|
-
const WINDOW_SHORT_NAMES = {
|
|
4
|
-
five_hour: "5h", seven_day: "7d", seven_day_opus: "7d op",
|
|
5
|
-
seven_day_sonnet: "7d sn", overage: "extra",
|
|
6
|
-
};
|
|
7
4
|
// ── Shared helpers ──
|
|
8
5
|
export function truncate(s, max) {
|
|
9
6
|
return s.length <= max ? s : s.slice(0, max - 1) + "\u2026";
|
|
@@ -90,13 +87,23 @@ function renderUsageBars(out, w, swarm) {
|
|
|
90
87
|
? chalk.red(`Budget $${swarm.extraUsageBudget} exhausted \u2014 finishing active`)
|
|
91
88
|
: chalk.yellow(`Capped at ${capFrac != null ? Math.round(capFrac * 100) : 100}% \u2014 finishing active`);
|
|
92
89
|
}
|
|
93
|
-
else if (swarm.rateLimitPaused > 0) {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
const
|
|
99
|
-
|
|
90
|
+
else if (swarm.rateLimitPaused > 0 || (swarm.rateLimitResetsAt && swarm.rateLimitResetsAt > Date.now())) {
|
|
91
|
+
const mcw = swarm.mostConstrainedWindow();
|
|
92
|
+
const when = (mcw?.resetsAt && mcw.resetsAt > Date.now()) ? mcw.resetsAt
|
|
93
|
+
: (swarm.rateLimitResetsAt && swarm.rateLimitResetsAt > Date.now()) ? swarm.rateLimitResetsAt
|
|
94
|
+
: undefined;
|
|
95
|
+
const winName = mcw ? (RATE_LIMIT_WINDOW_SHORT[mcw.type] ?? mcw.type.replace(/_/g, " ")) : undefined;
|
|
96
|
+
let txt = winName
|
|
97
|
+
? `Anthropic ${winName} limit hit`
|
|
98
|
+
: `Rate limited`;
|
|
99
|
+
if (when) {
|
|
100
|
+
const waitSec = Math.ceil((when - Date.now()) / 1000);
|
|
101
|
+
const mm = Math.floor(waitSec / 60), ss = waitSec % 60;
|
|
102
|
+
txt += ` \u2014 resets in ${mm > 0 ? `${mm}m ${ss}s` : `${ss}s`}`;
|
|
103
|
+
}
|
|
104
|
+
if (swarm.rateLimitPaused > 0)
|
|
105
|
+
txt += ` (${swarm.rateLimitPaused} waiting)`;
|
|
106
|
+
label = chalk.yellow(txt);
|
|
100
107
|
}
|
|
101
108
|
if (swarm.isUsingOverage && !swarm.cappedOut)
|
|
102
109
|
label += chalk.red(" [OVERAGE]");
|
|
@@ -106,7 +113,7 @@ function renderUsageBars(out, w, swarm) {
|
|
|
106
113
|
if (windows.length > 1) {
|
|
107
114
|
const cycleIdx = Math.floor(Date.now() / 3000) % windows.length;
|
|
108
115
|
const win = windows[cycleIdx];
|
|
109
|
-
const shortName =
|
|
116
|
+
const shortName = RATE_LIMIT_WINDOW_SHORT[win.type] ?? win.type.replace(/_/g, " ");
|
|
110
117
|
renderBar(win.utilization, shortName);
|
|
111
118
|
const dots = windows.map((_, i) => i === cycleIdx ? "\u25CF" : "\u25CB").join("");
|
|
112
119
|
out[out.length - 1] += chalk.dim(` ${dots}`);
|
|
@@ -195,10 +202,11 @@ export function renderFrame(swarm, showHotkeys, runInfo) {
|
|
|
195
202
|
const pending = runInfo?.pendingSteer ?? 0;
|
|
196
203
|
const chip = pending > 0 ? chalk.cyan(` \u270E ${pending} steer queued`) : "";
|
|
197
204
|
const fixChip = swarm.failed > 0 && swarm.active > 0 ? chalk.yellow(" [f] fix") : "";
|
|
205
|
+
const retryChip = swarm.rateLimitPaused > 0 ? chalk.yellow(" [r] retry-now") : "";
|
|
198
206
|
const pauseLabel = swarm.paused ? "[p] resume" : "[p] pause";
|
|
199
|
-
out.push(chalk.dim(` [b] budget [t] cap [c] conc [e] extra ${pauseLabel} [s] steer [?] ask [q] stop`) + fixChip + chip);
|
|
207
|
+
out.push(chalk.dim(` [b] budget [t] cap [c] conc [e] extra ${pauseLabel} [s] steer [?] ask [q] stop`) + fixChip + retryChip + chip);
|
|
200
208
|
if (swarm.blocked > 0 && swarm.blocked === swarm.active) {
|
|
201
|
-
out.push(chalk.yellow(` all workers rate-limited —
|
|
209
|
+
out.push(chalk.yellow(` all workers rate-limited — [r] retry-now, [c] reduce concurrency, [p] pause, [q] quit`));
|
|
202
210
|
}
|
|
203
211
|
}
|
|
204
212
|
out.push("");
|
package/dist/run.d.ts
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
import type { Task, PermMode, MergeStrategy, RunState, WaveSummary } from "./types.js";
|
|
2
|
+
import type { ProviderConfig } from "./providers.js";
|
|
2
3
|
export interface RunConfig {
|
|
3
4
|
tasks: Task[];
|
|
4
5
|
objective?: string;
|
|
5
6
|
budget: number;
|
|
6
7
|
workerModel: string;
|
|
7
8
|
plannerModel: string;
|
|
9
|
+
/** Custom provider for worker tasks (optional — Anthropic default when undefined). */
|
|
10
|
+
workerProvider?: ProviderConfig;
|
|
11
|
+
/** Custom provider for planner/steering calls (optional). */
|
|
12
|
+
plannerProvider?: ProviderConfig;
|
|
8
13
|
concurrency: number;
|
|
9
14
|
permissionMode: PermMode;
|
|
10
15
|
useWorktrees: boolean;
|
package/dist/run.js
CHANGED
|
@@ -4,7 +4,8 @@ import { execSync } from "child_process";
|
|
|
4
4
|
import chalk from "chalk";
|
|
5
5
|
import { Swarm } from "./swarm.js";
|
|
6
6
|
import { steerWave } from "./steering.js";
|
|
7
|
-
import { getTotalPlannerCost, getPlannerRateLimitInfo, runPlannerQuery } from "./planner-query.js";
|
|
7
|
+
import { getTotalPlannerCost, getPlannerRateLimitInfo, runPlannerQuery, setPlannerEnvResolver } from "./planner-query.js";
|
|
8
|
+
import { buildEnvResolver } from "./providers.js";
|
|
8
9
|
import { RunDisplay } from "./ui.js";
|
|
9
10
|
import { renderSummary } from "./render.js";
|
|
10
11
|
import { fmtTokens } from "./render.js";
|
|
@@ -16,6 +17,11 @@ export async function executeRun(cfg) {
|
|
|
16
17
|
}
|
|
17
18
|
catch { } };
|
|
18
19
|
const { objective, cwd, workerModel, plannerModel, concurrency, permissionMode, allowedTools, runDir, previousKnowledge, } = cfg;
|
|
20
|
+
const envForModel = buildEnvResolver({
|
|
21
|
+
plannerModel, plannerProvider: cfg.plannerProvider,
|
|
22
|
+
workerModel, workerProvider: cfg.workerProvider,
|
|
23
|
+
});
|
|
24
|
+
setPlannerEnvResolver(envForModel);
|
|
19
25
|
let { usageCap, flex } = cfg;
|
|
20
26
|
const useWorktrees = cfg.useWorktrees;
|
|
21
27
|
const mergeStrategy = cfg.mergeStrategy;
|
|
@@ -311,7 +317,7 @@ export async function executeRun(cfg) {
|
|
|
311
317
|
tasks: currentTasks, concurrency, cwd, model: workerModel, permissionMode, allowedTools,
|
|
312
318
|
useWorktrees, mergeStrategy: waveMerge, agentTimeoutMs: cfg.agentTimeoutMs,
|
|
313
319
|
usageCap, allowExtraUsage: cfg.allowExtraUsage, extraUsageBudget: cfg.extraUsageBudget,
|
|
314
|
-
baseCostUsd: accCost,
|
|
320
|
+
baseCostUsd: accCost, envForModel,
|
|
315
321
|
});
|
|
316
322
|
currentSwarm = swarm;
|
|
317
323
|
display.setWave(swarm);
|
|
@@ -359,7 +365,9 @@ export async function executeRun(cfg) {
|
|
|
359
365
|
const neverStarted = currentTasks.filter(t => !attemptedPrompts.has(t.prompt));
|
|
360
366
|
saveRunState(runDir, {
|
|
361
367
|
id: `run-${new Date().toISOString().slice(0, 19)}`, objective: objective ?? "", budget: cfg.budget,
|
|
362
|
-
remaining, workerModel, plannerModel,
|
|
368
|
+
remaining, workerModel, plannerModel,
|
|
369
|
+
workerProviderId: cfg.workerProvider?.id, plannerProviderId: cfg.plannerProvider?.id,
|
|
370
|
+
concurrency, permissionMode,
|
|
363
371
|
usageCap, allowExtraUsage: cfg.allowExtraUsage, extraUsageBudget: cfg.extraUsageBudget,
|
|
364
372
|
flex, useWorktrees, mergeStrategy, waveNum, currentTasks: neverStarted,
|
|
365
373
|
accCost, accCompleted, accFailed, accIn, accOut, accTools,
|
|
@@ -417,7 +425,9 @@ export async function executeRun(cfg) {
|
|
|
417
425
|
const finalPhase = trulyDone ? "done" : wasCapped ? "capped" : remaining <= 0 ? "capped" : "stopped";
|
|
418
426
|
saveRunState(runDir, {
|
|
419
427
|
id: `run-${new Date().toISOString().slice(0, 19)}`, objective: objective ?? "", budget: cfg.budget,
|
|
420
|
-
remaining, workerModel, plannerModel,
|
|
428
|
+
remaining, workerModel, plannerModel,
|
|
429
|
+
workerProviderId: cfg.workerProvider?.id, plannerProviderId: cfg.plannerProvider?.id,
|
|
430
|
+
concurrency, permissionMode,
|
|
421
431
|
usageCap, allowExtraUsage: cfg.allowExtraUsage, extraUsageBudget: cfg.extraUsageBudget,
|
|
422
432
|
flex, useWorktrees, mergeStrategy, waveNum, currentTasks: [],
|
|
423
433
|
accCost, accCompleted, accFailed, accIn, accOut, accTools,
|
package/dist/swarm.d.ts
CHANGED
|
@@ -15,6 +15,8 @@ export interface SwarmConfig {
|
|
|
15
15
|
allowExtraUsage?: boolean;
|
|
16
16
|
extraUsageBudget?: number;
|
|
17
17
|
baseCostUsd?: number;
|
|
18
|
+
/** Per-task env overrides: given a model id, return the env to pass to `query()` (or undefined for Anthropic default). */
|
|
19
|
+
envForModel?: (model?: string) => Record<string, string> | undefined;
|
|
18
20
|
}
|
|
19
21
|
export declare class Swarm {
|
|
20
22
|
readonly agents: AgentState[];
|
|
@@ -41,6 +43,8 @@ export declare class Swarm {
|
|
|
41
43
|
rateLimitPaused: number;
|
|
42
44
|
isUsingOverage: boolean;
|
|
43
45
|
overageCostUsd: number;
|
|
46
|
+
private rateLimitExplained;
|
|
47
|
+
private rateLimitWakers;
|
|
44
48
|
/** Live-adjustable concurrency target. Workers above this count exit on the next task boundary. */
|
|
45
49
|
targetConcurrency: number;
|
|
46
50
|
/** When true, dispatch is frozen — workers wait without starting new tasks. */
|
|
@@ -76,6 +80,13 @@ export declare class Swarm {
|
|
|
76
80
|
setConcurrency(n: number): void;
|
|
77
81
|
/** Freeze/resume dispatch without killing the run. Paused workers block at the top of their loop. */
|
|
78
82
|
setPaused(b: boolean): void;
|
|
83
|
+
/** Returns the rate-limit window currently holding the swarm back — rejected first, then highest utilization. */
|
|
84
|
+
mostConstrainedWindow(): RateLimitWindow | undefined;
|
|
85
|
+
private windowTag;
|
|
86
|
+
/** Cancellable sleep used by rate-limit waits. `retryRateLimitNow()` wakes every pending sleeper. */
|
|
87
|
+
private rateLimitSleep;
|
|
88
|
+
/** Force-wake every rate-limit sleeper and clear the reset timestamp so the next attempt fires immediately. */
|
|
89
|
+
retryRateLimitNow(): void;
|
|
79
90
|
/** Live-adjust the overage spend cap. `undefined` = unlimited. If already over the new cap, stop dispatch. */
|
|
80
91
|
setExtraUsageBudget(n: number | undefined): void;
|
|
81
92
|
run(): Promise<void>;
|
package/dist/swarm.js
CHANGED
|
@@ -2,7 +2,7 @@ import { mkdtempSync, rmSync } from "fs";
|
|
|
2
2
|
import { join } from "path";
|
|
3
3
|
import { tmpdir } from "os";
|
|
4
4
|
import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
5
|
-
import { NudgeError } from "./types.js";
|
|
5
|
+
import { NudgeError, RATE_LIMIT_WINDOW_SHORT } from "./types.js";
|
|
6
6
|
import { gitExec, autoCommit, mergeAllBranches, warnDirtyTree, cleanStaleWorktrees, writeSwarmLog } from "./merge.js";
|
|
7
7
|
const SIMPLIFY_PROMPT = `You just finished your task. Now review and simplify your changes.
|
|
8
8
|
|
|
@@ -34,6 +34,8 @@ export class Swarm {
|
|
|
34
34
|
rateLimitPaused = 0;
|
|
35
35
|
isUsingOverage = false;
|
|
36
36
|
overageCostUsd = 0;
|
|
37
|
+
rateLimitExplained = false;
|
|
38
|
+
rateLimitWakers = [];
|
|
37
39
|
/** Live-adjustable concurrency target. Workers above this count exit on the next task boundary. */
|
|
38
40
|
targetConcurrency;
|
|
39
41
|
/** When true, dispatch is frozen — workers wait without starting new tasks. */
|
|
@@ -109,6 +111,56 @@ export class Swarm {
|
|
|
109
111
|
this.paused = b;
|
|
110
112
|
this.log(-1, b ? "Dispatch paused" : "Dispatch resumed");
|
|
111
113
|
}
|
|
114
|
+
/** Returns the rate-limit window currently holding the swarm back — rejected first, then highest utilization. */
|
|
115
|
+
mostConstrainedWindow() {
|
|
116
|
+
const windows = Array.from(this.rateLimitWindows.values());
|
|
117
|
+
if (windows.length === 0)
|
|
118
|
+
return undefined;
|
|
119
|
+
const rejected = windows.find(w => w.status === "rejected" && (!w.resetsAt || w.resetsAt > Date.now()));
|
|
120
|
+
if (rejected)
|
|
121
|
+
return rejected;
|
|
122
|
+
return windows.reduce((a, b) => (a.utilization >= b.utilization ? a : b));
|
|
123
|
+
}
|
|
124
|
+
windowTag() {
|
|
125
|
+
const w = this.mostConstrainedWindow();
|
|
126
|
+
if (!w)
|
|
127
|
+
return "";
|
|
128
|
+
const name = RATE_LIMIT_WINDOW_SHORT[w.type] ?? w.type.replace(/_/g, " ");
|
|
129
|
+
return ` (${name} window)`;
|
|
130
|
+
}
|
|
131
|
+
/** Cancellable sleep used by rate-limit waits. `retryRateLimitNow()` wakes every pending sleeper. */
|
|
132
|
+
rateLimitSleep(ms) {
|
|
133
|
+
return new Promise(resolve => {
|
|
134
|
+
let done = false;
|
|
135
|
+
const finish = () => {
|
|
136
|
+
if (done)
|
|
137
|
+
return;
|
|
138
|
+
done = true;
|
|
139
|
+
clearTimeout(timer);
|
|
140
|
+
const i = this.rateLimitWakers.indexOf(finish);
|
|
141
|
+
if (i >= 0)
|
|
142
|
+
this.rateLimitWakers.splice(i, 1);
|
|
143
|
+
resolve();
|
|
144
|
+
};
|
|
145
|
+
const timer = setTimeout(finish, ms);
|
|
146
|
+
this.rateLimitWakers.push(finish);
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
/** Force-wake every rate-limit sleeper and clear the reset timestamp so the next attempt fires immediately. */
|
|
150
|
+
retryRateLimitNow() {
|
|
151
|
+
const n = this.rateLimitWakers.length;
|
|
152
|
+
if (n === 0) {
|
|
153
|
+
this.log(-1, "Retry-now: no workers waiting on rate limit");
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
this.rateLimitResetsAt = undefined;
|
|
157
|
+
this.rateLimitUtilization = 0;
|
|
158
|
+
const wakers = this.rateLimitWakers.slice();
|
|
159
|
+
this.rateLimitWakers.length = 0;
|
|
160
|
+
for (const w of wakers)
|
|
161
|
+
w();
|
|
162
|
+
this.log(-1, `Retry-now: woke ${n} worker(s) — hitting API immediately (may be rejected again)`);
|
|
163
|
+
}
|
|
112
164
|
/** Live-adjust the overage spend cap. `undefined` = unlimited. If already over the new cap, stop dispatch. */
|
|
113
165
|
setExtraUsageBudget(n) {
|
|
114
166
|
if (this.extraUsageBudget === n)
|
|
@@ -319,10 +371,10 @@ export class Swarm {
|
|
|
319
371
|
: fallbackMs;
|
|
320
372
|
const reason = capExceeded
|
|
321
373
|
? `Usage at ${Math.round(this.rateLimitUtilization * 100)}% (cap ${Math.round(cap * 100)}%)`
|
|
322
|
-
:
|
|
323
|
-
this.log(-1, `${reason} — waiting ${Math.ceil(waitMs / 1000)}s then retrying`);
|
|
374
|
+
: `Rate limited${this.windowTag()}`;
|
|
375
|
+
this.log(-1, `${reason} — waiting ${Math.ceil(waitMs / 1000)}s then retrying ([r] to retry now)`);
|
|
324
376
|
this.rateLimitPaused++;
|
|
325
|
-
await
|
|
377
|
+
await this.rateLimitSleep(waitMs);
|
|
326
378
|
this.rateLimitPaused--;
|
|
327
379
|
this.rateLimitUtilization = 0;
|
|
328
380
|
this.rateLimitResetsAt = undefined;
|
|
@@ -402,13 +454,16 @@ export class Swarm {
|
|
|
402
454
|
: this.config.useWorktrees && !task.noWorktree
|
|
403
455
|
? `You are working in an isolated git worktree. Focus only on this task. Do NOT commit your changes — the framework handles that.\n\n${preamble}${task.prompt}`
|
|
404
456
|
: `${preamble}${task.prompt}`;
|
|
457
|
+
const effectiveModel = task.model || this.config.model;
|
|
458
|
+
const envOverride = this.config.envForModel?.(effectiveModel);
|
|
405
459
|
const agentQuery = query({
|
|
406
460
|
prompt: agentPrompt,
|
|
407
461
|
options: {
|
|
408
|
-
cwd: agentCwd, model:
|
|
462
|
+
cwd: agentCwd, model: effectiveModel, permissionMode: perm,
|
|
409
463
|
...(perm === "bypassPermissions" && { allowDangerouslySkipPermissions: true }),
|
|
410
464
|
allowedTools: this.config.allowedTools, includePartialMessages: true, persistSession: true,
|
|
411
465
|
...(isResume && resumeSessionId && { resume: resumeSessionId }),
|
|
466
|
+
...(envOverride && { env: envOverride }),
|
|
412
467
|
},
|
|
413
468
|
});
|
|
414
469
|
const timeoutMs = isResume ? inactivityMs * 2 : inactivityMs;
|
|
@@ -503,10 +558,10 @@ export class Swarm {
|
|
|
503
558
|
// we eventually surrender instead of looping forever.
|
|
504
559
|
const globallyStalled = Date.now() - this.lastProgressAt > 15 * 60_000;
|
|
505
560
|
const freebie = !globallyStalled;
|
|
506
|
-
this.log(id, `Rate limited — waiting ${Math.ceil(waitMs / 1000)}s${freebie ? " (attempt not counted)" : " (counted — swarm stalled)"}`);
|
|
561
|
+
this.log(id, `Rate limited${this.windowTag()} — waiting ${Math.ceil(waitMs / 1000)}s${freebie ? " (attempt not counted)" : " (counted — swarm stalled)"} ([r] to retry now)`);
|
|
507
562
|
agent.blockedAt = Date.now();
|
|
508
563
|
this.rateLimitPaused++;
|
|
509
|
-
await
|
|
564
|
+
await this.rateLimitSleep(waitMs);
|
|
510
565
|
this.rateLimitPaused--;
|
|
511
566
|
agent.blockedAt = undefined;
|
|
512
567
|
this.isUsingOverage = false;
|
|
@@ -660,6 +715,12 @@ export class Swarm {
|
|
|
660
715
|
if (!this.rateLimitResetsAt || this.rateLimitResetsAt <= Date.now()) {
|
|
661
716
|
this.rateLimitResetsAt = Date.now() + 60_000;
|
|
662
717
|
}
|
|
718
|
+
if (!this.rateLimitExplained) {
|
|
719
|
+
this.rateLimitExplained = true;
|
|
720
|
+
const name = windowType ? (RATE_LIMIT_WINDOW_SHORT[windowType] ?? windowType.replace(/_/g, " ")) : "Anthropic";
|
|
721
|
+
const overageNote = this.isUsingOverage ? " even on overage" : "";
|
|
722
|
+
this.log(-1, `${name} window is full${overageNote} — plan-level Anthropic limit, not a claude-overnight cap. Press [r] to retry now, [c] to lower concurrency, or wait for reset.`);
|
|
723
|
+
}
|
|
663
724
|
throw new Error("rate limit rejected — retrying");
|
|
664
725
|
}
|
|
665
726
|
break;
|
package/dist/types.d.ts
CHANGED
|
@@ -116,6 +116,7 @@ export interface RateLimitWindow {
|
|
|
116
116
|
status: string;
|
|
117
117
|
resetsAt?: number;
|
|
118
118
|
}
|
|
119
|
+
export declare const RATE_LIMIT_WINDOW_SHORT: Record<string, string>;
|
|
119
120
|
/** Thrown when a query goes silent — carries session ID for interrupt+resume. */
|
|
120
121
|
export declare class NudgeError extends Error {
|
|
121
122
|
sessionId: string | undefined;
|
|
@@ -160,6 +161,10 @@ export interface RunState {
|
|
|
160
161
|
remaining: number;
|
|
161
162
|
workerModel: string;
|
|
162
163
|
plannerModel: string;
|
|
164
|
+
/** Optional id of a custom provider (from ~/.claude/claude-overnight/providers.json) used for worker tasks. */
|
|
165
|
+
workerProviderId?: string;
|
|
166
|
+
/** Optional id of a custom provider used for planner/steering calls. */
|
|
167
|
+
plannerProviderId?: string;
|
|
163
168
|
concurrency: number;
|
|
164
169
|
permissionMode: PermMode;
|
|
165
170
|
usageCap?: number;
|
package/dist/types.js
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
export const RATE_LIMIT_WINDOW_SHORT = {
|
|
2
|
+
five_hour: "5h", seven_day: "7d", seven_day_opus: "7d opus",
|
|
3
|
+
seven_day_sonnet: "7d sonnet", overage: "extra",
|
|
4
|
+
};
|
|
1
5
|
/** Thrown when a query goes silent — carries session ID for interrupt+resume. */
|
|
2
6
|
export class NudgeError extends Error {
|
|
3
7
|
sessionId;
|
package/dist/ui.js
CHANGED
|
@@ -387,6 +387,10 @@ export class RunDisplay {
|
|
|
387
387
|
this.swarm.requeueFailed();
|
|
388
388
|
return false;
|
|
389
389
|
}
|
|
390
|
+
if ((s === "r" || s === "R") && this.swarm && this.swarm.rateLimitPaused > 0) {
|
|
391
|
+
this.swarm.retryRateLimitNow();
|
|
392
|
+
return true;
|
|
393
|
+
}
|
|
390
394
|
if ((s === "s" || s === "S") && this.onSteer) {
|
|
391
395
|
this.inputMode = "steer";
|
|
392
396
|
this.inputSegs = [];
|
package/package.json
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-overnight",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.16.1",
|
|
4
4
|
"description": "Run 10, 100, or 1000 Claude agents overnight. Parallel autonomous AI coding with thinking waves, iterative quality steering, crash recovery, and rate limit handling. Built on the Claude Agent SDK.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
-
"claude-overnight": "dist/
|
|
7
|
+
"claude-overnight": "dist/bin.js"
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
10
|
"build": "tsc",
|
|
11
11
|
"dev": "tsc --watch",
|
|
12
|
-
"start": "node dist/
|
|
12
|
+
"start": "node dist/bin.js",
|
|
13
13
|
"prepublishOnly": "node scripts/sync-plugin-version.js"
|
|
14
14
|
},
|
|
15
15
|
"dependencies": {
|