commandmate 0.2.9 → 0.2.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.
Files changed (66) hide show
  1. package/.env.example +2 -1
  2. package/.next/BUILD_ID +1 -1
  3. package/.next/app-build-manifest.json +12 -12
  4. package/.next/app-path-routes-manifest.json +1 -1
  5. package/.next/build-manifest.json +2 -2
  6. package/.next/cache/.tsbuildinfo +1 -1
  7. package/.next/cache/config.json +3 -3
  8. package/.next/cache/webpack/client-production/0.pack +0 -0
  9. package/.next/cache/webpack/client-production/1.pack +0 -0
  10. package/.next/cache/webpack/client-production/2.pack +0 -0
  11. package/.next/cache/webpack/client-production/index.pack +0 -0
  12. package/.next/cache/webpack/client-production/index.pack.old +0 -0
  13. package/.next/cache/webpack/edge-server-production/index.pack +0 -0
  14. package/.next/cache/webpack/server-production/0.pack +0 -0
  15. package/.next/cache/webpack/server-production/index.pack +0 -0
  16. package/.next/next-minimal-server.js.nft.json +1 -1
  17. package/.next/next-server.js.nft.json +1 -1
  18. package/.next/prerender-manifest.json +1 -1
  19. package/.next/required-server-files.json +1 -1
  20. package/.next/routes-manifest.json +1 -1
  21. package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  22. package/.next/server/app/api/app/update-check/route.js +1 -1
  23. package/.next/server/app/api/repositories/clone/[jobId]/route.js +1 -1
  24. package/.next/server/app/api/repositories/clone/route.js +1 -1
  25. package/.next/server/app/api/repositories/route.js +10 -10
  26. package/.next/server/app/api/repositories/route.js.nft.json +1 -1
  27. package/.next/server/app/api/worktrees/[id]/auto-yes/route.js +1 -1
  28. package/.next/server/app/api/worktrees/[id]/auto-yes/route.js.nft.json +1 -1
  29. package/.next/server/app/api/worktrees/[id]/current-output/route.js +1 -1
  30. package/.next/server/app/api/worktrees/[id]/current-output/route.js.nft.json +1 -1
  31. package/.next/server/app/api/worktrees/[id]/files/[...path]/route.js +1 -1
  32. package/.next/server/app/api/worktrees/[id]/prompt-response/route.js +1 -1
  33. package/.next/server/app/api/worktrees/[id]/search/route.js +1 -1
  34. package/.next/server/app/api/worktrees/[id]/upload/[...path]/route.js +1 -1
  35. package/.next/server/app/page_client-reference-manifest.js +1 -1
  36. package/.next/server/app/worktrees/[id]/files/[...path]/page.js +1 -1
  37. package/.next/server/app/worktrees/[id]/files/[...path]/page_client-reference-manifest.js +1 -1
  38. package/.next/server/app/worktrees/[id]/page.js +3 -3
  39. package/.next/server/app/worktrees/[id]/page_client-reference-manifest.js +1 -1
  40. package/.next/server/app/worktrees/[id]/terminal/page_client-reference-manifest.js +1 -1
  41. package/.next/server/app-paths-manifest.json +5 -5
  42. package/.next/server/chunks/5488.js +4 -4
  43. package/.next/server/chunks/7536.js +1 -1
  44. package/.next/server/chunks/8693.js +1 -0
  45. package/.next/server/chunks/9238.js +14 -14
  46. package/.next/server/chunks/9367.js +2 -2
  47. package/.next/server/pages/500.html +1 -1
  48. package/.next/server/server-reference-manifest.json +1 -1
  49. package/.next/static/chunks/5970-2e18108d0cabd8af.js +1 -0
  50. package/.next/static/chunks/app/worktrees/[id]/files/[...path]/page-4a3c0861367e0391.js +1 -0
  51. package/.next/static/chunks/app/worktrees/[id]/page-dc0dde49ed95076f.js +1 -0
  52. package/.next/static/css/7c6675f6f65b4990.css +3 -0
  53. package/.next/trace +5 -5
  54. package/README.md +232 -46
  55. package/dist/server/src/lib/auto-yes-manager.js +124 -84
  56. package/dist/server/src/lib/claude-session.js +60 -21
  57. package/dist/server/src/lib/prompt-answer-sender.js +89 -0
  58. package/dist/server/src/lib/prompt-detector.js +9 -0
  59. package/dist/server/src/lib/prompt-key.js +30 -0
  60. package/package.json +12 -1
  61. package/.next/static/chunks/5970-9e999084275f2995.js +0 -1
  62. package/.next/static/chunks/app/worktrees/[id]/files/[...path]/page-ce9ac3658f2b7d91.js +0 -1
  63. package/.next/static/chunks/app/worktrees/[id]/page-5abc1e9b59430db1.js +0 -1
  64. package/.next/static/css/a69d9c70fce558b4.css +0 -3
  65. /package/.next/static/{5sR3jeFe8ymnzcEan6Rdt → ZTtC8-q8xrYUSilnXmMHl}/_buildManifest.js +0 -0
  66. /package/.next/static/{5sR3jeFe8ymnzcEan6Rdt → ZTtC8-q8xrYUSilnXmMHl}/_ssgManifest.js +0 -0
