claude-overnight 1.11.9 → 1.11.11
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/cli.d.ts +27 -0
- package/dist/cli.js +141 -3
- package/dist/merge.d.ts +1 -1
- package/dist/merge.js +44 -20
- package/dist/state.js +4 -1
- package/dist/swarm.js +7 -1
- package/dist/types.d.ts +2 -0
- package/dist/ui.d.ts +5 -1
- package/dist/ui.js +149 -95
- package/package.json +1 -1
package/dist/cli.d.ts
CHANGED
|
@@ -6,6 +6,33 @@ export declare function parseCliFlags(argv: string[]): {
|
|
|
6
6
|
};
|
|
7
7
|
export declare function isAuthError(err: unknown): boolean;
|
|
8
8
|
export declare function fetchModels(timeoutMs?: number): Promise<ModelInfo[]>;
|
|
9
|
+
export declare const PASTE_START = "\u001B[200~";
|
|
10
|
+
export declare const PASTE_END = "\u001B[201~";
|
|
11
|
+
export declare const PASTE_PLACEHOLDER_MAX = 80;
|
|
12
|
+
export type InputSegment = {
|
|
13
|
+
type: "text";
|
|
14
|
+
content: string;
|
|
15
|
+
} | {
|
|
16
|
+
type: "paste";
|
|
17
|
+
content: string;
|
|
18
|
+
};
|
|
19
|
+
/** Split a raw stdin chunk into typed and pasted segments. */
|
|
20
|
+
export declare function splitPaste(chunk: string): Array<{
|
|
21
|
+
type: "typed" | "paste";
|
|
22
|
+
text: string;
|
|
23
|
+
}>;
|
|
24
|
+
export declare function segmentsToString(segs: InputSegment[]): string;
|
|
25
|
+
export declare function renderSegments(segs: InputSegment[]): string;
|
|
26
|
+
export declare function appendCharToSegments(segs: InputSegment[], ch: string): void;
|
|
27
|
+
/** Appends a pasted block. Short single-line pastes inline as text; the rest become placeholders. */
|
|
28
|
+
export declare function appendPasteToSegments(segs: InputSegment[], text: string): void;
|
|
29
|
+
/** Backspace removes one char, or an entire paste block atomically. */
|
|
30
|
+
export declare function backspaceSegments(segs: InputSegment[]): void;
|
|
31
|
+
/**
|
|
32
|
+
* Read a line from the user with bracketed-paste awareness.
|
|
33
|
+
* Pasted multi-line text stays in the buffer as a single block — only a typed
|
|
34
|
+
* Enter submits. Falls back to cooked readline when stdin isn't a TTY.
|
|
35
|
+
*/
|
|
9
36
|
export declare function ask(question: string): Promise<string>;
|
|
10
37
|
export declare function select<T>(label: string, items: {
|
|
11
38
|
name: string;
|
package/dist/cli.js
CHANGED
|
@@ -67,11 +67,149 @@ export async function fetchModels(timeoutMs = 10_000) {
|
|
|
67
67
|
return [];
|
|
68
68
|
}
|
|
69
69
|
}
|
|
70
|
+
// ── Bracketed paste + segment-based input ──
|
|
71
|
+
//
|
|
72
|
+
// When the terminal is in bracketed paste mode, pasted content is wrapped with
|
|
73
|
+
// \x1B[200~ ... \x1B[201~ so we can distinguish typed Enter from pasted newlines.
|
|
74
|
+
// Multi-line or long pastes are stored as opaque segments and shown as a compact
|
|
75
|
+
// [Pasted +N lines] placeholder while editing — the full text is substituted on submit.
|
|
76
|
+
export const PASTE_START = "\x1B[200~";
|
|
77
|
+
export const PASTE_END = "\x1B[201~";
|
|
78
|
+
export const PASTE_PLACEHOLDER_MAX = 80;
|
|
79
|
+
/** Split a raw stdin chunk into typed and pasted segments. */
|
|
80
|
+
export function splitPaste(chunk) {
|
|
81
|
+
const out = [];
|
|
82
|
+
let i = 0;
|
|
83
|
+
while (i < chunk.length) {
|
|
84
|
+
const start = chunk.indexOf(PASTE_START, i);
|
|
85
|
+
if (start === -1) {
|
|
86
|
+
out.push({ type: "typed", text: chunk.slice(i) });
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
if (start > i)
|
|
90
|
+
out.push({ type: "typed", text: chunk.slice(i, start) });
|
|
91
|
+
const bodyStart = start + PASTE_START.length;
|
|
92
|
+
const end = chunk.indexOf(PASTE_END, bodyStart);
|
|
93
|
+
if (end === -1) {
|
|
94
|
+
out.push({ type: "paste", text: chunk.slice(bodyStart) });
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
out.push({ type: "paste", text: chunk.slice(bodyStart, end) });
|
|
98
|
+
i = end + PASTE_END.length;
|
|
99
|
+
}
|
|
100
|
+
return out;
|
|
101
|
+
}
|
|
102
|
+
export function segmentsToString(segs) {
|
|
103
|
+
return segs.map((s) => s.content).join("");
|
|
104
|
+
}
|
|
105
|
+
export function renderSegments(segs) {
|
|
106
|
+
return segs.map((s) => {
|
|
107
|
+
if (s.type === "text")
|
|
108
|
+
return s.content;
|
|
109
|
+
const lines = s.content.split("\n").length;
|
|
110
|
+
return chalk.dim(`[Pasted +${lines} line${lines === 1 ? "" : "s"}]`);
|
|
111
|
+
}).join("");
|
|
112
|
+
}
|
|
113
|
+
export function appendCharToSegments(segs, ch) {
|
|
114
|
+
const last = segs[segs.length - 1];
|
|
115
|
+
if (last && last.type === "text")
|
|
116
|
+
last.content += ch;
|
|
117
|
+
else
|
|
118
|
+
segs.push({ type: "text", content: ch });
|
|
119
|
+
}
|
|
120
|
+
/** Appends a pasted block. Short single-line pastes inline as text; the rest become placeholders. */
|
|
121
|
+
export function appendPasteToSegments(segs, text) {
|
|
122
|
+
if (!text)
|
|
123
|
+
return;
|
|
124
|
+
const norm = text.replace(/\r\n?/g, "\n");
|
|
125
|
+
if (!norm.includes("\n") && norm.length <= PASTE_PLACEHOLDER_MAX) {
|
|
126
|
+
appendCharToSegments(segs, norm);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
segs.push({ type: "paste", content: norm });
|
|
130
|
+
}
|
|
131
|
+
/** Backspace removes one char, or an entire paste block atomically. */
|
|
132
|
+
export function backspaceSegments(segs) {
|
|
133
|
+
while (segs.length > 0) {
|
|
134
|
+
const last = segs[segs.length - 1];
|
|
135
|
+
if (last.type === "paste") {
|
|
136
|
+
segs.pop();
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
if (last.content.length > 1) {
|
|
140
|
+
last.content = last.content.slice(0, -1);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
segs.pop();
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
70
147
|
// ── Interactive primitives ──
|
|
148
|
+
/**
|
|
149
|
+
* Read a line from the user with bracketed-paste awareness.
|
|
150
|
+
* Pasted multi-line text stays in the buffer as a single block — only a typed
|
|
151
|
+
* Enter submits. Falls back to cooked readline when stdin isn't a TTY.
|
|
152
|
+
*/
|
|
71
153
|
export function ask(question) {
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
rl
|
|
154
|
+
const { stdin, stdout } = process;
|
|
155
|
+
if (!stdin.isTTY) {
|
|
156
|
+
const rl = createInterface({ input: stdin, output: stdout });
|
|
157
|
+
return new Promise((res) => rl.question(question, (a) => { rl.close(); res(a.trim()); }));
|
|
158
|
+
}
|
|
159
|
+
return new Promise((resolve) => {
|
|
160
|
+
const segs = [];
|
|
161
|
+
const lastLine = question.split("\n").pop() ?? "";
|
|
162
|
+
const redraw = () => {
|
|
163
|
+
stdout.write("\r\x1B[K" + lastLine + renderSegments(segs));
|
|
164
|
+
};
|
|
165
|
+
stdout.write(question);
|
|
166
|
+
stdout.write("\x1B[?2004h");
|
|
167
|
+
try {
|
|
168
|
+
stdin.setRawMode(true);
|
|
169
|
+
}
|
|
170
|
+
catch { }
|
|
171
|
+
stdin.resume();
|
|
172
|
+
const cleanup = () => {
|
|
173
|
+
stdout.write("\x1B[?2004l");
|
|
174
|
+
try {
|
|
175
|
+
stdin.setRawMode(false);
|
|
176
|
+
}
|
|
177
|
+
catch { }
|
|
178
|
+
stdin.removeListener("data", onData);
|
|
179
|
+
stdin.pause();
|
|
180
|
+
};
|
|
181
|
+
const onData = (buf) => {
|
|
182
|
+
const chunk = buf.toString();
|
|
183
|
+
for (const seg of splitPaste(chunk)) {
|
|
184
|
+
if (seg.type === "paste") {
|
|
185
|
+
appendPasteToSegments(segs, seg.text);
|
|
186
|
+
redraw();
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
for (const ch of seg.text) {
|
|
190
|
+
if (ch === "\r" || ch === "\n") {
|
|
191
|
+
stdout.write("\n");
|
|
192
|
+
cleanup();
|
|
193
|
+
resolve(segmentsToString(segs).trim());
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
if (ch === "\x03") {
|
|
197
|
+
cleanup();
|
|
198
|
+
stdout.write("\n");
|
|
199
|
+
process.exit(130);
|
|
200
|
+
}
|
|
201
|
+
if (ch === "\x7F" || ch === "\b") {
|
|
202
|
+
backspaceSegments(segs);
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
const code = ch.charCodeAt(0);
|
|
206
|
+
if (ch !== "\x1B" && code >= 0x20)
|
|
207
|
+
appendCharToSegments(segs, ch);
|
|
208
|
+
}
|
|
209
|
+
redraw();
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
stdin.on("data", onData);
|
|
75
213
|
});
|
|
76
214
|
}
|
|
77
215
|
export async function select(label, items, defaultIdx = 0) {
|
package/dist/merge.d.ts
CHANGED
|
@@ -7,7 +7,7 @@ export interface MergeResult {
|
|
|
7
7
|
error?: string;
|
|
8
8
|
filesChanged: number;
|
|
9
9
|
}
|
|
10
|
-
export declare function autoCommit(agentId: number, taskPrompt: string, worktreeCwd: string, log: (id: number, msg: string) => void): number;
|
|
10
|
+
export declare function autoCommit(agentId: number, taskPrompt: string, worktreeCwd: string, baseRef: string | undefined, log: (id: number, msg: string) => void): number;
|
|
11
11
|
export interface MergeAllResult {
|
|
12
12
|
mergeResults: MergeResult[];
|
|
13
13
|
mergeBranch?: string;
|
package/dist/merge.js
CHANGED
|
@@ -5,11 +5,13 @@ import { tmpdir } from "os";
|
|
|
5
5
|
export function gitExec(cmd, cwd) {
|
|
6
6
|
return execSync(cmd, { cwd, encoding: "utf-8", stdio: "pipe" });
|
|
7
7
|
}
|
|
8
|
-
export function autoCommit(agentId, taskPrompt, worktreeCwd, log) {
|
|
8
|
+
export function autoCommit(agentId, taskPrompt, worktreeCwd, baseRef, log) {
|
|
9
9
|
if (!existsSync(worktreeCwd)) {
|
|
10
10
|
log(agentId, "Worktree directory gone, skipping commit");
|
|
11
11
|
return 0;
|
|
12
12
|
}
|
|
13
|
+
// Step 1: commit any uncommitted changes. Agents *may* commit their own work;
|
|
14
|
+
// we pick up whatever is still dirty.
|
|
13
15
|
let status;
|
|
14
16
|
try {
|
|
15
17
|
status = gitExec("git status --porcelain", worktreeCwd);
|
|
@@ -18,27 +20,40 @@ export function autoCommit(agentId, taskPrompt, worktreeCwd, log) {
|
|
|
18
20
|
log(agentId, `git status failed: ${String(err.message || err).slice(0, 120)}`);
|
|
19
21
|
return 0;
|
|
20
22
|
}
|
|
21
|
-
if (
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
23
|
+
if (status.trim()) {
|
|
24
|
+
try {
|
|
25
|
+
gitExec("git add -A", worktreeCwd);
|
|
26
|
+
}
|
|
27
|
+
catch (err) {
|
|
28
|
+
log(agentId, `git add failed: ${String(err.message || err).slice(0, 120)}`);
|
|
29
|
+
}
|
|
30
|
+
try {
|
|
31
|
+
const msg = taskPrompt.slice(0, 72).replace(/'/g, "'\\''");
|
|
32
|
+
gitExec(`git commit -m 'swarm: ${msg}'`, worktreeCwd);
|
|
33
|
+
}
|
|
34
|
+
catch (err) {
|
|
35
|
+
const m = String(err.message || err);
|
|
36
|
+
if (!m.includes("nothing to commit"))
|
|
37
|
+
log(agentId, `git commit failed: ${m.slice(0, 120)}`);
|
|
38
|
+
}
|
|
30
39
|
}
|
|
40
|
+
// Step 2: measure against the worktree's origin commit — true regardless of
|
|
41
|
+
// who made the commits (agent or autoCommit). Pre-1.11.10 this counted
|
|
42
|
+
// `git status --porcelain` lines, which was zero whenever the agent committed
|
|
43
|
+
// its own work → filesChanged=0 → branch skipped by the merge gate → orphaned.
|
|
44
|
+
if (!baseRef)
|
|
45
|
+
return 0;
|
|
31
46
|
try {
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
47
|
+
const diff = gitExec(`git diff --name-only ${baseRef}..HEAD`, worktreeCwd);
|
|
48
|
+
const count = diff.trim().split("\n").filter(Boolean).length;
|
|
49
|
+
if (count > 0)
|
|
50
|
+
log(agentId, `${count} file(s) changed`);
|
|
51
|
+
return count;
|
|
35
52
|
}
|
|
36
53
|
catch (err) {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
log(agentId, `git commit failed: ${msg.slice(0, 120)}`);
|
|
54
|
+
log(agentId, `diff vs base failed: ${String(err.message || err).slice(0, 120)}`);
|
|
55
|
+
return 0;
|
|
40
56
|
}
|
|
41
|
-
return lines;
|
|
42
57
|
}
|
|
43
58
|
export function mergeAllBranches(agents, cwd, strategy, log) {
|
|
44
59
|
const mergeResults = [];
|
|
@@ -194,14 +209,23 @@ export function cleanStaleWorktrees(cwd, log) {
|
|
|
194
209
|
.split("\n")
|
|
195
210
|
.map(b => b.trim().replace(/^\* /, ""))
|
|
196
211
|
.filter(b => b.startsWith("swarm/task-") && !worktreeBranches.has(b));
|
|
212
|
+
// Use -d (safe delete) rather than -D: branches whose commits aren't in
|
|
213
|
+
// HEAD are orphaned work from a prior failed/unmerged run and must survive
|
|
214
|
+
// so the user can recover them. Only already-merged (ff) branches are
|
|
215
|
+
// actually deleted here.
|
|
216
|
+
let cleaned = 0;
|
|
197
217
|
for (const b of branches) {
|
|
198
218
|
try {
|
|
199
|
-
gitExec(`git branch -
|
|
219
|
+
gitExec(`git branch -d "${b}"`, cwd);
|
|
220
|
+
cleaned++;
|
|
200
221
|
}
|
|
201
222
|
catch { }
|
|
202
223
|
}
|
|
203
|
-
if (
|
|
204
|
-
log(-1, `Cleaned ${
|
|
224
|
+
if (cleaned > 0)
|
|
225
|
+
log(-1, `Cleaned ${cleaned} stale swarm branch(es)`);
|
|
226
|
+
const orphaned = branches.length - cleaned;
|
|
227
|
+
if (orphaned > 0)
|
|
228
|
+
log(-1, `Kept ${orphaned} orphaned swarm branch(es) with unmerged commits — resume to recover`);
|
|
205
229
|
}
|
|
206
230
|
catch { }
|
|
207
231
|
}
|
package/dist/state.js
CHANGED
|
@@ -441,7 +441,10 @@ export function recordBranches(agents, mergeResults, branches) {
|
|
|
441
441
|
}
|
|
442
442
|
}
|
|
443
443
|
export function autoMergeBranches(cwd, branches, onLog) {
|
|
444
|
-
|
|
444
|
+
// Do NOT gate on filesChanged — pre-1.11.10 runs can record filesChanged=0
|
|
445
|
+
// for branches that actually contain real commits (agent self-committed).
|
|
446
|
+
// Feed every unmerged branch to git; it will no-op harmlessly if truly empty.
|
|
447
|
+
const unmerged = branches.filter(b => b.status === "unmerged");
|
|
445
448
|
if (unmerged.length === 0)
|
|
446
449
|
return;
|
|
447
450
|
onLog(`Merging ${unmerged.length} unmerged branches...`);
|
package/dist/swarm.js
CHANGED
|
@@ -227,6 +227,11 @@ export class Swarm {
|
|
|
227
227
|
if (this.config.useWorktrees && this.worktreeBase && !task.noWorktree) {
|
|
228
228
|
const branch = `swarm/task-${id}`;
|
|
229
229
|
const dir = join(this.worktreeBase, `agent-${id}`);
|
|
230
|
+
let baseRef;
|
|
231
|
+
try {
|
|
232
|
+
baseRef = gitExec("git rev-parse HEAD", this.config.cwd).trim();
|
|
233
|
+
}
|
|
234
|
+
catch { }
|
|
230
235
|
let worktreeOk = false;
|
|
231
236
|
for (let wt = 0; wt < 2 && !worktreeOk; wt++) {
|
|
232
237
|
try {
|
|
@@ -254,6 +259,7 @@ export class Swarm {
|
|
|
254
259
|
if (worktreeOk) {
|
|
255
260
|
agentCwd = dir;
|
|
256
261
|
agent.branch = branch;
|
|
262
|
+
agent.baseRef = baseRef;
|
|
257
263
|
this.log(id, `Worktree: ${branch}`);
|
|
258
264
|
}
|
|
259
265
|
else {
|
|
@@ -397,7 +403,7 @@ export class Swarm {
|
|
|
397
403
|
}
|
|
398
404
|
}
|
|
399
405
|
if (this.config.useWorktrees && agent.branch) {
|
|
400
|
-
agent.filesChanged = autoCommit(agent.id, agent.task.prompt, agentCwd, (id, text) => this.log(id, text));
|
|
406
|
+
agent.filesChanged = autoCommit(agent.id, agent.task.prompt, agentCwd, agent.baseRef, (id, text) => this.log(id, text));
|
|
401
407
|
}
|
|
402
408
|
}
|
|
403
409
|
agentSummary(agent) {
|
package/dist/types.d.ts
CHANGED
|
@@ -64,6 +64,8 @@ export interface AgentState {
|
|
|
64
64
|
costUsd?: number;
|
|
65
65
|
/** Git branch name when using worktree isolation. */
|
|
66
66
|
branch?: string;
|
|
67
|
+
/** Commit the worktree branch was created from — the baseline for measuring filesChanged. */
|
|
68
|
+
baseRef?: string;
|
|
67
69
|
/** Number of files changed by the agent (from git diff). */
|
|
68
70
|
filesChanged?: number;
|
|
69
71
|
}
|
package/dist/ui.d.ts
CHANGED
|
@@ -57,7 +57,7 @@ export declare class RunDisplay {
|
|
|
57
57
|
private interval?;
|
|
58
58
|
private keyHandler?;
|
|
59
59
|
private inputMode;
|
|
60
|
-
private
|
|
60
|
+
private inputSegs;
|
|
61
61
|
private started;
|
|
62
62
|
private readonly isTTY;
|
|
63
63
|
private lastSeq;
|
|
@@ -94,6 +94,10 @@ export declare class RunDisplay {
|
|
|
94
94
|
private renderAskPanel;
|
|
95
95
|
private hasHotkeys;
|
|
96
96
|
private setupHotkeys;
|
|
97
|
+
/** Handle a pasted block. Returns true if the frame needs a redraw. */
|
|
98
|
+
private handlePaste;
|
|
99
|
+
/** Handle a typed (non-pasted) chunk. Returns true if the frame needs a redraw. */
|
|
100
|
+
private handleTyped;
|
|
97
101
|
private plainTick;
|
|
98
102
|
}
|
|
99
103
|
export {};
|
package/dist/ui.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import chalk from "chalk";
|
|
2
2
|
import { renderFrame, renderSteeringFrame } from "./render.js";
|
|
3
|
+
import { splitPaste, segmentsToString, renderSegments, appendCharToSegments, appendPasteToSegments, backspaceSegments, } from "./cli.js";
|
|
3
4
|
const MAX_STEERING_EVENTS = 60;
|
|
4
5
|
const MAX_INPUT_LEN = 600;
|
|
5
6
|
export class RunDisplay {
|
|
@@ -14,7 +15,7 @@ export class RunDisplay {
|
|
|
14
15
|
interval;
|
|
15
16
|
keyHandler;
|
|
16
17
|
inputMode = "none";
|
|
17
|
-
|
|
18
|
+
inputSegs = [];
|
|
18
19
|
started = false;
|
|
19
20
|
isTTY;
|
|
20
21
|
lastSeq = 0;
|
|
@@ -87,6 +88,10 @@ export class RunDisplay {
|
|
|
87
88
|
if (this.keyHandler) {
|
|
88
89
|
process.stdin.removeListener("data", this.keyHandler);
|
|
89
90
|
this.keyHandler = undefined;
|
|
91
|
+
try {
|
|
92
|
+
process.stdout.write("\x1B[?2004l");
|
|
93
|
+
}
|
|
94
|
+
catch { }
|
|
90
95
|
try {
|
|
91
96
|
process.stdin.setRawMode(false);
|
|
92
97
|
process.stdin.pause();
|
|
@@ -149,17 +154,18 @@ export class RunDisplay {
|
|
|
149
154
|
renderInputPrompt() {
|
|
150
155
|
if (this.inputMode === "none")
|
|
151
156
|
return "";
|
|
157
|
+
const rendered = renderSegments(this.inputSegs);
|
|
152
158
|
if (this.inputMode === "budget") {
|
|
153
|
-
return `\n ${chalk.cyan(">")} New budget (remaining sessions): ${
|
|
159
|
+
return `\n ${chalk.cyan(">")} New budget (remaining sessions): ${rendered}\u2588`;
|
|
154
160
|
}
|
|
155
161
|
if (this.inputMode === "threshold") {
|
|
156
|
-
return `\n ${chalk.cyan(">")} New usage cap (0-100%): ${
|
|
162
|
+
return `\n ${chalk.cyan(">")} New usage cap (0-100%): ${rendered}\u2588`;
|
|
157
163
|
}
|
|
158
164
|
if (this.inputMode === "steer") {
|
|
159
|
-
return `\n ${chalk.cyan(">")} ${chalk.bold("Steer next wave")} ${chalk.dim("(Enter to queue, Esc to cancel)")}\n ${
|
|
165
|
+
return `\n ${chalk.cyan(">")} ${chalk.bold("Steer next wave")} ${chalk.dim("(Enter to queue, Esc to cancel)")}\n ${rendered}\u2588`;
|
|
160
166
|
}
|
|
161
167
|
if (this.inputMode === "ask") {
|
|
162
|
-
return `\n ${chalk.cyan(">")} ${chalk.bold("Ask the planner")} ${chalk.dim("(Enter to send, Esc to cancel)")}\n ${
|
|
168
|
+
return `\n ${chalk.cyan(">")} ${chalk.bold("Ask the planner")} ${chalk.dim("(Enter to send, Esc to cancel)")}\n ${rendered}\u2588`;
|
|
163
169
|
}
|
|
164
170
|
return "";
|
|
165
171
|
}
|
|
@@ -196,12 +202,52 @@ export class RunDisplay {
|
|
|
196
202
|
catch {
|
|
197
203
|
return;
|
|
198
204
|
}
|
|
199
|
-
|
|
205
|
+
try {
|
|
206
|
+
process.stdout.write("\x1B[?2004h");
|
|
207
|
+
}
|
|
208
|
+
catch { }
|
|
200
209
|
this.keyHandler = (buf) => {
|
|
201
|
-
const
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
210
|
+
const chunk = buf.toString();
|
|
211
|
+
let dirty = false;
|
|
212
|
+
for (const seg of splitPaste(chunk)) {
|
|
213
|
+
if (seg.type === "paste") {
|
|
214
|
+
if (this.handlePaste(seg.text))
|
|
215
|
+
dirty = true;
|
|
216
|
+
}
|
|
217
|
+
else {
|
|
218
|
+
if (this.handleTyped(seg.text))
|
|
219
|
+
dirty = true;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
if (dirty)
|
|
223
|
+
this.flush();
|
|
224
|
+
};
|
|
225
|
+
process.stdin.on("data", this.keyHandler);
|
|
226
|
+
}
|
|
227
|
+
/** Handle a pasted block. Returns true if the frame needs a redraw. */
|
|
228
|
+
handlePaste(text) {
|
|
229
|
+
if (this.inputMode === "budget" || this.inputMode === "threshold") {
|
|
230
|
+
const clean = text.replace(/[^0-9.]/g, "");
|
|
231
|
+
if (clean)
|
|
232
|
+
appendCharToSegments(this.inputSegs, clean);
|
|
233
|
+
return !!clean;
|
|
234
|
+
}
|
|
235
|
+
if (this.inputMode === "steer" || this.inputMode === "ask") {
|
|
236
|
+
if (segmentsToString(this.inputSegs).length + text.length > MAX_INPUT_LEN)
|
|
237
|
+
return false;
|
|
238
|
+
appendPasteToSegments(this.inputSegs, text);
|
|
239
|
+
return true;
|
|
240
|
+
}
|
|
241
|
+
return false;
|
|
242
|
+
}
|
|
243
|
+
/** Handle a typed (non-pasted) chunk. Returns true if the frame needs a redraw. */
|
|
244
|
+
handleTyped(s) {
|
|
245
|
+
const lc = this.liveConfig;
|
|
246
|
+
if (this.inputMode === "budget" || this.inputMode === "threshold") {
|
|
247
|
+
let dirty = false;
|
|
248
|
+
for (const ch of s) {
|
|
249
|
+
if (ch === "\r" || ch === "\n") {
|
|
250
|
+
const val = parseFloat(segmentsToString(this.inputSegs));
|
|
205
251
|
if (this.inputMode === "budget" && !isNaN(val) && val > 0) {
|
|
206
252
|
lc.remaining = Math.round(val);
|
|
207
253
|
lc.dirty = true;
|
|
@@ -216,105 +262,113 @@ export class RunDisplay {
|
|
|
216
262
|
this.swarm?.log(-1, `Usage cap changed to ${val > 0 ? val + "%" : "unlimited"}`);
|
|
217
263
|
}
|
|
218
264
|
this.inputMode = "none";
|
|
219
|
-
this.
|
|
265
|
+
this.inputSegs = [];
|
|
266
|
+
return true;
|
|
220
267
|
}
|
|
221
|
-
|
|
268
|
+
if (ch === "\x1B" || ch === "\x03") {
|
|
222
269
|
this.inputMode = "none";
|
|
223
|
-
this.
|
|
270
|
+
this.inputSegs = [];
|
|
271
|
+
return true;
|
|
224
272
|
}
|
|
225
|
-
|
|
226
|
-
this.
|
|
273
|
+
if (ch === "\x7F") {
|
|
274
|
+
backspaceSegments(this.inputSegs);
|
|
275
|
+
dirty = true;
|
|
276
|
+
continue;
|
|
227
277
|
}
|
|
228
|
-
|
|
229
|
-
this.
|
|
278
|
+
if (/^[0-9.]$/.test(ch)) {
|
|
279
|
+
appendCharToSegments(this.inputSegs, ch);
|
|
280
|
+
dirty = true;
|
|
230
281
|
}
|
|
231
|
-
this.flush();
|
|
232
|
-
return;
|
|
233
282
|
}
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
}
|
|
250
|
-
if (ch === "\x03") {
|
|
251
|
-
this.inputMode = "none";
|
|
252
|
-
this.inputBuf = "";
|
|
253
|
-
this.flush();
|
|
254
|
-
return;
|
|
255
|
-
}
|
|
256
|
-
// Ignore raw ESC only — let ANSI sequences (arrows etc.) fall through
|
|
257
|
-
if (ch === "\x1B" && s.length === 1) {
|
|
258
|
-
this.inputMode = "none";
|
|
259
|
-
this.inputBuf = "";
|
|
260
|
-
this.flush();
|
|
261
|
-
return;
|
|
262
|
-
}
|
|
263
|
-
if (ch === "\x7F" || ch === "\b") {
|
|
264
|
-
this.inputBuf = this.inputBuf.slice(0, -1);
|
|
265
|
-
continue;
|
|
266
|
-
}
|
|
267
|
-
const code = ch.charCodeAt(0);
|
|
268
|
-
if (code >= 0x20 && code <= 0x7E && this.inputBuf.length < MAX_INPUT_LEN) {
|
|
269
|
-
this.inputBuf += ch;
|
|
283
|
+
return dirty;
|
|
284
|
+
}
|
|
285
|
+
if (this.inputMode === "steer" || this.inputMode === "ask") {
|
|
286
|
+
let dirty = false;
|
|
287
|
+
for (const ch of s) {
|
|
288
|
+
if (ch === "\r" || ch === "\n") {
|
|
289
|
+
const text = segmentsToString(this.inputSegs).trim();
|
|
290
|
+
const wasAsk = this.inputMode === "ask";
|
|
291
|
+
this.inputMode = "none";
|
|
292
|
+
this.inputSegs = [];
|
|
293
|
+
if (text) {
|
|
294
|
+
if (wasAsk)
|
|
295
|
+
this.onAsk?.(text);
|
|
296
|
+
else
|
|
297
|
+
this.onSteer?.(text);
|
|
270
298
|
}
|
|
299
|
+
return true;
|
|
271
300
|
}
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
301
|
+
if (ch === "\x03") {
|
|
302
|
+
this.inputMode = "none";
|
|
303
|
+
this.inputSegs = [];
|
|
304
|
+
return true;
|
|
305
|
+
}
|
|
306
|
+
// Ignore raw ESC only — let ANSI sequences (arrows etc.) fall through
|
|
307
|
+
if (ch === "\x1B" && s.length === 1) {
|
|
308
|
+
this.inputMode = "none";
|
|
309
|
+
this.inputSegs = [];
|
|
310
|
+
return true;
|
|
311
|
+
}
|
|
312
|
+
if (ch === "\x7F" || ch === "\b") {
|
|
313
|
+
backspaceSegments(this.inputSegs);
|
|
314
|
+
dirty = true;
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
const code = ch.charCodeAt(0);
|
|
318
|
+
if (code >= 0x20 && code <= 0x7E && segmentsToString(this.inputSegs).length < MAX_INPUT_LEN) {
|
|
319
|
+
appendCharToSegments(this.inputSegs, ch);
|
|
320
|
+
dirty = true;
|
|
288
321
|
}
|
|
289
322
|
}
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
323
|
+
return dirty;
|
|
324
|
+
}
|
|
325
|
+
// Hotkey mode
|
|
326
|
+
if (s === "\x1B" && this.askState && !this.askState.streaming) {
|
|
327
|
+
this.askState = undefined;
|
|
328
|
+
return false;
|
|
329
|
+
}
|
|
330
|
+
if (s === "b" || s === "B") {
|
|
331
|
+
this.inputMode = "budget";
|
|
332
|
+
this.inputSegs = [];
|
|
333
|
+
return true;
|
|
334
|
+
}
|
|
335
|
+
if (s === "t" || s === "T") {
|
|
336
|
+
if (this.swarm) {
|
|
337
|
+
this.inputMode = "threshold";
|
|
338
|
+
this.inputSegs = [];
|
|
339
|
+
return true;
|
|
296
340
|
}
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
341
|
+
return false;
|
|
342
|
+
}
|
|
343
|
+
if ((s === "f" || s === "F") && this.swarm && this.swarm.failed > 0 && this.swarm.active > 0) {
|
|
344
|
+
this.swarm.requeueFailed();
|
|
345
|
+
return false;
|
|
346
|
+
}
|
|
347
|
+
if ((s === "s" || s === "S") && this.onSteer) {
|
|
348
|
+
this.inputMode = "steer";
|
|
349
|
+
this.inputSegs = [];
|
|
350
|
+
return true;
|
|
351
|
+
}
|
|
352
|
+
if (s === "?" && this.onAsk && this.swarm && !this.askBusy) {
|
|
353
|
+
if (this.askState && !this.askState.streaming) {
|
|
354
|
+
this.askState = undefined;
|
|
355
|
+
return false;
|
|
305
356
|
}
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
357
|
+
this.inputMode = "ask";
|
|
358
|
+
this.inputSegs = [];
|
|
359
|
+
return true;
|
|
360
|
+
}
|
|
361
|
+
if (s === "q" || s === "Q" || s === "\x03") {
|
|
362
|
+
if (this.swarm) {
|
|
363
|
+
if (this.swarm.aborted)
|
|
313
364
|
process.exit(0);
|
|
314
|
-
|
|
365
|
+
this.swarm.abort();
|
|
315
366
|
}
|
|
316
|
-
|
|
317
|
-
|
|
367
|
+
else {
|
|
368
|
+
process.exit(0);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
return false;
|
|
318
372
|
}
|
|
319
373
|
plainTick() {
|
|
320
374
|
if (!this.swarm)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-overnight",
|
|
3
|
-
"version": "1.11.
|
|
3
|
+
"version": "1.11.11",
|
|
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": {
|