pikiclaw 0.3.67 → 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
- if (!promptNudged && !state.promptSubmittedAt && Date.now() - start > PROMPT_SUBMIT_NUDGE_MS) {
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,9 +2398,8 @@ 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, seen on 2.1.160).',
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
- 'If this keeps happening, pin the claude CLI to a known-good version: npm install -g @anthropic-ai/claude-code@2.1.159',
2404
2403
  ].join(' '),
2405
2404
  };
2406
2405
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pikiclaw",
3
- "version": "0.3.67",
3
+ "version": "0.3.69",
4
4
  "description": "Put the world's smartest AI agents in your pocket. Command local Claude & Gemini via IM. | 让最好用的 IM 变成你电脑上的顶级 Agent 控制台",
5
5
  "type": "module",
6
6
  "bin": {