@@ -9,7 +9,7 @@
9
9
  * auto-yes responses when browser tabs are in background.
10
10
  */
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
- exports.THINKING_CHECK_LINE_COUNT = exports.MAX_CONCURRENT_POLLERS = exports.MAX_CONSECUTIVE_ERRORS = exports.MAX_BACKOFF_MS = exports.POLLING_INTERVAL_MS = void 0;
12
+ exports.THINKING_CHECK_LINE_COUNT = exports.MAX_CONCURRENT_POLLERS = exports.MAX_CONSECUTIVE_ERRORS = exports.MAX_BACKOFF_MS = exports.COOLDOWN_INTERVAL_MS = exports.POLLING_INTERVAL_MS = void 0;
13
13
  exports.isValidWorktreeId = isValidWorktreeId;
14
14
  exports.calculateBackoffInterval = calculateBackoffInterval;
15
15
  exports.isAutoYesExpired = isAutoYesExpired;
@@ -25,15 +25,18 @@ exports.stopAllAutoYesPolling = stopAllAutoYesPolling;
25
25
  const cli_session_1 = require("./cli-session");
26
26
  const prompt_detector_1 = require("./prompt-detector");
27
27
  const auto_yes_resolver_1 = require("./auto-yes-resolver");
28
- const tmux_1 = require("./tmux");
28
+ const prompt_answer_sender_1 = require("./prompt-answer-sender");
29
29
  const manager_1 = require("./cli-tools/manager");
30
30
  const cli_patterns_1 = require("./cli-patterns");
31
31
  const auto_yes_config_1 = require("../config/auto-yes-config");
32
+ const prompt_key_1 = require("./prompt-key");
32
33
  // =============================================================================
33
34
  // Constants (Issue #138)
34
35
  // =============================================================================
35
36
  /** Polling interval in milliseconds */
36
37
  exports.POLLING_INTERVAL_MS = 2000;
38
+ /** Cooldown interval after successful response (milliseconds) (Issue #306) */
39
+ exports.COOLDOWN_INTERVAL_MS = 5000;
37
40
  /** Maximum backoff interval in milliseconds (60 seconds) */
38
41
  exports.MAX_BACKOFF_MS = 60000;
39
42
  /** Number of consecutive errors before applying backoff */
@@ -52,6 +55,16 @@ exports.MAX_CONCURRENT_POLLERS = 50;
52
55
  exports.THINKING_CHECK_LINE_COUNT = 50;
53
56
  /** Worktree ID validation pattern (security: prevent command injection) */
54
57
  const WORKTREE_ID_PATTERN = /^[a-zA-Z0-9_-]+$/;
58
+ /**
59
+ * Extract error message from unknown error type.
60
+ * Provides consistent error message extraction across the module (DRY).
61
+ *
62
+ * @param error - Unknown error object
63
+ * @returns Error message string, or 'Unknown error' for non-Error values
64
+ */
65
+ function getErrorMessage(error) {
66
+ return error instanceof Error ? error.message : 'Unknown error';
67
+ }
55
68
  /** In-memory storage for auto-yes states (globalThis for hot reload persistence) */
56
69
  const autoYesStates = globalThis.__autoYesStates ??
57
70
  (globalThis.__autoYesStates = new Map());
@@ -62,8 +75,11 @@ const autoYesPollerStates = globalThis.__autoYesPollerStates ??
62
75
  // Utility Functions
63
76
  // =============================================================================
64
77
  /**
65
- * Validate worktree ID format (security measure)
78
+ * Validate worktree ID format (security measure).
66
79
  * Only allows alphanumeric characters, hyphens, and underscores.
80
+ *
81
+ * @param worktreeId - Worktree ID to validate
82
+ * @returns true if the ID matches the allowed pattern
67
83
  */
