claude-overnight 1.25.20 → 1.25.22
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/dist/_version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const VERSION = "1.25.
|
|
1
|
+
export declare const VERSION = "1.25.22";
|
package/dist/_version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// Auto-generated by build — do not edit manually.
|
|
2
|
-
export const VERSION = "1.25.
|
|
2
|
+
export const VERSION = "1.25.22";
|
package/dist/index.js
CHANGED
|
@@ -822,14 +822,44 @@ async function main() {
|
|
|
822
822
|
// proxy now uses an account pool (`CURSOR_CONFIG_DIRS`) — each parallel
|
|
823
823
|
// cursor-agent subprocess gets its own config dir, eliminating the race.
|
|
824
824
|
// See ensureCursorAccountPool() in providers.ts.
|
|
825
|
-
|
|
825
|
+
//
|
|
826
|
+
// Single in-place status line collapses N parallel progress streams (one
|
|
827
|
+
// per provider) into one tty line updated via `\r` + ANSI clear. Keeps the
|
|
828
|
+
// "window head" calm instead of appending 3 lines per 3s tick.
|
|
829
|
+
const statuses = new Map();
|
|
830
|
+
const isTTY = process.stdout.isTTY;
|
|
831
|
+
let statusLineActive = false;
|
|
832
|
+
const renderStatus = () => {
|
|
833
|
+
if (!isTTY)
|
|
834
|
+
return;
|
|
835
|
+
const parts = [...statuses.entries()].map(([r, s]) => `${r} ${chalk.dim(s)}`);
|
|
836
|
+
process.stdout.write(`\x1B[2K\r`);
|
|
837
|
+
if (parts.length === 0) {
|
|
838
|
+
statusLineActive = false;
|
|
839
|
+
return;
|
|
840
|
+
}
|
|
841
|
+
process.stdout.write(chalk.dim(" " + parts.join(" · ")));
|
|
842
|
+
statusLineActive = true;
|
|
843
|
+
};
|
|
844
|
+
const clearStatusLine = () => {
|
|
845
|
+
if (isTTY && statusLineActive) {
|
|
846
|
+
process.stdout.write(`\x1B[2K\r`);
|
|
847
|
+
statusLineActive = false;
|
|
848
|
+
}
|
|
849
|
+
};
|
|
826
850
|
/** Cursor agent cold start + thinking-variant model latency can exceed 20s; API providers stay tight. */
|
|
827
851
|
const preflightMs = (p) => isCursorProxyProvider(p) ? 60_000 : 20_000;
|
|
828
|
-
const results = await Promise.all(pending.map(async ([role, p]) =>
|
|
829
|
-
role,
|
|
830
|
-
|
|
831
|
-
result
|
|
832
|
-
|
|
852
|
+
const results = await Promise.all(pending.map(async ([role, p]) => {
|
|
853
|
+
statuses.set(role, "connecting…");
|
|
854
|
+
renderStatus();
|
|
855
|
+
const result = await preflightProvider(p, cwd, preflightMs(p), {
|
|
856
|
+
onProgress: (msg) => { statuses.set(role, msg); renderStatus(); },
|
|
857
|
+
});
|
|
858
|
+
statuses.delete(role);
|
|
859
|
+
renderStatus();
|
|
860
|
+
return { role, provider: p, result };
|
|
861
|
+
}));
|
|
862
|
+
clearStatusLine();
|
|
833
863
|
for (const { role, provider, result } of results) {
|
|
834
864
|
if (!result.ok) {
|
|
835
865
|
console.error(chalk.red(` ✗ ${role} preflight failed: ${chalk.dim(result.error)}`));
|
package/dist/planner-query.d.ts
CHANGED
|
@@ -25,6 +25,16 @@ export interface PlannerOpts {
|
|
|
25
25
|
};
|
|
26
26
|
/** When set, stream events are appended to <runDir>/transcripts/<name>.ndjson */
|
|
27
27
|
transcriptName?: string;
|
|
28
|
+
/**
|
|
29
|
+
* Hard cap on conversation turns. Defaults to 20.
|
|
30
|
+
*/
|
|
31
|
+
maxTurns?: number;
|
|
32
|
+
/**
|
|
33
|
+
* Tools the planner agent may use. Defaults to read-only + Write (for outFile
|
|
34
|
+
* resilience). Deliberately excludes Bash/Agent/TodoWrite/WebFetch to prevent
|
|
35
|
+
* the multi-turn tool loops that cause error_max_turns with thinking models.
|
|
36
|
+
*/
|
|
37
|
+
tools?: string[];
|
|
28
38
|
}
|
|
29
39
|
export declare function setPlannerEnvResolver(fn: ((model?: string) => Record<string, string> | undefined) | undefined): void;
|
|
30
40
|
export declare function getTotalPlannerCost(): number;
|
package/dist/planner-query.js
CHANGED
|
@@ -2,6 +2,7 @@ import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
|
2
2
|
import { readFileSync } from "fs";
|
|
3
3
|
import { NudgeError } from "./types.js";
|
|
4
4
|
import { writeTranscriptEvent } from "./transcripts.js";
|
|
5
|
+
const DEFAULT_MAX_TURNS = 20;
|
|
5
6
|
// ── Shared env resolver (set once at run start, used by every planner query) ──
|
|
6
7
|
//
|
|
7
8
|
// Swarm and planner calls share a model→env map so a custom provider configured
|
|
@@ -143,20 +144,32 @@ async function runPlannerQueryOnce(prompt, opts, onLog) {
|
|
|
143
144
|
options: {
|
|
144
145
|
cwd: opts.cwd,
|
|
145
146
|
model: opts.model,
|
|
146
|
-
tools: ["Read", "Glob", "Grep", "Write"
|
|
147
|
-
allowedTools: ["Read", "Glob", "Grep", "Write"
|
|
147
|
+
tools: opts.tools ?? ["Read", "Glob", "Grep", "Write"],
|
|
148
|
+
allowedTools: opts.tools ?? ["Read", "Glob", "Grep", "Write"],
|
|
148
149
|
permissionMode: opts.permissionMode,
|
|
149
150
|
...(opts.permissionMode === "bypassPermissions" && { allowDangerouslySkipPermissions: true }),
|
|
150
151
|
persistSession: true,
|
|
151
152
|
includePartialMessages: true,
|
|
153
|
+
maxTurns: opts.maxTurns ?? DEFAULT_MAX_TURNS,
|
|
152
154
|
...(isResume && { resume: opts.resumeSessionId }),
|
|
153
155
|
...(opts.outputFormat && { outputFormat: opts.outputFormat }),
|
|
154
156
|
...(envOverride && { env: envOverride }),
|
|
155
157
|
},
|
|
156
158
|
});
|
|
157
|
-
|
|
159
|
+
// Default to "thinking…" so the ticker conveys meaning during the pre-output
|
|
160
|
+
// reasoning phase. Thinking-variant models (e.g. claude-opus-4-7-thinking-*)
|
|
161
|
+
// can sit silent for 60-90s before emitting any tokens, and cursor-agent
|
|
162
|
+
// doesn't forward thinking deltas — without this, the ticker reads "4m 5s"
|
|
163
|
+
// with nothing else for a minute plus.
|
|
164
|
+
let lastLogText = "thinking…";
|
|
158
165
|
let toolCount = 0;
|
|
159
166
|
let costUsd = 0;
|
|
167
|
+
const jsonOutput = opts.outputFormat?.type === "json_schema";
|
|
168
|
+
let jsonCharCount = 0;
|
|
169
|
+
// Dedup identical text snippets: cursor-agent with json_schema-ignoring
|
|
170
|
+
// proxies causes the SDK to loop multiple turns, each re-emitting the same
|
|
171
|
+
// final JSON. We don't want to spam the ticker or transcript with it.
|
|
172
|
+
let lastTextSeen = "";
|
|
160
173
|
const ticker = setInterval(() => {
|
|
161
174
|
const elapsed = Math.round((Date.now() - startedAt) / 1000);
|
|
162
175
|
const m = Math.floor(elapsed / 60);
|
|
@@ -202,6 +215,11 @@ async function runPlannerQueryOnce(prompt, opts, onLog) {
|
|
|
202
215
|
// Track the open tool block so we can re-log with the enriched target once
|
|
203
216
|
// the input arrives, and write a complete transcript entry on block stop.
|
|
204
217
|
let pendingTool = null;
|
|
218
|
+
// Dedup tool_use logging between stream_event path and full-assistant-message
|
|
219
|
+
// path. The Cursor proxy doesn't always relay content_block_* frames through
|
|
220
|
+
// the Claude CLI's stream-json, so we also mine complete `SDKAssistantMessage`
|
|
221
|
+
// content for progress -- without double-counting when both paths fire.
|
|
222
|
+
const seenToolIds = new Set();
|
|
205
223
|
const logTool = (name, input) => {
|
|
206
224
|
const target = extractToolTarget(input);
|
|
207
225
|
lastLogText = target ? `${name} ${target}` : name;
|
|
@@ -220,6 +238,8 @@ async function runPlannerQueryOnce(prompt, opts, onLog) {
|
|
|
220
238
|
toolCount++;
|
|
221
239
|
const input = (cb.input ?? {});
|
|
222
240
|
const hasInput = Object.keys(input).length > 0;
|
|
241
|
+
if (cb.id)
|
|
242
|
+
seenToolIds.add(cb.id);
|
|
223
243
|
pendingTool = {
|
|
224
244
|
index: ev.index ?? 0,
|
|
225
245
|
name: cb.name,
|
|
@@ -252,9 +272,18 @@ async function runPlannerQueryOnce(prompt, opts, onLog) {
|
|
|
252
272
|
: delta?.type === "thinking_delta" ? delta.thinking
|
|
253
273
|
: undefined;
|
|
254
274
|
if (typeof raw === "string" && raw) {
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
275
|
+
if (jsonOutput && delta.type === "text_delta") {
|
|
276
|
+
// Don't surface tail-of-JSON as "progress" — it reads as noise
|
|
277
|
+
// like `…ppression and optimistic-update rollback`. Show size
|
|
278
|
+
// growing instead, which is a genuine signal.
|
|
279
|
+
jsonCharCount += raw.length;
|
|
280
|
+
lastLogText = `writing JSON (${jsonCharCount} chars)…`;
|
|
281
|
+
}
|
|
282
|
+
else {
|
|
283
|
+
const snippet = raw.trim().replace(/[{}"\\,[\]]+/g, " ").replace(/\s+/g, " ").trim();
|
|
284
|
+
if (snippet.length > 5)
|
|
285
|
+
lastLogText = snippet.slice(-60);
|
|
286
|
+
}
|
|
258
287
|
if (tname)
|
|
259
288
|
writeTranscriptEvent(tname, { kind: delta.type, text: raw });
|
|
260
289
|
}
|
|
@@ -274,6 +303,48 @@ async function runPlannerQueryOnce(prompt, opts, onLog) {
|
|
|
274
303
|
pendingTool = null;
|
|
275
304
|
}
|
|
276
305
|
}
|
|
306
|
+
// Fallback progress surfacing: when stream events are sparse (e.g. the
|
|
307
|
+
// Cursor proxy's heartbeat thinking block doesn't always round-trip
|
|
308
|
+
// through the Claude CLI as partial messages), mine the full assistant
|
|
309
|
+
// turn message for tool_use / thinking / text so the ticker still moves
|
|
310
|
+
// every ~6-15s instead of sitting silent for minutes.
|
|
311
|
+
if (msg.type === "assistant") {
|
|
312
|
+
const content = msg.message?.content;
|
|
313
|
+
if (Array.isArray(content)) {
|
|
314
|
+
for (const part of content) {
|
|
315
|
+
if (part?.type === "tool_use" && part.id && !seenToolIds.has(part.id)) {
|
|
316
|
+
seenToolIds.add(part.id);
|
|
317
|
+
toolCount++;
|
|
318
|
+
const input = (part.input ?? {});
|
|
319
|
+
logTool(part.name, input);
|
|
320
|
+
if (tname)
|
|
321
|
+
writeTranscriptEvent(tname, { kind: "tool_use", tool: part.name, input });
|
|
322
|
+
}
|
|
323
|
+
else if (part?.type === "thinking" && typeof part.thinking === "string" && part.thinking) {
|
|
324
|
+
const snippet = part.thinking.trim().replace(/\s+/g, " ").slice(-60);
|
|
325
|
+
if (snippet)
|
|
326
|
+
lastLogText = snippet;
|
|
327
|
+
if (tname)
|
|
328
|
+
writeTranscriptEvent(tname, { kind: "thinking", text: part.thinking });
|
|
329
|
+
}
|
|
330
|
+
else if (part?.type === "text" && typeof part.text === "string" && part.text) {
|
|
331
|
+
if (part.text === lastTextSeen)
|
|
332
|
+
continue; // dedup repeated turns
|
|
333
|
+
lastTextSeen = part.text;
|
|
334
|
+
if (jsonOutput) {
|
|
335
|
+
lastLogText = `writing JSON (${part.text.length} chars)…`;
|
|
336
|
+
}
|
|
337
|
+
else {
|
|
338
|
+
const snippet = part.text.trim().replace(/[{}"\\,[\]]+/g, " ").replace(/\s+/g, " ").slice(-60);
|
|
339
|
+
if (snippet.length > 5)
|
|
340
|
+
lastLogText = snippet;
|
|
341
|
+
}
|
|
342
|
+
if (tname)
|
|
343
|
+
writeTranscriptEvent(tname, { kind: "text", text: part.text });
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
277
348
|
if (msg.type === "rate_limit_event") {
|
|
278
349
|
const info = msg.rate_limit_info;
|
|
279
350
|
if (info) {
|
package/dist/planner.js
CHANGED
|
@@ -158,7 +158,8 @@ export async function planTasks(objective, cwd, plannerModel, workerModel, permi
|
|
|
158
158
|
const fileInstruction = outFile ? `\n\nAFTER generating the JSON, also write it to ${outFile} using the Write tool.` : "";
|
|
159
159
|
let resultText;
|
|
160
160
|
try {
|
|
161
|
-
resultText = await runPlannerQuery(prompt + fileInstruction, { cwd, model: plannerModel, permissionMode, outputFormat: TASKS_SCHEMA, transcriptName
|
|
161
|
+
resultText = await runPlannerQuery(prompt + fileInstruction, { cwd, model: plannerModel, permissionMode, outputFormat: TASKS_SCHEMA, transcriptName, maxTurns: 40,
|
|
162
|
+
tools: ["Read", "Glob", "Grep", "Write"] }, onLog);
|
|
162
163
|
}
|
|
163
164
|
catch (err) {
|
|
164
165
|
const salvaged = salvageFromFile(outFile, budget, onLog, err?.message ?? String(err));
|
|
@@ -168,7 +169,7 @@ export async function planTasks(objective, cwd, plannerModel, workerModel, permi
|
|
|
168
169
|
}
|
|
169
170
|
const parsed = await extractTaskJson(resultText, async () => {
|
|
170
171
|
onLog("Retrying...");
|
|
171
|
-
return runPlannerQuery(`Your previous response was not valid JSON. Respond with ONLY a JSON object {"tasks":[{"prompt":"..."}]}.\n\n${prompt}`, { cwd, model: plannerModel, permissionMode, outputFormat: TASKS_SCHEMA, transcriptName: `${transcriptName}-retry
|
|
172
|
+
return runPlannerQuery(`Your previous response was not valid JSON. Respond with ONLY a JSON object {"tasks":[{"prompt":"..."}]}.\n\n${prompt}`, { cwd, model: plannerModel, permissionMode, outputFormat: TASKS_SCHEMA, transcriptName: `${transcriptName}-retry`, maxTurns: 15 }, onLog);
|
|
172
173
|
}, onLog, outFile);
|
|
173
174
|
let tasks = (parsed.tasks || []).map((t, i) => ({
|
|
174
175
|
id: String(i), prompt: typeof t === "string" ? t : t.prompt,
|
|
@@ -188,7 +189,7 @@ Then pick ${count} angles that carve up THIS specific codebase orthogonally. Pre
|
|
|
188
189
|
|
|
189
190
|
Objective: ${objective}
|
|
190
191
|
|
|
191
|
-
Return ONLY a JSON object: {"themes": ["angle description", ...]}`, { cwd, model, permissionMode, outputFormat: THEMES_SCHEMA, transcriptName }, onLog);
|
|
192
|
+
Return ONLY a JSON object: {"themes": ["angle description", ...]}`, { cwd, model, permissionMode, outputFormat: THEMES_SCHEMA, transcriptName, maxTurns: 12 }, onLog);
|
|
192
193
|
const parsed = attemptJsonParse(resultText);
|
|
193
194
|
if (parsed?.themes && Array.isArray(parsed.themes))
|
|
194
195
|
return parsed.themes.slice(0, count);
|
|
@@ -259,7 +260,8 @@ Respond with ONLY a JSON object (no markdown fences):
|
|
|
259
260
|
onLog("Synthesizing...");
|
|
260
261
|
let resultText;
|
|
261
262
|
try {
|
|
262
|
-
resultText = await runPlannerQuery(prompt, { cwd, model: plannerModel, permissionMode, outputFormat: TASKS_SCHEMA, transcriptName
|
|
263
|
+
resultText = await runPlannerQuery(prompt, { cwd, model: plannerModel, permissionMode, outputFormat: TASKS_SCHEMA, transcriptName, maxTurns: 25,
|
|
264
|
+
tools: ["Write"] }, onLog);
|
|
263
265
|
}
|
|
264
266
|
catch (err) {
|
|
265
267
|
const salvaged = salvageFromFile(outFile, budget, onLog, err?.message ?? String(err));
|
|
@@ -269,7 +271,7 @@ Respond with ONLY a JSON object (no markdown fences):
|
|
|
269
271
|
}
|
|
270
272
|
const parsed = await extractTaskJson(resultText, async () => {
|
|
271
273
|
onLog("Retrying...");
|
|
272
|
-
return runPlannerQuery(`Your previous response was not valid JSON. Respond with ONLY a JSON object {"tasks":[{"prompt":"..."}]}.\n\n${prompt}`, { cwd, model: plannerModel, permissionMode, outputFormat: TASKS_SCHEMA, transcriptName: `${transcriptName}-retry
|
|
274
|
+
return runPlannerQuery(`Your previous response was not valid JSON. Respond with ONLY a JSON object {"tasks":[{"prompt":"..."}]}.\n\n${prompt}`, { cwd, model: plannerModel, permissionMode, outputFormat: TASKS_SCHEMA, transcriptName: `${transcriptName}-retry`, maxTurns: 10 }, onLog);
|
|
273
275
|
}, onLog, outFile);
|
|
274
276
|
let tasks = (parsed.tasks || []).map((t, i) => ({
|
|
275
277
|
id: String(i), prompt: typeof t === "string" ? t : t.prompt,
|
|
@@ -303,10 +305,10 @@ ${scaleNote} ${concurrency} agents run in parallel. Update the plan accordingly.
|
|
|
303
305
|
|
|
304
306
|
Respond with ONLY a JSON object (no markdown):
|
|
305
307
|
{"tasks":[{"prompt":"..."}]}`;
|
|
306
|
-
const resultText = await runPlannerQuery(prompt, { cwd, model: plannerModel, permissionMode, outputFormat: TASKS_SCHEMA, transcriptName }, onLog);
|
|
308
|
+
const resultText = await runPlannerQuery(prompt, { cwd, model: plannerModel, permissionMode, outputFormat: TASKS_SCHEMA, transcriptName, maxTurns: 15 }, onLog);
|
|
307
309
|
const parsed = await extractTaskJson(resultText, async () => {
|
|
308
310
|
onLog("Retrying...");
|
|
309
|
-
return runPlannerQuery(`Your previous response was not valid JSON. Respond with ONLY a JSON object {"tasks":[{"prompt":"..."}]}.\n\n${prompt}`, { cwd, model: plannerModel, permissionMode, outputFormat: TASKS_SCHEMA, transcriptName: `${transcriptName}-retry
|
|
311
|
+
return runPlannerQuery(`Your previous response was not valid JSON. Respond with ONLY a JSON object {"tasks":[{"prompt":"..."}]}.\n\n${prompt}`, { cwd, model: plannerModel, permissionMode, outputFormat: TASKS_SCHEMA, transcriptName: `${transcriptName}-retry`, maxTurns: 8 }, onLog);
|
|
310
312
|
}, onLog);
|
|
311
313
|
let tasks = (parsed.tasks || []).map((t, i) => ({
|
|
312
314
|
id: String(i), prompt: typeof t === "string" ? t : t.prompt,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-overnight",
|
|
3
|
-
"version": "1.25.
|
|
3
|
+
"version": "1.25.22",
|
|
4
4
|
"description": "Parallel Claude agents in git worktrees with a usage cap that reserves headroom for your interactive Claude Code. Crash-safe resume. Provider-agnostic model catalog (Anthropic, Cursor, OpenAI, Gemini, DeepSeek, Llama, Qwen) with capability-based task scoping.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-overnight",
|
|
3
|
-
"version": "1.25.
|
|
3
|
+
"version": "1.25.22",
|
|
4
4
|
"description": "Claude Code skill for understanding, installing, and inspecting claude-overnight runs -- parallel Claude agents in git worktrees with thinking waves, multi-wave steering, and crash-safe resume. Supports Cursor API Proxy, Qwen, OpenRouter.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Francesco Fornace"
|