claude-overnight 1.16.5 → 1.16.7

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.js CHANGED
@@ -144,6 +144,9 @@ export function backspaceSegments(segs) {
144
144
  return;
145
145
  }
146
146
  }
147
+ function stripAnsi(s) {
148
+ return s.replace(/\x1B\[[0-9;]*[a-zA-Z]/g, "");
149
+ }
147
150
  // ── Interactive primitives ──
148
151
  /**
149
152
  * Read a line from the user with bracketed-paste awareness.
@@ -158,12 +161,22 @@ export function ask(question) {
158
161
  }
159
162
  return new Promise((resolve) => {
160
163
  const segs = [];
161
- // DEC save/restore cursor + clear-to-end-of-screen so redraws don't pile
162
- // up when the input wraps past the terminal width onto additional rows.
164
+ const tail = question.split("\n").pop() ?? "";
165
+ const tailVisibleLen = stripAnsi(tail).length;
166
+ let prevWrapRows = 0;
167
+ // Only rewrite the input line (and any wrapped continuation rows). The
168
+ // question header above is never touched, so redraws can't stack copies
169
+ // even if the initial write scrolled the viewport.
163
170
  const redraw = () => {
164
- stdout.write("\x1B8\x1B[J" + question + renderSegments(segs));
171
+ const cols = stdout.columns || 80;
172
+ if (prevWrapRows > 0)
173
+ stdout.write(`\x1B[${prevWrapRows}A`);
174
+ stdout.write("\r\x1B[J");
175
+ const rendered = renderSegments(segs);
176
+ stdout.write(tail + rendered);
177
+ const visible = tailVisibleLen + stripAnsi(rendered).length;
178
+ prevWrapRows = visible > 0 ? Math.floor((visible - 1) / cols) : 0;
165
179
  };
166
- stdout.write("\x1B7");
167
180
  stdout.write(question);
168
181
  stdout.write("\x1B[?2004h");
169
182
  try {
@@ -188,7 +201,8 @@ export function ask(question) {
188
201
  redraw();
189
202
  continue;
190
203
  }
191
- for (const ch of seg.text) {
204
+ for (let ci = 0; ci < seg.text.length; ci++) {
205
+ const ch = seg.text[ci];
192
206
  if (ch === "\r" || ch === "\n") {
193
207
  stdout.write("\n");
194
208
  cleanup();
@@ -202,11 +216,20 @@ export function ask(question) {
202
216
  }
203
217
  if (ch === "\x7F" || ch === "\b") {
204
218
  backspaceSegments(segs);
219
+ redraw();
220
+ continue;
221
+ }
222
+ // Skip ESC and any bytes that are part of an ANSI escape sequence
223
+ // (arrow keys, function keys, etc. arrive as \x1B [ ... letter)
224
+ if (ch === "\x1B") {
205
225
  continue;
206
226
  }
207
227
  const code = ch.charCodeAt(0);
208
- if (ch !== "\x1B" && code >= 0x20)
209
- appendCharToSegments(segs, ch);
228
+ if (code < 0x20)
229
+ continue; // control chars
230
+ if (code >= 0x7F && code < 0xA0)
231
+ continue; // DEL + C1 controls
232
+ appendCharToSegments(segs, ch);
210
233
  }
211
234
  redraw();
212
235
  }
@@ -241,15 +264,10 @@ export async function select(label, items, defaultIdx = 0) {
241
264
  };
242
265
  const handler = (buf) => {
243
266
  const s = buf.toString();
244
- if (s === "\x1B[A") {
245
- idx = (idx - 1 + items.length) % items.length;
246
- draw();
247
- }
248
- else if (s === "\x1B[B") {
249
- idx = (idx + 1) % items.length;
250
- draw();
251
- }
252
- else if (s === "\r")
267
+ // Ignore ANSI escape sequences (arrow keys etc.)
268
+ if (s[0] === "\x1B")
269
+ return;
270
+ if (s === "\r")
253
271
  done(items[idx].value);
254
272
  else if (s === "\x03") {
255
273
  stdin.setRawMode(false);
@@ -277,6 +295,9 @@ export async function selectKey(label, options) {
277
295
  stdin.resume();
278
296
  const handler = (buf) => {
279
297
  const s = buf.toString().toLowerCase();
298
+ // Ignore ANSI escape sequences
299
+ if (s[0] === "\x1B")
300
+ return;
280
301
  if (s === "\x03") {
281
302
  stdin.setRawMode(false);
282
303
  process.exit(0);
@@ -288,7 +309,7 @@ export async function selectKey(label, options) {
288
309
  resolve(keys[0]);
289
310
  return;
290
311
  }
291
- if (keys.includes(s)) {
312
+ if (s.length === 1 && keys.includes(s)) {
292
313
  stdin.setRawMode(false);
293
314
  stdin.removeListener("data", handler);
294
315
  stdin.pause();
package/dist/index.js CHANGED
@@ -166,7 +166,7 @@ async function main() {
166
166
  }
167
167
  if (argv.includes("-h") || argv.includes("--help")) {
168
168
  console.log(`
169
- ${chalk.bold("🌙 claude-overnight")} ${chalk.dim("— fire off Claude agents, come back to shipped work")}
169
+ ${chalk.bold("🌙 claude-overnight")} ${chalk.dim("— background lane for your Claude Max plan")}
170
170
  ${chalk.dim("─".repeat(60))}
171
171
 
172
172
  ${chalk.cyan("Usage")}
package/dist/ui.js CHANGED
@@ -282,7 +282,13 @@ export class RunDisplay {
282
282
  this.inputSegs = [];
283
283
  return true;
284
284
  }
285
- if (ch === "\x1B" || ch === "\x03") {
285
+ if (ch === "\x03") {
286
+ this.inputMode = "none";
287
+ this.inputSegs = [];
288
+ return true;
289
+ }
290
+ // ESC cancels input mode
291
+ if (ch === "\x1B") {
286
292
  this.inputMode = "none";
287
293
  this.inputSegs = [];
288
294
  return true;
@@ -301,7 +307,8 @@ export class RunDisplay {
301
307
  }
302
308
  if (this.inputMode === "steer" || this.inputMode === "ask") {
303
309
  let dirty = false;
304
- for (const ch of s) {
310
+ for (let ci = 0; ci < s.length; ci++) {
311
+ const ch = s[ci];
305
312
  if (ch === "\r" || ch === "\n") {
306
313
  const text = segmentsToString(this.inputSegs).trim();
307
314
  const wasAsk = this.inputMode === "ask";
@@ -320,10 +327,18 @@ export class RunDisplay {
320
327
  this.inputSegs = [];
321
328
  return true;
322
329
  }
323
- // Ignore raw ESC onlylet ANSI sequences (arrows etc.) fall through
324
- if (ch === "\x1B" && s.length === 1) {
330
+ // ESC cancelsconsume this byte and any following ANSI sequence bytes
331
+ if (ch === "\x1B") {
325
332
  this.inputMode = "none";
326
333
  this.inputSegs = [];
334
+ // Skip any remaining ANSI sequence bytes (e.g. [A for arrow keys)
335
+ while (ci + 1 < s.length) {
336
+ const next = s[ci + 1];
337
+ const nc = next.charCodeAt(0);
338
+ ci++;
339
+ if ((nc >= 0x40 && nc <= 0x7E) || nc === 0x7F)
340
+ break; // final byte
341
+ }
327
342
  return true;
328
343
  }
329
344
  if (ch === "\x7F" || ch === "\b") {
@@ -332,6 +347,10 @@ export class RunDisplay {
332
347
  continue;
333
348
  }
334
349
  const code = ch.charCodeAt(0);
350
+ if (code < 0x20)
351
+ continue; // control chars
352
+ if (code >= 0x7F && code < 0xA0)
353
+ continue; // DEL + C1 controls
335
354
  if (code >= 0x20 && code <= 0x7E && segmentsToString(this.inputSegs).length < MAX_INPUT_LEN) {
336
355
  appendCharToSegments(this.inputSegs, ch);
337
356
  dirty = true;
@@ -339,17 +358,26 @@ export class RunDisplay {
339
358
  }
340
359
  return dirty;
341
360
  }
342
- // Hotkey mode
343
- if (s === "\x1B" && this.askState && !this.askState.streaming) {
361
+ // Hotkey mode — only accept single printable ASCII characters
362
+ // Skip ESC and ANSI sequences entirely
363
+ if (s.length > 1 && (s[0] === "\x1B" || s.charCodeAt(0) < 0x20))
364
+ return false;
365
+ if (s.length !== 1)
366
+ return false;
367
+ const key = s[0];
368
+ const code = key.charCodeAt(0);
369
+ if (code < 0x20 || code > 0x7E)
370
+ return false;
371
+ if (key === "\x1B" && this.askState && !this.askState.streaming) {
344
372
  this.askState = undefined;
345
373
  return false;
346
374
  }
347
- if (s === "b" || s === "B") {
375
+ if (key === "b" || key === "B") {
348
376
  this.inputMode = "budget";
349
377
  this.inputSegs = [];
350
378
  return true;
351
379
  }
352
- if (s === "t" || s === "T") {
380
+ if (key === "t" || key === "T") {
353
381
  if (this.swarm) {
354
382
  this.inputMode = "threshold";
355
383
  this.inputSegs = [];
@@ -357,7 +385,7 @@ export class RunDisplay {
357
385
  }
358
386
  return false;
359
387
  }
360
- if (s === "c" || s === "C") {
388
+ if (key === "c" || key === "C") {
361
389
  if (this.swarm) {
362
390
  this.inputMode = "concurrency";
363
391
  this.inputSegs = [];
@@ -365,7 +393,7 @@ export class RunDisplay {
365
393
  }
366
394
  return false;
367
395
  }
368
- if (s === "e" || s === "E") {
396
+ if (key === "e" || key === "E") {
369
397
  if (this.swarm) {
370
398
  this.inputMode = "extra";
371
399
  this.inputSegs = [];
@@ -373,7 +401,7 @@ export class RunDisplay {
373
401
  }
374
402
  return false;
375
403
  }
376
- if (s === "p" || s === "P") {
404
+ if (key === "p" || key === "P") {
377
405
  if (this.swarm) {
378
406
  const next = !this.swarm.paused;
379
407
  this.swarm.setPaused(next);
@@ -383,20 +411,20 @@ export class RunDisplay {
383
411
  }
384
412
  return false;
385
413
  }
386
- if ((s === "f" || s === "F") && this.swarm && this.swarm.failed > 0 && this.swarm.active > 0) {
414
+ if ((key === "f" || key === "F") && this.swarm && this.swarm.failed > 0 && this.swarm.active > 0) {
387
415
  this.swarm.requeueFailed();
388
416
  return false;
389
417
  }
390
- if ((s === "r" || s === "R") && this.swarm && this.swarm.rateLimitPaused > 0) {
418
+ if ((key === "r" || key === "R") && this.swarm && this.swarm.rateLimitPaused > 0) {
391
419
  this.swarm.retryRateLimitNow();
392
420
  return true;
393
421
  }
394
- if ((s === "s" || s === "S") && this.onSteer) {
422
+ if ((key === "s" || key === "S") && this.onSteer) {
395
423
  this.inputMode = "steer";
396
424
  this.inputSegs = [];
397
425
  return true;
398
426
  }
399
- if (s === "?" && this.onAsk && this.swarm && !this.askBusy) {
427
+ if (key === "?" && this.onAsk && this.swarm && !this.askBusy) {
400
428
  if (this.askState && !this.askState.streaming) {
401
429
  this.askState = undefined;
402
430
  return false;
@@ -405,7 +433,7 @@ export class RunDisplay {
405
433
  this.inputSegs = [];
406
434
  return true;
407
435
  }
408
- if (s === "q" || s === "Q" || s === "\x03") {
436
+ if (key === "q" || key === "Q" || key === "\x03") {
409
437
  if (this.swarm) {
410
438
  if (this.swarm.aborted)
411
439
  process.exit(0);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-overnight",
3
- "version": "1.16.5",
3
+ "version": "1.16.7",
4
4
  "description": "Background lane for your Claude Max plan. Parallel Claude Agent SDK sessions in git worktrees with a usage cap that reserves headroom for your interactive Claude Code. Crash-safe resume. Opus/Sonnet/Haiku + Qwen/OpenRouter.",
5
5
  "type": "module",
6
6
  "bin": {