68
84
  function isValidWorktreeId(worktreeId) {
69
85
  if (!worktreeId || worktreeId.length === 0)
@@ -71,7 +87,12 @@ function isValidWorktreeId(worktreeId) {
71
87
  return WORKTREE_ID_PATTERN.test(worktreeId);
72
88
  }
73
89
  /**
74
- * Calculate backoff interval based on consecutive errors
90
+ * Calculate backoff interval based on consecutive errors.
91
+ * Returns the normal polling interval when errors are below the threshold,
92
+ * and applies exponential backoff (capped at MAX_BACKOFF_MS) above it.
93
+ *
94
+ * @param consecutiveErrors - Number of consecutive errors encountered
95
+ * @returns Polling interval in milliseconds
75
96
  */
76
97
  function calculateBackoffInterval(consecutiveErrors) {
77
98
  if (consecutiveErrors < exports.MAX_CONSECUTIVE_ERRORS) {
@@ -89,14 +110,21 @@ function calculateBackoffInterval(consecutiveErrors) {
89
110
  // Auto-Yes State Management (Existing)
90
111
  // =============================================================================
91
112
  /**
92
- * Check if an auto-yes state has expired
113
+ * Check if an auto-yes state has expired.
114
+ * Compares current time against the expiresAt timestamp.
115
+ *
116
+ * @param state - Auto-yes state to check
117
+ * @returns true if the current time is past the expiration time
93
118
  */
94
119
  function isAutoYesExpired(state) {
95
120
  return Date.now() > state.expiresAt;
96
121
  }
97
122
  /**
98
- * Get the auto-yes state for a worktree
99
- * Returns null if no state exists or if expired (auto-disables on expiry)
123
+ * Get the auto-yes state for a worktree.
124
+ * Returns null if no state exists. If expired, auto-disables and returns the disabled state.
125
+ *
126
+ * @param worktreeId - Worktree identifier
127
+ * @returns Current auto-yes state, or null if no state exists
100
128
  */
101
129
  function getAutoYesState(worktreeId) {
102
130
  const state = autoYesStates.get(worktreeId);
@@ -142,7 +170,8 @@ function setAutoYesEnabled(worktreeId, enabled, duration) {
142
170
  }
143
171
  }
144
172
  /**
145
- * Clear all auto-yes states (for testing)
173
+ * Clear all auto-yes states.
174
+ * @internal Exported for testing purposes only.
146
175
  */
147
176
  function clearAllAutoYesStates() {
148
177
  autoYesStates.clear();
@@ -151,28 +180,38 @@ function clearAllAutoYesStates() {
151
180
  // Server-side Polling (Issue #138)
152
181
  // =============================================================================
153
182
  /**
154
- * Get the number of active pollers
183
+ * Get the number of active pollers.
184
+ *
185
+ * @returns Count of currently active polling instances
155
186
  */
156
187
  function getActivePollerCount() {
157
188
  return autoYesPollerStates.size;
158
189
  }
159
190
  /**
160
- * Clear all poller states (for testing)
191
+ * Clear all poller states.
192
+ * Stops all active pollers before clearing state.
193
+ * @internal Exported for testing purposes only.
161
194
  */
162
195
  function clearAllPollerStates() {
163
196
  stopAllAutoYesPolling();
164
197
  autoYesPollerStates.clear();
165
198
  }
166
199
  /**
167
- * Get the last server response timestamp for a worktree
168
- * Used by clients to prevent duplicate responses
200
+ * Get the last server response timestamp for a worktree.
201
+ * Used by clients to prevent duplicate responses.
202
+ *
203
+ * @param worktreeId - Worktree identifier
204
+ * @returns Timestamp (Date.now()) of the last server response, or null if none
169
205
  */
170
206
  function getLastServerResponseTimestamp(worktreeId) {
171
207
  const pollerState = autoYesPollerStates.get(worktreeId);
172
208
  return pollerState?.lastServerResponseTimestamp ?? null;
173
209
  }
174
210
  /**
175
- * Update the last server response timestamp
211
+ * Update the last server response timestamp.
212
+ *
213
+ * @param worktreeId - Worktree identifier
214
+ * @param timestamp - Timestamp value (Date.now())
176
215
  */
177
216
  function updateLastServerResponseTimestamp(worktreeId, timestamp) {
178
217
  const pollerState = autoYesPollerStates.get(worktreeId);
@@ -181,7 +220,9 @@ function updateLastServerResponseTimestamp(worktreeId, timestamp) {
181
220
  }
182
221
  }
183
222
  /**
184
- * Reset error count for a poller
223
+ * Reset error count for a poller and restore the default polling interval.
224
+ *
225
+ * @param worktreeId - Worktree identifier
185
226
  */
186
227
  function resetErrorCount(worktreeId) {
187
228
  const pollerState = autoYesPollerStates.get(worktreeId);
@@ -191,7 +232,9 @@ function resetErrorCount(worktreeId) {
191
232
  }
192
233
  }
193
234
  /**
194
- * Increment error count and apply backoff if needed
235
+ * Increment error count and apply backoff if the threshold is exceeded.
236
+ *
237
+ * @param worktreeId - Worktree identifier
195
238
  */
196
239
  function incrementErrorCount(worktreeId) {
197
240
  const pollerState = autoYesPollerStates.get(worktreeId);
@@ -201,7 +244,26 @@ function incrementErrorCount(worktreeId) {
201
244
  }
202
245
  }
203
246
  /**
204
- * Internal polling function (setTimeout recursive)
247
+ * Check if the given prompt has already been answered.
248
+ * Extracted from pollAutoYes() to reduce responsibility concentration (F005/SRP).
249
+ *
250
+ * @param pollerState - Current poller state containing the last answered prompt key
251
+ * @param promptKey - Composite key of the current prompt (generated by generatePromptKey)
252
+ * @returns true if the prompt key matches the last answered prompt key
253
+ */
254
+ function isDuplicatePrompt(pollerState, promptKey) {
255
+ return pollerState.lastAnsweredPromptKey === promptKey;
256
+ }
257
+ /**
258
+ * Internal polling function that recursively schedules itself via setTimeout.
259
+ * Captures tmux output, detects prompts, and sends auto-responses when appropriate.
260
+ *
261
+ * Includes duplicate prevention (Issue #306): skips prompts that have already
262
+ * been answered (tracked via lastAnsweredPromptKey) and applies a cooldown
263
+ * interval (COOLDOWN_INTERVAL_MS) after successful responses.
264
+ *
265
+ * @param worktreeId - Worktree identifier
266
+ * @param cliToolId - CLI tool type being polled
205
267
  */
206
268
  async function pollAutoYes(worktreeId, cliToolId) {
207
269
  // Check if poller was stopped
@@ -224,7 +286,7 @@ async function pollAutoYes(worktreeId, cliToolId) {
224
286
  // while Claude is actively processing (thinking/planning).
225
287
  //
226
288
  // Issue #191: Apply windowing to detectThinking() to prevent stale thinking
227
- // summary lines (e.g., "· Simmering") from blocking prompt detection.
289
+ // summary lines (e.g., "· Simmering...") from blocking prompt detection.
228
290
  // Window size matches detectPrompt()'s multiple_choice scan range (50 lines).
229
291
  //
230
292
  // Safety: Claude CLI does not emit prompts during thinking, so narrowing
@@ -246,7 +308,14 @@ async function pollAutoYes(worktreeId, cliToolId) {
246
308
  const promptOptions = (0, cli_patterns_1.buildDetectPromptOptions)(cliToolId);
247
309
  const promptDetection = (0, prompt_detector_1.detectPrompt)(cleanOutput, promptOptions);
248
310
  if (!promptDetection.isPrompt || !promptDetection.promptData) {
249
- // No prompt detected, schedule next poll
311
+ // No prompt detected - reset lastAnsweredPromptKey (Issue #306)
312
+ pollerState.lastAnsweredPromptKey = null;
313
+ scheduleNextPoll(worktreeId, cliToolId);
314
+ return;
315
+ }
316
+ // Issue #306: Check for duplicate prompt before responding
317
+ const promptKey = (0, prompt_key_1.generatePromptKey)(promptDetection.promptData);
318
+ if (isDuplicatePrompt(pollerState, promptKey)) {
250
319
  scheduleNextPoll(worktreeId, cliToolId);
251
320
  return;
252
321
  }
@@ -261,94 +330,60 @@ async function pollAutoYes(worktreeId, cliToolId) {
261
330
  const manager = manager_1.CLIToolManager.getInstance();
262
331
  const cliTool = manager.getTool(cliToolId);
263
332
  const sessionName = cliTool.getSessionName(worktreeId);
264
- // Issue #193: Claude Code AskUserQuestion uses cursor-based navigation
265
- // (Arrow/Space/Enter), not number input. Detect multi-choice and send
266
- // appropriate key sequence instead of typing the number.
267
- const isClaudeMultiChoice = cliToolId === 'claude'
268
- && promptDetection.promptData?.type === 'multiple_choice'
269
- && /^\d+$/.test(answer);
270
- if (isClaudeMultiChoice && promptDetection.promptData?.type === 'multiple_choice') {
271
- const targetNum = parseInt(answer, 10);
272
- const mcOptions = promptDetection.promptData.options;
273
- const defaultOption = mcOptions.find(o => o.isDefault);
274
- const defaultNum = defaultOption?.number ?? 1;
275
- const offset = targetNum - defaultNum;
276
- // Detect multi-select (checkbox) prompts by checking for [ ] in option labels.
277
- const isMultiSelect = mcOptions.some(o => /^\[[ x]\] /.test(o.label));
278
- if (isMultiSelect) {
279
- // Multi-select: toggle checkbox, then navigate to "Next" and submit
280
- const checkboxCount = mcOptions.filter(o => /^\[[ x]\] /.test(o.label)).length;
281
- const keys = [];
282
- // 1. Navigate to target option
283
- if (offset > 0) {
284
- for (let i = 0; i < offset; i++)
285
- keys.push('Down');
286
- }
287
- else if (offset < 0) {
288
- for (let i = 0; i < Math.abs(offset); i++)
289
- keys.push('Up');
290
- }
291
- // 2. Space to toggle checkbox
292
- keys.push('Space');
293
- // 3. Navigate to "Next" button (positioned right after all checkbox options)
294
- const downToNext = checkboxCount - targetNum + 1;
295
- for (let i = 0; i < downToNext; i++)
296
- keys.push('Down');
297
- // 4. Enter to submit
298
- keys.push('Enter');
299
- await (0, tmux_1.sendSpecialKeys)(sessionName, keys);
300
- }
301
- else {
302
- // Single-select: navigate and Enter to select
303
- const keys = [];
304
- if (offset > 0) {
305
- for (let i = 0; i < offset; i++)
306
- keys.push('Down');
307
- }
308
- else if (offset < 0) {
309
- for (let i = 0; i < Math.abs(offset); i++)
310
- keys.push('Up');
311
- }
312
- keys.push('Enter');
313
- await (0, tmux_1.sendSpecialKeys)(sessionName, keys);
314
- }
315
- }
316
- else {
317
- // Standard CLI prompt: send text + Enter (y/n, Approve?, etc.)
318
- await (0, tmux_1.sendKeys)(sessionName, answer, false);
319
- await new Promise(resolve => setTimeout(resolve, 100));
320
- await (0, tmux_1.sendKeys)(sessionName, '', true);
321
- }
333
+ // Issue #287 Bug2: Uses shared sendPromptAnswer() to unify logic
334
+ // with route.ts, including cursor-key navigation for Claude Code
335
+ // multiple-choice prompts and fallback handling.
336
+ await (0, prompt_answer_sender_1.sendPromptAnswer)({
337
+ sessionName,
338
+ answer,
339
+ cliToolId,
340
+ promptData: promptDetection.promptData,
341
+ });
322
342
  // 6. Update timestamp
323
343
  updateLastServerResponseTimestamp(worktreeId, Date.now());
324
344
  // 7. Reset error count on success
325
345
  resetErrorCount(worktreeId);
346
+ // Issue #306: Record answered prompt key and apply cooldown
347
+ pollerState.lastAnsweredPromptKey = promptKey;
326
348
  // Log success (without sensitive content)
327
349
  console.info(`[Auto-Yes Poller] Sent response for worktree: ${worktreeId}`);
350
+ // Issue #306: Apply cooldown interval after successful response (early return)
351
+ scheduleNextPoll(worktreeId, cliToolId, exports.COOLDOWN_INTERVAL_MS);
352
+ return;
328
353
  }
329
354
  catch (error) {
330
355
  // Increment error count on failure
331
356
  incrementErrorCount(worktreeId);
332
357
  // Log error (without sensitive details)
333
- const errorMessage = error instanceof Error ? error.message : 'Unknown error';
334
- console.warn(`[Auto-Yes Poller] Error for worktree ${worktreeId}: ${errorMessage}`);
358
+ console.warn(`[Auto-Yes Poller] Error for worktree ${worktreeId}: ${getErrorMessage(error)}`);
335
359
  }
336
- // Schedule next poll
360
+ // Schedule next poll (catch block fallthrough or other paths)
337
361
  scheduleNextPoll(worktreeId, cliToolId);
338
362
  }
339
363
  /**
340
364
  * Schedule the next polling iteration
365
+ * @param overrideInterval - Optional interval in milliseconds (S2-F009: type definition).
366
+ * When provided (e.g., COOLDOWN_INTERVAL_MS), overrides pollerState.currentInterval.
367
+ * Type: number | undefined (optional parameter).
341
368
  */
342
- function scheduleNextPoll(worktreeId, cliToolId) {
369
+ function scheduleNextPoll(worktreeId, cliToolId, overrideInterval) {
343
370
  const pollerState = autoYesPollerStates.get(worktreeId);
344
371
  if (!pollerState)
345
372
  return;
373
+ // S4-F003: Floor guard - polling interval must not be below POLLING_INTERVAL_MS
374
+ const interval = Math.max(overrideInterval ?? pollerState.currentInterval, exports.POLLING_INTERVAL_MS);
346
375
  pollerState.timerId = setTimeout(() => {
347
376
  pollAutoYes(worktreeId, cliToolId);
348
- }, pollerState.currentInterval);
377
+ }, interval);
349
378
  }
350
379
  /**
351
- * Start server-side auto-yes polling for a worktree
380
+ * Start server-side auto-yes polling for a worktree.
381
+ * Validates the worktree ID, checks auto-yes state, enforces concurrent poller limits,
382
+ * and begins the polling loop.
383
+ *
384
+ * @param worktreeId - Worktree identifier (must match WORKTREE_ID_PATTERN)
385
+ * @param cliToolId - CLI tool type to poll for
386
+ * @returns Result indicating whether the poller was started, with reason if not
352
387
  */
353
388
  function startAutoYesPolling(worktreeId, cliToolId) {
354
389
  // Validate worktree ID (security)
@@ -377,6 +412,7 @@ function startAutoYesPolling(worktreeId, cliToolId) {
377
412
  consecutiveErrors: 0,
378
413
  currentInterval: exports.POLLING_INTERVAL_MS,
379
414
  lastServerResponseTimestamp: null,
415
+ lastAnsweredPromptKey: null, // S2-F003: initialized to null
380
416
  };
381
417
  autoYesPollerStates.set(worktreeId, pollerState);
382
418
  // Start polling immediately
@@ -387,7 +423,10 @@ function startAutoYesPolling(worktreeId, cliToolId) {
387
423
  return { started: true };
388
424
  }
389
425
  /**
390
- * Stop server-side auto-yes polling for a worktree
426
+ * Stop server-side auto-yes polling for a worktree.
427
+ * Clears the timer and removes the poller state.
428
+ *
429
+ * @param worktreeId - Worktree identifier
391
430
  */
392
431
  function stopAutoYesPolling(worktreeId) {
393
432
  const pollerState = autoYesPollerStates.get(worktreeId);
@@ -402,7 +441,8 @@ function stopAutoYesPolling(worktreeId) {
402
441
  console.info(`[Auto-Yes Poller] Stopped for worktree: ${worktreeId}`);
403
442
  }
404
443
  /**
405
- * Stop all server-side auto-yes polling (graceful shutdown)
444
+ * Stop all server-side auto-yes polling (graceful shutdown).
445
+ * Clears all timers and removes all poller states.
406
446
  */
407
447
  function stopAllAutoYesPolling() {
408
448
  for (const [worktreeId, pollerState] of autoYesPollerStates.entries()) {
@@ -6,6 +6,7 @@
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
7
  exports.CLAUDE_PROMPT_POLL_INTERVAL = exports.CLAUDE_SEND_PROMPT_WAIT_TIMEOUT = exports.CLAUDE_PROMPT_WAIT_TIMEOUT = exports.CLAUDE_POST_PROMPT_DELAY = exports.CLAUDE_INIT_POLL_INTERVAL = exports.CLAUDE_INIT_TIMEOUT = void 0;
8
8
  exports.clearCachedClaudePath = clearCachedClaudePath;
9
+ exports.isSessionHealthy = isSessionHealthy;
9
10
  exports.getSessionName = getSessionName;
10
11
  exports.isClaudeInstalled = isClaudeInstalled;
11
12
  exports.isClaudeRunning = isClaudeRunning;
@@ -124,6 +125,17 @@ exports.CLAUDE_SEND_PROMPT_WAIT_TIMEOUT = 10000;
124
125
  * 200ms provides quick response while minimizing CPU usage.
125
126
  */
126
127
  exports.CLAUDE_PROMPT_POLL_INTERVAL = 200;
128
+ /**
129
+ * Maximum expected length of a shell prompt line (characters)
130
+ *
131
+ * Shell prompts are typically under 40 characters (e.g., "user@host:~/project$" ~30 chars).
132
+ * Lines at or above this threshold are not considered shell prompts, preventing
133
+ * false positives from Claude CLI output that happens to end with $, %, or #.
134
+ *
135
+ * Used by isSessionHealthy() to distinguish shell prompts from CLI output.
136
+ * 40 is an empirical threshold with safety margin.
137
+ */
138
+ const MAX_SHELL_PROMPT_LENGTH = 40;
127
139
  /**
128
140
  * Cached Claude CLI path
129
141
  */
@@ -229,45 +241,69 @@ async function getCleanPaneOutput(sessionName, lines = 50) {
229
241
  const output = await (0, tmux_1.capturePane)(sessionName, { startLine: -lines });
230
242
  return (0, cli_patterns_1.stripAnsi)(output);
231
243
  }
232
- // ----- Health Check Functions (Bug 2) -----
233
244
  /**
234
245
  * Verify that Claude CLI is actually running inside a tmux session
235
246
  * Detects broken sessions where tmux exists but Claude failed to start
236
247
  *
248
+ * @internal Exported for testing purposes only.
249
+ * Follows clearCachedClaudePath() precedent (L148-156).
250
+ *
237
251
  * @param sessionName - tmux session name
238
- * @returns true if Claude CLI is responsive (prompt detected or initializing)
252
+ * @returns HealthCheckResult with healthy status and optional reason
239
253
  */
240
254
  async function isSessionHealthy(sessionName) {
241
255
  try {
242
256
  // SF-001: Use shared helper instead of inline capturePane + stripAnsi
243
257
  const cleanOutput = await getCleanPaneOutput(sessionName);
244
- // MF-001: Check error patterns from cli-patterns.ts (SRP - pattern management centralized)
245
- for (const pattern of cli_patterns_1.CLAUDE_SESSION_ERROR_PATTERNS) {
246
- if (cleanOutput.includes(pattern)) {
247
- return false;
248
- }
249
- }
250
- for (const regex of cli_patterns_1.CLAUDE_SESSION_ERROR_REGEX_PATTERNS) {
251
- if (regex.test(cleanOutput)) {
252
- return false;
253
- }
254
- }
255
258
  // MF-002: Check shell prompt endings from extensible array (OCP)
256
259
  const trimmed = cleanOutput.trim();
260
+ // S2-F010: Empty output judgment (HealthCheckResult format)
257
261
  // C-S2-001: Empty output means tmux session exists but Claude CLI has no output.
258
262
  // This is treated as unhealthy because a properly running Claude CLI always
259
263
  // produces output (prompt, spinner, or response). An empty pane indicates
260
264
  // the CLI process has exited or failed to start.
261
265
  if (trimmed === '') {
262
- return false;
266
+ return { healthy: false, reason: 'empty output' };
263
267
  }
264
- if (SHELL_PROMPT_ENDINGS.some(ending => trimmed.endsWith(ending))) {
265
- return false;
268
+ // S2-F010: Error pattern detection (HealthCheckResult format)
269
+ // MF-001: Check error patterns from cli-patterns.ts (SRP - pattern management centralized)
270
+ for (const pattern of cli_patterns_1.CLAUDE_SESSION_ERROR_PATTERNS) {
271
+ if (trimmed.includes(pattern)) {
272
+ return { healthy: false, reason: `error pattern: ${pattern}` };
273
+ }
266
274
  }
267
- return true;
275
+ for (const regex of cli_patterns_1.CLAUDE_SESSION_ERROR_REGEX_PATTERNS) {
276
+ if (regex.test(trimmed)) {
277
+ return { healthy: false, reason: `error pattern: ${regex.source}` };
278
+ }
279
+ }
280
+ // S2-F002: Extract last line after empty line filtering
281
+ const lines = trimmed.split('\n').filter(line => line.trim() !== '');
282
+ const lastLine = lines[lines.length - 1]?.trim() ?? '';
283
+ // F006: Line length check BEFORE SHELL_PROMPT_ENDINGS check (early return)
284
+ if (lastLine.length >= MAX_SHELL_PROMPT_LENGTH) {
285
+ // Long lines are not shell prompts -> treat as healthy (early return)
286
+ return { healthy: true };
287
+ }
288
+ // F003: Individual pattern exclusions for SHELL_PROMPT_ENDINGS
289
+ // NOTE(F003): If new false positive patterns are found in the future,
290
+ // consider refactoring to a structure that associates exclusionPattern
291
+ // with each SHELL_PROMPT_ENDINGS entry. Currently only % needs exclusion (YAGNI).
292
+ if (SHELL_PROMPT_ENDINGS.some(ending => {
293
+ if (!lastLine.endsWith(ending))
294
+ return false;
295
+ // Exclude N% pattern (e.g., "Context left until auto-compact: 7%")
296
+ if (ending === '%' && /\d+%$/.test(lastLine))
297
+ return false;
298
+ return true;
299
+ })) {
300
+ return { healthy: false, reason: `shell prompt ending detected: ${lastLine}` };
301
+ }
302
+ return { healthy: true };
268
303
  }
269
304
  catch {
270
- return false;
305
+ // S3-F001: Catch block also returns HealthCheckResult format
306
+ return { healthy: false, reason: 'capture error' };
271
307
  }
272
308
  }
273
309
  /**
@@ -279,8 +315,9 @@ async function isSessionHealthy(sessionName) {
279
315
  * @returns true if session is healthy and can be reused, false if it was killed
280
316
  */
281
317
  async function ensureHealthySession(sessionName) {
282
- const healthy = await isSessionHealthy(sessionName);
283
- if (!healthy) {
318
+ const result = await isSessionHealthy(sessionName);
319
+ if (!result.healthy) {
320
+ console.warn(`[health-check] Session ${sessionName} unhealthy: ${result.reason}`);
284
321
  await (0, tmux_1.killSession)(sessionName);
285
322
  return false;
286
323
  }
@@ -376,7 +413,9 @@ async function isClaudeRunning(worktreeId) {
376
413
  return false;
377
414
  }
378
415
  // MF-S3-001: Verify session health to avoid reporting broken sessions as running
379
- return isSessionHealthy(sessionName);
416
+ // S2-F001: await + extract .healthy to maintain boolean return type
417
+ const result = await isSessionHealthy(sessionName);
418
+ return result.healthy;
380
419
  }
381
420
  /**
382
421
  * Get Claude session state
@@ -0,0 +1,89 @@
1
+ "use strict";
2
+ /**
3
+ * Shared prompt answer sender for cursor-key and text-based tmux input.
4
+ *
5
+ * Issue #287 Bug2: Extracted from route.ts and auto-yes-manager.ts to
6
+ * eliminate code duplication and ensure consistent behavior (including
7
+ * the promptType/defaultOptionNumber fallback introduced in Bug1).
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.sendPromptAnswer = sendPromptAnswer;
11
+ const tmux_1 = require("./tmux");
12
+ /** Regex pattern to detect checkbox-style multi-select options */
13
+ const CHECKBOX_OPTION_PATTERN = /^\[[ x]\] /;
14
+ /**
15
+ * Build navigation key array for cursor movement.
16
+ * @param offset - positive = Down, negative = Up
17
+ */
18
+ function buildNavigationKeys(offset) {
19
+ if (offset === 0)
20
+ return [];
21
+ const direction = offset > 0 ? 'Down' : 'Up';
22
+ return Array.from({ length: Math.abs(offset) }, () => direction);
23
+ }
24
+ /**
25
+ * Send an answer to a tmux session, using cursor-key navigation for
26
+ * Claude Code multiple-choice prompts and text input for everything else.
27
+ *
28
+ * This function unifies the logic previously duplicated in:
29
+ * - src/app/api/worktrees/[id]/prompt-response/route.ts (L114-187)
30
+ * - src/lib/auto-yes-manager.ts (L340-399)
31
+ */
32
+ async function sendPromptAnswer(params) {
33
+ const { sessionName, answer, cliToolId, promptData, fallbackPromptType, fallbackDefaultOptionNumber } = params;
34
+ // Determine if this is a Claude Code multiple-choice prompt requiring cursor navigation
35
+ const isClaudeMultiChoice = cliToolId === 'claude'
36
+ && (promptData?.type === 'multiple_choice' || fallbackPromptType === 'multiple_choice')
37
+ && /^\d+$/.test(answer);
38
+ if (isClaudeMultiChoice) {
39
+ const targetNum = parseInt(answer, 10);
40
+ let defaultNum;
41
+ let mcOptions = null;
42
+ if (promptData?.type === 'multiple_choice') {
43
+ // Primary path: use fresh promptData
44
+ mcOptions = promptData.options;
45
+ const defaultOption = mcOptions.find(o => o.isDefault);
46
+ defaultNum = defaultOption?.number ?? 1;
47
+ }
48
+ else {
49
+ // Fallback path (Issue #287): promptData is undefined or type mismatch, use fallback fields
50
+ defaultNum = fallbackDefaultOptionNumber ?? 1;
51
+ }
52
+ const offset = targetNum - defaultNum;
53
+ // Detect multi-select (checkbox) prompts by checking for [ ] in option labels.
54
+ // Multi-select prompts require: Space to toggle checkbox -> navigate to "Next" -> Enter.
55
+ // Single-select prompts require: navigate to option -> Enter.
56
+ // Note: multi-select detection is only possible when promptData succeeded (mcOptions available).
57
+ const isMultiSelect = mcOptions !== null && mcOptions.some(o => CHECKBOX_OPTION_PATTERN.test(o.label));
58
+ if (isMultiSelect && mcOptions !== null) {
59
+ // Multi-select: toggle checkbox, then navigate to "Next" and submit
60
+ const checkboxCount = mcOptions.filter(o => CHECKBOX_OPTION_PATTERN.test(o.label)).length;
61
+ const keys = [
62
+ ...buildNavigationKeys(offset), // 1. Navigate to target option
63
+ 'Space', // 2. Toggle checkbox
64
+ ];
65
+ // 3. Navigate to "Next" button (positioned right after all checkbox options)
66
+ const downToNext = checkboxCount - targetNum + 1;
67
+ keys.push(...buildNavigationKeys(downToNext));
68
+ // 4. Enter to submit
69
+ keys.push('Enter');
70
+ await (0, tmux_1.sendSpecialKeys)(sessionName, keys);
71
+ }
72
+ else {
73
+ // Single-select: navigate and Enter to select
74
+ const keys = [
75
+ ...buildNavigationKeys(offset),
76
+ 'Enter',
77
+ ];
78
+ await (0, tmux_1.sendSpecialKeys)(sessionName, keys);
79
+ }
80
+ }
81
+ else {
82
+ // Standard CLI prompt: send text + Enter (y/n, Approve?, etc.)
83
+ await (0, tmux_1.sendKeys)(sessionName, answer, false);
84
+ // Wait a moment for the input to be processed
85
+ await new Promise(resolve => setTimeout(resolve, 100));
86
+ // Send Enter
87
+ await (0, tmux_1.sendKeys)(sessionName, '', true);
88
+ }
89
+ }
@@ -561,6 +561,15 @@ function detectMultipleChoicePrompt(output, options) {
561
561
  collectedOptions.unshift({ number, label, isDefault: false });
562
562
  continue;
563
563
  }
564
+ // [Issue #287 Bug3] User input prompt barrier:
565
+ // When no options have been collected yet and the line starts with ❯ (U+276F)
566
+ // but did NOT match DEFAULT_OPTION_PATTERN above, this line is a Claude Code
567
+ // user input prompt (e.g., "❯ 1", "❯ /command") or idle prompt ("❯").
568
+ // Anything above this line in the scrollback is historical conversation text,
569
+ // not an active prompt. Stop scanning to prevent false positives.
570
+ if (collectedOptions.length === 0 && line.startsWith('\u276F')) {
571
+ return noPromptResult(output);
572
+ }
564
573
  // Non-option line handling
565
574
  if (collectedOptions.length > 0 && line && !SEPARATOR_LINE_PATTERN.test(line)) {
566
575
  // [MF-001 / Issue #256] Check if line is a question-like line BEFORE
@@ -0,0 +1,30 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.generatePromptKey = generatePromptKey;
4
+ /**
5
+ * Generate a composite key for prompt deduplication.
6
+ *
7
+ * Used by both client-side (useAutoYes.ts) and server-side (auto-yes-manager.ts)
8
+ * to ensure consistent prompt identification across the duplicate prevention system.
9
+ *
10
+ * The key format is `{type}:{question}`, which uniquely identifies a prompt
11
+ * by combining its type and question text.
12
+ *
13
+ * @param promptData - Prompt data containing type and question fields
14
+ * @returns Composite key string in the format "type:question"
15
+ *
16
+ * @internal Used for in-memory comparison only. Do NOT use for logging,
17
+ * persistence, or external output. If the return value is ever used in
18
+ * log output, DB storage, or HTML rendering, apply appropriate sanitization
19
+ * (CR/LF escaping, prepared statements, HTML escaping respectively).
20
+ * See SEC: S4-F001.
21
+ *
22
+ * @example
23
+ * ```typescript
24
+ * const key = generatePromptKey({ type: 'yes_no', question: 'Continue?' });
25
+ * // Returns 'yes_no:Continue?'
26
+ * ```
27
+ */
28
+ function generatePromptKey(promptData) {
29
+ return `${promptData.type}:${promptData.question}`;
30
+ }
package/package.json CHANGED
@@ -1,7 +1,18 @@
1
1
  {
2
2
  "name": "commandmate",
3
- "version": "0.2.9",
3
+ "version": "0.2.11",
4
4
  "description": "Git worktree management with Claude CLI and tmux sessions",
5
+ "keywords": [
6
+ "claude-code",
7
+ "codex-cli",
8
+ "terminal",
9
+ "session-manager",
10
+ "tmux",
11
+ "git-worktree",
12
+ "ai-coding",
13
+ "cli",
14
+ "developer-tools"
15
+ ],
5
16
  "repository": {
6
17
  "type": "git",
7
18
  "url": "https://github.com/Kewton/CommandMate.git"