pikiclaw 0.3.68 → 0.3.69
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.
|
@@ -323,6 +323,39 @@ export function detectClaudeTuiTerminalLimitNotice(msgOrText) {
|
|
|
323
323
|
return null;
|
|
324
324
|
return limitNoticeFromText(extractTextBlocks(msgOrText.content));
|
|
325
325
|
}
|
|
326
|
+
/**
|
|
327
|
+
* Detect Claude Code's startup "Bypass Permissions mode" confirmation dialog in
|
|
328
|
+
* a slice of (ANSI-stripped) PTY screen output. When pikiclaw spawns the TUI
|
|
329
|
+
* with `--permission-mode bypassPermissions` (the default) on a machine that
|
|
330
|
+
* has not yet accepted bypass mode, Claude paints a blocking prompt:
|
|
331
|
+
*
|
|
332
|
+
* WARNING: Claude Code running in Bypass Permissions mode
|
|
333
|
+
* ...
|
|
334
|
+
* ❯ 1. No, exit
|
|
335
|
+
* 2. Yes, I accept
|
|
336
|
+
*
|
|
337
|
+
* The default highlight sits on "No, exit", so the driver's blind prompt-submit
|
|
338
|
+
* Enter nudge would pick *exit* — the message never gets processed and the turn
|
|
339
|
+
* hangs on a pre-prompt. Seeding `bypassPermissionsModeAccepted` in config is
|
|
340
|
+
* not a reliable fix: it is version-fragile (observed no-op on 2.1.169) and
|
|
341
|
+
* gated by org policy (`isBypassPermissionsModeAvailable`). So we detect the
|
|
342
|
+
* dialog on the wire and auto-select "Yes, I accept". Require all three
|
|
343
|
+
* distinctive fragments so ordinary text mentioning "bypass" can't trigger it.
|
|
344
|
+
*/
|
|
345
|
+
export function detectClaudeBypassPrompt(screen) {
|
|
346
|
+
if (typeof screen !== 'string' || !screen)
|
|
347
|
+
return false;
|
|
348
|
+
// Claude's TUI lays words out with cursor-move escapes (`\x1b[<col>G`) rather
|
|
349
|
+
// than literal spaces, so once ANSI is stripped the on-screen text runs
|
|
350
|
+
// together — the real dialog reads "BypassPermissionsmode" / "Yes,Iaccept" /
|
|
351
|
+
// "No,exit", not the spaced form. Collapse all whitespace before matching so
|
|
352
|
+
// the detector fires on the live PTY screen *and* on space-preserving
|
|
353
|
+
// renderings. (Verified against claude 2.1.168's actual bypass screen.)
|
|
354
|
+
const t = stripAnsiEscapes(screen).replace(/\s+/g, '').toLowerCase();
|
|
355
|
+
return t.includes('bypasspermissionsmode')
|
|
356
|
+
&& t.includes('yes,iaccept')
|
|
357
|
+
&& t.includes('no,exit');
|
|
358
|
+
}
|
|
326
359
|
/**
|
|
327
360
|
* Extract text / thinking blocks from an assistant JSONL event and route them:
|
|
328
361
|
* text → the chunked stream buffer (slow drain), thinking → `s.thinking`
|
|
@@ -1055,6 +1088,30 @@ export async function doClaudeTuiStream(opts) {
|
|
|
1055
1088
|
const dbg = process.env.PIKICLAW_CLAUDE_TUI_DEBUG === '1';
|
|
1056
1089
|
/** Wall-clock of the last raw PTY byte — stall watchdog fast-path signal. */
|
|
1057
1090
|
let lastPtyDataAt = Date.now();
|
|
1091
|
+
// Startup-dialog auto-answer. Claude's TUI can paint a blocking "Bypass
|
|
1092
|
+
// Permissions mode" confirmation before it accepts our positional prompt
|
|
1093
|
+
// (default highlight = "No, exit"). We keep a bounded ANSI-stripped tail of
|
|
1094
|
+
// the screen, detect that dialog (see detectClaudeBypassPrompt), and select
|
|
1095
|
+
// "Yes, I accept" so the turn never stalls on a pre-prompt.
|
|
1096
|
+
const SCREEN_TAIL_MAX = 8192;
|
|
1097
|
+
const BYPASS_ACCEPT_MAX_ATTEMPTS = 3;
|
|
1098
|
+
// Settle delay after the dialog first paints before we send any key. Claude's
|
|
1099
|
+
// Ink select drops input aimed at it during the first frames — sending the
|
|
1100
|
+
// digit too early is a no-op. ~500ms is comfortably past readiness in repro.
|
|
1101
|
+
const BYPASS_SETTLE_MS = 500;
|
|
1102
|
+
// Gap between the selection key and the confirm Enter. Claude's Ink select
|
|
1103
|
+
// swallows a combined "2\r" (only the digit lands; the Enter is dropped before
|
|
1104
|
+
// the highlight repaints), so the two keystrokes must be split in time —
|
|
1105
|
+
// 600ms is what reproduces reliably against the live 2.1.168 dialog.
|
|
1106
|
+
const BYPASS_CONFIRM_DELAY_MS = 600;
|
|
1107
|
+
// How long after the last bypass-dialog repaint we still treat it as on
|
|
1108
|
+
// screen — suppresses the blind prompt-submit Enter nudge across the whole
|
|
1109
|
+
// select→confirm sequence so a stray CR can't land on "No, exit".
|
|
1110
|
+
const BYPASS_DIALOG_ACTIVE_WINDOW_MS = 2000;
|
|
1111
|
+
let screenTail = '';
|
|
1112
|
+
let bypassPromptLastSeenAt = 0;
|
|
1113
|
+
let bypassAcceptAttempts = 0;
|
|
1114
|
+
let bypassPhase = 'idle';
|
|
1058
1115
|
proc.onData((data) => {
|
|
1059
1116
|
// We deliberately do not parse the TUI screen output. The JSONL is the
|
|
1060
1117
|
// canonical source of structured events. Stash bytes only when debugging.
|
|
@@ -1068,6 +1125,55 @@ export async function doClaudeTuiStream(opts) {
|
|
|
1068
1125
|
}
|
|
1069
1126
|
catch { }
|
|
1070
1127
|
}
|
|
1128
|
+
// Auto-answer the bypass-permissions confirmation. Detect it the moment it
|
|
1129
|
+
// paints (off the raw PTY, not the 200ms poll tick) and arm a short timed
|
|
1130
|
+
// keystroke sequence. Keep a bounded stripped tail across chunks so a dialog
|
|
1131
|
+
// split across reads still matches.
|
|
1132
|
+
screenTail = (screenTail + stripAnsiEscapes(data)).slice(-SCREEN_TAIL_MAX);
|
|
1133
|
+
if (detectClaudeBypassPrompt(screenTail)) {
|
|
1134
|
+
bypassPromptLastSeenAt = Date.now();
|
|
1135
|
+
if (bypassPhase === 'idle' && bypassAcceptAttempts < BYPASS_ACCEPT_MAX_ATTEMPTS) {
|
|
1136
|
+
bypassAcceptAttempts++;
|
|
1137
|
+
bypassPhase = 'armed';
|
|
1138
|
+
// Three timed steps — verified 3/3 against the live 2.1.168 dialog:
|
|
1139
|
+
// settle (dialog ignores input on its first frames)
|
|
1140
|
+
// → "2" (jumps to the second option "Yes, I accept"; idempotent —
|
|
1141
|
+
// re-sending can't overshoot a 2-option menu onto "No, exit")
|
|
1142
|
+
// → Enter (confirms; must arrive *after* the highlight repaints — a
|
|
1143
|
+
// combined "2\r" gets swallowed, only the digit lands).
|
|
1144
|
+
agentLog(`[claude-tui] bypass-permissions prompt — auto-accepting "Yes, I accept" (attempt ${bypassAcceptAttempts}/${BYPASS_ACCEPT_MAX_ATTEMPTS})`);
|
|
1145
|
+
setTimeout(() => {
|
|
1146
|
+
if (processExited)
|
|
1147
|
+
return;
|
|
1148
|
+
try {
|
|
1149
|
+
proc.write('2');
|
|
1150
|
+
}
|
|
1151
|
+
catch { }
|
|
1152
|
+
setTimeout(() => {
|
|
1153
|
+
if (processExited)
|
|
1154
|
+
return;
|
|
1155
|
+
try {
|
|
1156
|
+
proc.write('\r');
|
|
1157
|
+
}
|
|
1158
|
+
catch { }
|
|
1159
|
+
bypassPhase = 'confirmed';
|
|
1160
|
+
agentLog('[claude-tui] bypass-permissions — confirm Enter sent');
|
|
1161
|
+
// Drop the buffered dialog frame: the post-accept REPL output can be
|
|
1162
|
+
// tiny (e.g. a "Not logged in" line), so the old dialog text would
|
|
1163
|
+
// otherwise linger in the 8192-char tail and make the re-arm below
|
|
1164
|
+
// re-fire on a stale screen — typing "2"/Enter into the live prompt.
|
|
1165
|
+
// Clearing means the re-arm only sees output that arrives *after*
|
|
1166
|
+
// the confirm, so it re-fires only on a genuine repaint of the
|
|
1167
|
+
// dialog (accept didn't take), never on stale bytes.
|
|
1168
|
+
screenTail = '';
|
|
1169
|
+
setTimeout(() => {
|
|
1170
|
+
if (!processExited && detectClaudeBypassPrompt(screenTail))
|
|
1171
|
+
bypassPhase = 'idle';
|
|
1172
|
+
}, 1200);
|
|
1173
|
+
}, BYPASS_CONFIRM_DELAY_MS);
|
|
1174
|
+
}, BYPASS_SETTLE_MS);
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1071
1177
|
// Capture stderr-ish bytes (TUI startup errors, "claude: command not
|
|
1072
1178
|
// found"-style messages) for the final error payload when the run aborts
|
|
1073
1179
|
// before any JSONL is written. Strip ANSI on the way in — otherwise the
|
|
@@ -1314,8 +1420,15 @@ export async function doClaudeTuiStream(opts) {
|
|
|
1314
1420
|
else if (state.transcriptPath && state.transcriptPath !== activeJsonlPath) {
|
|
1315
1421
|
activeJsonlPath = state.transcriptPath;
|
|
1316
1422
|
}
|
|
1317
|
-
// Submit nudge — only if UserPromptSubmit hook hasn't fired yet.
|
|
1318
|
-
|
|
1423
|
+
// Submit nudge — only if UserPromptSubmit hook hasn't fired yet. Suppress
|
|
1424
|
+
// it while the bypass-permissions dialog is (or was just) on screen: a blind
|
|
1425
|
+
// CR there lands on the default "No, exit" and kills the session. The dialog
|
|
1426
|
+
// auto-answer in onData drives that screen instead; once it clears the
|
|
1427
|
+
// prompt submits on its own (or this nudge fires on a later tick).
|
|
1428
|
+
const bypassDialogActive = bypassPromptLastSeenAt > 0
|
|
1429
|
+
&& Date.now() - bypassPromptLastSeenAt < BYPASS_DIALOG_ACTIVE_WINDOW_MS;
|
|
1430
|
+
if (!promptNudged && !state.promptSubmittedAt && !bypassDialogActive
|
|
1431
|
+
&& Date.now() - start > PROMPT_SUBMIT_NUDGE_MS) {
|
|
1319
1432
|
promptNudged = true;
|
|
1320
1433
|
try {
|
|
1321
1434
|
proc.write('\r');
|
|
@@ -2398,7 +2398,7 @@ async function doClaudeWithRetry(opts) {
|
|
|
2398
2398
|
ok: false,
|
|
2399
2399
|
incomplete: true,
|
|
2400
2400
|
message: [
|
|
2401
|
-
'The agent process stalled mid-turn and could not be auto-recovered (known claude CLI freeze
|
|
2401
|
+
'The agent process stalled mid-turn and could not be auto-recovered (a known claude CLI mid-turn freeze).',
|
|
2402
2402
|
'Your session is intact — re-send your message (or say "continue") to pick up where it stopped.',
|
|
2403
2403
|
].join(' '),
|
|
2404
2404
|
};
|
package/package.json
CHANGED