commandmate 0.2.0 → 0.2.2

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 (109) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/app-build-manifest.json +13 -13
  3. package/.next/app-path-routes-manifest.json +1 -1
  4. package/.next/build-manifest.json +2 -2
  5. package/.next/cache/.tsbuildinfo +1 -1
  6. package/.next/cache/config.json +3 -3
  7. package/.next/cache/webpack/client-production/0.pack +0 -0
  8. package/.next/cache/webpack/client-production/1.pack +0 -0
  9. package/.next/cache/webpack/client-production/2.pack +0 -0
  10. package/.next/cache/webpack/client-production/index.pack +0 -0
  11. package/.next/cache/webpack/client-production/index.pack.old +0 -0
  12. package/.next/cache/webpack/edge-server-production/index.pack +0 -0
  13. package/.next/cache/webpack/server-production/0.pack +0 -0
  14. package/.next/cache/webpack/server-production/index.pack +0 -0
  15. package/.next/next-server.js.nft.json +1 -1
  16. package/.next/prerender-manifest.json +1 -1
  17. package/.next/required-server-files.json +1 -1
  18. package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  19. package/.next/server/app/_not-found.html +1 -1
  20. package/.next/server/app/_not-found.rsc +1 -1
  21. package/.next/server/app/api/external-apps/[id]/health/route.js.nft.json +1 -1
  22. package/.next/server/app/api/external-apps/[id]/route.js.nft.json +1 -1
  23. package/.next/server/app/api/external-apps/route.js.nft.json +1 -1
  24. package/.next/server/app/api/hooks/claude-done/route.js.nft.json +1 -1
  25. package/.next/server/app/api/repositories/clone/[jobId]/route.js.nft.json +1 -1
  26. package/.next/server/app/api/repositories/clone/route.js.nft.json +1 -1
  27. package/.next/server/app/api/repositories/excluded/route.js +8 -8
  28. package/.next/server/app/api/repositories/excluded/route.js.nft.json +1 -1
  29. package/.next/server/app/api/repositories/restore/route.js +7 -7
  30. package/.next/server/app/api/repositories/restore/route.js.nft.json +1 -1
  31. package/.next/server/app/api/repositories/route.js +5 -5
  32. package/.next/server/app/api/repositories/route.js.nft.json +1 -1
  33. package/.next/server/app/api/repositories/scan/route.js.nft.json +1 -1
  34. package/.next/server/app/api/repositories/sync/route.js +5 -5
  35. package/.next/server/app/api/repositories/sync/route.js.nft.json +1 -1
  36. package/.next/server/app/api/slash-commands.body +1 -1
  37. package/.next/server/app/api/worktrees/[id]/auto-yes/route.js +1 -1
  38. package/.next/server/app/api/worktrees/[id]/auto-yes/route.js.nft.json +1 -1
  39. package/.next/server/app/api/worktrees/[id]/capture/route.js +2 -2
  40. package/.next/server/app/api/worktrees/[id]/cli-tool/route.js.nft.json +1 -1
  41. package/.next/server/app/api/worktrees/[id]/current-output/route.js +1 -1
  42. package/.next/server/app/api/worktrees/[id]/current-output/route.js.nft.json +1 -1
  43. package/.next/server/app/api/worktrees/[id]/files/[...path]/route.js.nft.json +1 -1
  44. package/.next/server/app/api/worktrees/[id]/interrupt/route.js.nft.json +1 -1
  45. package/.next/server/app/api/worktrees/[id]/kill-session/route.js.nft.json +1 -1
  46. package/.next/server/app/api/worktrees/[id]/logs/[filename]/route.js.nft.json +1 -1
  47. package/.next/server/app/api/worktrees/[id]/logs/route.js.nft.json +1 -1
  48. package/.next/server/app/api/worktrees/[id]/memos/[memoId]/route.js.nft.json +1 -1
  49. package/.next/server/app/api/worktrees/[id]/memos/route.js.nft.json +1 -1
  50. package/.next/server/app/api/worktrees/[id]/messages/route.js.nft.json +1 -1
  51. package/.next/server/app/api/worktrees/[id]/prompt-response/route.js +1 -1
  52. package/.next/server/app/api/worktrees/[id]/prompt-response/route.js.nft.json +1 -1
  53. package/.next/server/app/api/worktrees/[id]/respond/route.js.nft.json +1 -1
  54. package/.next/server/app/api/worktrees/[id]/route.js +1 -1
  55. package/.next/server/app/api/worktrees/[id]/route.js.nft.json +1 -1
  56. package/.next/server/app/api/worktrees/[id]/search/route.js.nft.json +1 -1
  57. package/.next/server/app/api/worktrees/[id]/send/route.js.nft.json +1 -1
  58. package/.next/server/app/api/worktrees/[id]/slash-commands/route.js.nft.json +1 -1
  59. package/.next/server/app/api/worktrees/[id]/start-polling/route.js.nft.json +1 -1
  60. package/.next/server/app/api/worktrees/[id]/terminal/route.js +1 -1
  61. package/.next/server/app/api/worktrees/[id]/tree/[...path]/route.js.nft.json +1 -1
  62. package/.next/server/app/api/worktrees/[id]/tree/route.js.nft.json +1 -1
  63. package/.next/server/app/api/worktrees/[id]/upload/[...path]/route.js.nft.json +1 -1
  64. package/.next/server/app/api/worktrees/[id]/viewed/route.js.nft.json +1 -1
  65. package/.next/server/app/api/worktrees/route.js +1 -1
  66. package/.next/server/app/api/worktrees/route.js.nft.json +1 -1
  67. package/.next/server/app/index.html +2 -2
  68. package/.next/server/app/index.rsc +2 -2
  69. package/.next/server/app/page_client-reference-manifest.js +1 -1
  70. package/.next/server/app/proxy/[...path]/route.js.nft.json +1 -1
  71. package/.next/server/app/worktrees/[id]/files/[...path]/page.js +1 -1
  72. package/.next/server/app/worktrees/[id]/files/[...path]/page_client-reference-manifest.js +1 -1
  73. package/.next/server/app/worktrees/[id]/page.js +5 -5
  74. package/.next/server/app/worktrees/[id]/page_client-reference-manifest.js +1 -1
  75. package/.next/server/app/worktrees/[id]/simple-terminal/page_client-reference-manifest.js +1 -1
  76. package/.next/server/app/worktrees/[id]/terminal/page_client-reference-manifest.js +1 -1
  77. package/.next/server/app-paths-manifest.json +9 -9
  78. package/.next/server/chunks/5488.js +5 -5
  79. package/.next/server/chunks/7536.js +1 -1
  80. package/.next/server/chunks/8174.js +6 -6
  81. package/.next/server/chunks/9367.js +2 -2
  82. package/.next/server/functions-config-manifest.json +1 -1
  83. package/.next/server/pages/404.html +1 -1
  84. package/.next/server/pages/500.html +1 -1
  85. package/.next/server/server-reference-manifest.json +1 -1
  86. package/.next/static/chunks/6568-c65d7e4d7db7655b.js +1 -0
  87. package/.next/static/chunks/{2957-327e43ef4c12808f.js → 9325-9e98829c1e75f42f.js} +1 -1
  88. package/.next/static/chunks/app/worktrees/[id]/files/[...path]/{page-9e5adf57cbbbdf05.js → page-7eb14f8043796805.js} +1 -1
  89. package/.next/static/chunks/app/worktrees/[id]/page-912c3c4c66821d99.js +1 -0
  90. package/.next/static/css/d4b58a1129eff6af.css +3 -0
  91. package/.next/trace +5 -5
  92. package/dist/server/server.js +25 -2
  93. package/dist/server/src/config/auto-yes-config.js +53 -0
  94. package/dist/server/src/lib/auto-yes-manager.js +94 -11
  95. package/dist/server/src/lib/claude-poller.js +4 -0
  96. package/dist/server/src/lib/claude-session.js +54 -19
  97. package/dist/server/src/lib/cli-patterns.js +100 -4
  98. package/dist/server/src/lib/cli-tools/codex.js +16 -3
  99. package/dist/server/src/lib/db-repository.js +482 -0
  100. package/dist/server/src/lib/pasted-text-helper.js +58 -0
  101. package/dist/server/src/lib/prompt-detector.js +199 -109
  102. package/dist/server/src/lib/response-poller.js +74 -27
  103. package/dist/server/src/lib/tmux.js +48 -0
  104. package/package.json +1 -1
  105. package/.next/static/chunks/6568-38a33aa67d82e12b.js +0 -1
  106. package/.next/static/chunks/app/worktrees/[id]/page-d64624eb67af57c0.js +0 -1
  107. package/.next/static/css/28be35e4727ae7ef.css +0 -3
  108. /package/.next/static/{bdUePCj-b9Gv5okYGp49O → HhG0EHeG9E4wTJ4sqnLdv}/_buildManifest.js +0 -0
  109. /package/.next/static/{bdUePCj-b9Gv5okYGp49O → HhG0EHeG9E4wTJ4sqnLdv}/_ssgManifest.js +0 -0
@@ -37,6 +37,8 @@ const response_poller_1 = require("./src/lib/response-poller");
37
37
  const auto_yes_manager_1 = require("./src/lib/auto-yes-manager");
38
38
  const db_migrations_1 = require("./src/lib/db-migrations");
39
39
  const env_1 = require("./src/lib/env");
40
+ const db_repository_1 = require("./src/lib/db-repository");
41
+ const db_1 = require("./src/lib/db");
40
42
  const dev = process.env.NODE_ENV !== 'production';
41
43
  const hostname = (0, env_1.getEnvByKey)('CM_BIND') || '127.0.0.1';
42
44
  const port = parseInt((0, env_1.getEnvByKey)('CM_PORT') || '3000', 10);
@@ -76,8 +78,29 @@ app.prepare().then(() => {
76
78
  repositoryPaths.forEach((path, i) => {
77
79
  console.log(` ${i + 1}. ${path}`);
78
80
  });
79
- // Scan all repositories
80
- const worktrees = await (0, worktrees_1.scanMultipleRepositories)(repositoryPaths);
81
+ // Issue #202: Register environment variable repositories and filter out excluded ones
82
+ // registerAndFilterRepositories() encapsulates the ordering constraint:
83
+ // registration MUST happen before filtering (see design policy Section 4)
84
+ const { filteredPaths, excludedPaths, excludedCount } = (0, db_repository_1.registerAndFilterRepositories)(db, repositoryPaths);
85
+ if (excludedCount > 0) {
86
+ console.log(`Excluded repositories: ${excludedCount}, Active repositories: ${filteredPaths.length}`);
87
+ // SF-SEC-003: Log excluded repository paths for audit/troubleshooting
88
+ excludedPaths.forEach(p => {
89
+ console.log(` [excluded] ${p}`);
90
+ });
91
+ // Issue #202: Remove worktrees of excluded repositories from DB
92
+ // Without this, worktree records remain in DB and appear in the UI
93
+ for (const excludedPath of excludedPaths) {
94
+ const resolvedPath = (0, db_repository_1.resolveRepositoryPath)(excludedPath);
95
+ const worktreeIds = (0, db_1.getWorktreeIdsByRepository)(db, resolvedPath);
96
+ if (worktreeIds.length > 0) {
97
+ const result = (0, db_1.deleteWorktreesByIds)(db, worktreeIds);
98
+ console.log(` Removed ${result.deletedCount} worktree(s) from excluded repository: ${resolvedPath}`);
99
+ }
100
+ }
101
+ }
102
+ // Scan filtered repositories (excluded repos are skipped)
103
+ const worktrees = await (0, worktrees_1.scanMultipleRepositories)(filteredPaths);
81
104
  // Sync to database
82
105
  (0, worktrees_1.syncWorktreesToDB)(db, worktrees);
83
106
  console.log(`✓ Total: ${worktrees.length} worktree(s) synced to database`);
@@ -0,0 +1,53 @@
1
+ "use strict";
2
+ /**
3
+ * Auto-Yes Configuration Constants
4
+ *
5
+ * Shared config for Auto-Yes duration settings.
6
+ * Used by both server (auto-yes-manager.ts, route.ts) and client
7
+ * (AutoYesConfirmDialog.tsx, AutoYesToggle.tsx) components.
8
+ *
9
+ * Issue #225: Duration selection feature
10
+ */
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.DURATION_LABELS = exports.DEFAULT_AUTO_YES_DURATION = exports.ALLOWED_DURATIONS = void 0;
13
+ exports.isAllowedDuration = isAllowedDuration;
14
+ exports.formatTimeRemaining = formatTimeRemaining;
15
+ /** Allowed Auto-Yes durations in milliseconds */
16
+ exports.ALLOWED_DURATIONS = [3600000, 10800000, 28800000];
17
+ /** Default Auto-Yes duration (1 hour = 3600000ms) */
18
+ exports.DEFAULT_AUTO_YES_DURATION = 3600000;
19
+ /** UI display labels for each duration value */
20
+ exports.DURATION_LABELS = {
21
+ 3600000: '1時間',
22
+ 10800000: '3時間',
23
+ 28800000: '8時間',
24
+ };
25
+ /**
26
+ * Type guard: check whether a value is a valid AutoYesDuration.
27
+ * Replaces `as AutoYesDuration` casts with a runtime-safe check.
28
+ */
29
+ function isAllowedDuration(value) {
30
+ return typeof value === 'number' && exports.ALLOWED_DURATIONS.includes(value);
31
+ }
32
+ /** Milliseconds per second */
33
+ const MS_PER_SECOND = 1000;
34
+ /** Milliseconds per minute */
35
+ const MS_PER_MINUTE = 60000;
36
+ /** Milliseconds per hour */
37
+ const MS_PER_HOUR = 3600000;
38
+ /**
39
+ * Format remaining time as MM:SS (under 1 hour) or H:MM:SS (1 hour or more).
40
+ *
41
+ * Extracted from AutoYesToggle.tsx for direct testability and reuse.
42
+ * Negative remaining time is clamped to 0.
43
+ */
44
+ function formatTimeRemaining(expiresAt) {
45
+ const remaining = Math.max(0, expiresAt - Date.now());
46
+ const hours = Math.floor(remaining / MS_PER_HOUR);
47
+ const minutes = Math.floor((remaining % MS_PER_HOUR) / MS_PER_MINUTE);
48
+ const seconds = Math.floor((remaining % MS_PER_MINUTE) / MS_PER_SECOND);
49
+ if (hours > 0) {
50
+ return `${hours}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
51
+ }
52
+ return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
53
+ }
@@ -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.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.POLLING_INTERVAL_MS = void 0;
13
13
  exports.isValidWorktreeId = isValidWorktreeId;
14
14
  exports.calculateBackoffInterval = calculateBackoffInterval;
15
15
  exports.isAutoYesExpired = isAutoYesExpired;
@@ -28,6 +28,7 @@ const auto_yes_resolver_1 = require("./auto-yes-resolver");
28
28
  const tmux_1 = require("./tmux");
29
29
  const manager_1 = require("./cli-tools/manager");
30
30
  const cli_patterns_1 = require("./cli-patterns");
31
+ const auto_yes_config_1 = require("../config/auto-yes-config");
31
32
  // =============================================================================
32
33
  // Constants (Issue #138)
33
34
  // =============================================================================
@@ -39,8 +40,16 @@ exports.MAX_BACKOFF_MS = 60000;
39
40
  exports.MAX_CONSECUTIVE_ERRORS = 5;
40
41
  /** Maximum concurrent pollers (DoS protection) */
41
42
  exports.MAX_CONCURRENT_POLLERS = 50;
42
- /** Timeout duration: 1 hour in milliseconds */
43
- const AUTO_YES_TIMEOUT_MS = 3600000;
43
+ /**
44
+ * Number of lines from the end to check for thinking indicators (Issue #191)
45
+ * Matches detectPrompt()'s multiple_choice scan range (50 lines in prompt-detector.ts)
46
+ * to ensure Issue #161 Layer 1 defense covers the same scope as prompt detection.
47
+ *
48
+ * IMPORTANT: This value is semantically coupled to the hardcoded 50 in
49
+ * prompt-detector.ts detectMultipleChoicePrompt() (L268: Math.max(0, lines.length - 50)).
50
+ * See SF-001 in Stage 1 review. A cross-reference test validates this coupling.
51
+ */
52
+ exports.THINKING_CHECK_LINE_COUNT = 50;
44
53
  /** Worktree ID validation pattern (security: prevent command injection) */
45
54
  const WORKTREE_ID_PATTERN = /^[a-zA-Z0-9_-]+$/;
46
55
  /** In-memory storage for auto-yes states (globalThis for hot reload persistence) */
@@ -106,14 +115,17 @@ function getAutoYesState(worktreeId) {
106
115
  }
107
116
  /**
108
117
  * Set the auto-yes enabled state for a worktree
118
+ * @param duration - Optional duration in milliseconds (must be an ALLOWED_DURATIONS value).
119
+ * Defaults to DEFAULT_AUTO_YES_DURATION (1 hour) when omitted.
109
120
  */
110
- function setAutoYesEnabled(worktreeId, enabled) {
121
+ function setAutoYesEnabled(worktreeId, enabled, duration) {
111
122
  if (enabled) {
112
123
  const now = Date.now();
124
+ const effectiveDuration = duration ?? auto_yes_config_1.DEFAULT_AUTO_YES_DURATION;
113
125
  const state = {
114
126
  enabled: true,
115
127
  enabledAt: now,
116
- expiresAt: now + AUTO_YES_TIMEOUT_MS,
128
+ expiresAt: now + effectiveDuration,
117
129
  };
118
130
  autoYesStates.set(worktreeId, state);
119
131
  return state;
@@ -210,12 +222,29 @@ async function pollAutoYes(worktreeId, cliToolId) {
210
222
  // 2.5. Skip prompt detection during thinking state (Issue #161, Layer 1)
211
223
  // This prevents false positive detection of numbered lists in CLI output
212
224
  // while Claude is actively processing (thinking/planning).
213
- if ((0, cli_patterns_1.detectThinking)(cliToolId, cleanOutput)) {
225
+ //
226
+ // Issue #191: Apply windowing to detectThinking() to prevent stale thinking
227
+ // summary lines (e.g., "· Simmering…") from blocking prompt detection.
228
+ // Window size matches detectPrompt()'s multiple_choice scan range (50 lines).
229
+ //
230
+ // Safety: Claude CLI does not emit prompts during thinking, so narrowing
231
+ // the window cannot cause false auto-responses (see IA-003 in design doc).
232
+ //
233
+ // Processing order: stripAnsi -> split -> slice -> join
234
+ // stripAnsi is applied BEFORE split to ensure ANSI escape sequences spanning
235
+ // line boundaries do not affect line counting (IA-002).
236
+ //
237
+ // Boundary case: if buffer has fewer than 50 lines, slice(-50) returns the
238
+ // entire array (Array.prototype.slice specification), which is safe degradation
239
+ // equivalent to pre-fix behavior (IA-001).
240
+ const recentLines = cleanOutput.split('\n').slice(-exports.THINKING_CHECK_LINE_COUNT).join('\n');
241
+ if ((0, cli_patterns_1.detectThinking)(cliToolId, recentLines)) {
214
242
  scheduleNextPoll(worktreeId, cliToolId);
215
243
  return;
216
244
  }
217
245
  // 3. Detect prompt
218
- const promptDetection = (0, prompt_detector_1.detectPrompt)(cleanOutput);
246
+ const promptOptions = (0, cli_patterns_1.buildDetectPromptOptions)(cliToolId);
247
+ const promptDetection = (0, prompt_detector_1.detectPrompt)(cleanOutput, promptOptions);
219
248
  if (!promptDetection.isPrompt || !promptDetection.promptData) {
220
249
  // No prompt detected, schedule next poll
221
250
  scheduleNextPoll(worktreeId, cliToolId);
@@ -232,10 +261,64 @@ async function pollAutoYes(worktreeId, cliToolId) {
232
261
  const manager = manager_1.CLIToolManager.getInstance();
233
262
  const cliTool = manager.getTool(cliToolId);
234
263
  const sessionName = cliTool.getSessionName(worktreeId);
235
- // Send answer followed by Enter
236
- await (0, tmux_1.sendKeys)(sessionName, answer, false);
237
- await new Promise(resolve => setTimeout(resolve, 100));
238
- await (0, tmux_1.sendKeys)(sessionName, '', true);
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
+ }
239
322
  // 6. Update timestamp
240
323
  updateLastServerResponseTimestamp(worktreeId, Date.now());
241
324
  // 7. Reset error count on success
@@ -138,6 +138,8 @@ function extractClaudeResponse(output, lastCapturedLine) {
138
138
  }
139
139
  // Check if this is an interactive prompt (yes/no or multiple choice)
140
140
  // Interactive prompts don't have the ">" prompt and separator, so we need to detect them separately
141
+ // TODO [Issue #193]: This code path is unreachable (claude-poller.ts is superseded by response-poller.ts).
142
+ // When refactoring/removing claude-poller.ts, apply stripAnsi() + buildDetectPromptOptions() here.
141
143
  if (!isThinking) {
142
144
  const fullOutput = lines.join('\n');
143
145
  const promptDetection = (0, prompt_detector_1.detectPrompt)(fullOutput);
@@ -196,6 +198,8 @@ async function checkForResponse(worktreeId) {
196
198
  return false;
197
199
  }
198
200
  // Response is complete! Check if it's a prompt
201
+ // TODO [Issue #193]: This code path is unreachable (claude-poller.ts is superseded by response-poller.ts).
202
+ // When refactoring/removing claude-poller.ts, apply stripAnsi() + buildDetectPromptOptions() here.
199
203
  const promptDetection = (0, prompt_detector_1.detectPrompt)(result.response);
200
204
  if (promptDetection.isPrompt) {
201
205
  // This is a prompt - save as prompt message
@@ -4,7 +4,7 @@
4
4
  * Manages Claude CLI sessions within tmux for each worktree
5
5
  */
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
- exports.CLAUDE_PROMPT_POLL_INTERVAL = exports.CLAUDE_PROMPT_WAIT_TIMEOUT = exports.CLAUDE_POST_PROMPT_DELAY = exports.CLAUDE_INIT_POLL_INTERVAL = exports.CLAUDE_INIT_TIMEOUT = void 0;
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.getSessionName = getSessionName;
9
9
  exports.isClaudeInstalled = isClaudeInstalled;
10
10
  exports.isClaudeRunning = isClaudeRunning;
@@ -17,6 +17,7 @@ exports.stopClaudeSession = stopClaudeSession;
17
17
  exports.restartClaudeSession = restartClaudeSession;
18
18
  const tmux_1 = require("./tmux");
19
19
  const cli_patterns_1 = require("./cli-patterns");
20
+ const pasted_text_helper_1 = require("./pasted-text-helper");
20
21
  const child_process_1 = require("child_process");
21
22
  const util_1 = require("util");
22
23
  const execAsync = (0, util_1.promisify)(child_process_1.exec);
@@ -42,6 +43,9 @@ function getErrorMessage(error) {
42
43
  * - Authenticate with Anthropic servers (if needed)
43
44
  * - Display the interactive prompt
44
45
  *
46
+ * This timeout also covers trust dialog auto-response time (typically <1s).
47
+ * When reducing this value, consider dialog response overhead.
48
+ *
45
49
  * 15 seconds provides headroom for slower networks or cold starts.
46
50
  */
47
51
  exports.CLAUDE_INIT_TIMEOUT = 15000;
@@ -75,6 +79,24 @@ exports.CLAUDE_POST_PROMPT_DELAY = 500;
75
79
  * if it's still processing a previous request.
76
80
  */
77
81
  exports.CLAUDE_PROMPT_WAIT_TIMEOUT = 5000;
82
+ /**
83
+ * Prompt wait timeout before message send (milliseconds).
84
+ *
85
+ * Used exclusively by sendMessageToClaude() to limit how long we wait
86
+ * for Claude to return to a prompt state before sending a user message.
87
+ * This is separate from CLAUDE_PROMPT_WAIT_TIMEOUT (5000ms, the default
88
+ * for waitForPrompt()) because sendMessageToClaude() may be called
89
+ * shortly after session initialization, where Claude CLI needs additional
90
+ * time to become ready.
91
+ *
92
+ * Relationship to other timeout constants:
93
+ * - CLAUDE_PROMPT_WAIT_TIMEOUT (5000ms): Default for waitForPrompt()
94
+ * - CLAUDE_SEND_PROMPT_WAIT_TIMEOUT (10000ms): sendMessageToClaude() specific
95
+ * - CLAUDE_INIT_TIMEOUT (15000ms): Session initialization timeout
96
+ *
97
+ * @see Issue #187 - Constant unification for sendMessageToClaude timeout
98
+ */
99
+ exports.CLAUDE_SEND_PROMPT_WAIT_TIMEOUT = 10000;
78
100
  /**
79
101
  * Prompt wait polling interval (milliseconds)
80
102
  *
@@ -227,7 +249,6 @@ async function waitForPrompt(sessionName, timeout = exports.CLAUDE_PROMPT_WAIT_T
227
249
  * await startClaudeSession({
228
250
  * worktreeId: 'feature-foo',
229
251
  * worktreePath: '/path/to/worktree',
230
- * baseUrl: 'http://localhost:3000',
231
252
  * });
232
253
  * ```
233
254
  */
@@ -262,21 +283,33 @@ async function startClaudeSession(options) {
262
283
  const pollInterval = exports.CLAUDE_INIT_POLL_INTERVAL;
263
284
  const startTime = Date.now();
264
285
  let initialized = false;
286
+ let trustDialogHandled = false;
265
287
  while (Date.now() - startTime < maxWaitTime) {
266
288
  await new Promise((resolve) => setTimeout(resolve, pollInterval));
267
289
  try {
268
290
  const output = await (0, tmux_1.capturePane)(sessionName, { startLine: -50 });
269
- // Claude is ready when we see the prompt or separator line (DRY-001, DRY-002)
270
- // Use patterns from cli-patterns.ts for consistency
291
+ // Claude is ready when we see the prompt (DRY-001)
292
+ // Use CLAUDE_PROMPT_PATTERN from cli-patterns.ts for consistency
271
293
  // Strip ANSI escape sequences before pattern matching (Issue #152)
294
+ // Note: CLAUDE_SEPARATOR_PATTERN was removed from initialization check (Issue #187, P1-1)
295
+ // because separator early-detection caused premature returns before prompt was ready
272
296
  const cleanOutput = (0, cli_patterns_1.stripAnsi)(output);
273
- if (cli_patterns_1.CLAUDE_PROMPT_PATTERN.test(cleanOutput) || cli_patterns_1.CLAUDE_SEPARATOR_PATTERN.test(cleanOutput)) {
297
+ if (cli_patterns_1.CLAUDE_PROMPT_PATTERN.test(cleanOutput)) {
274
298
  // Wait for stability after prompt detection (CONS-007, DOC-001)
275
299
  await new Promise((resolve) => setTimeout(resolve, exports.CLAUDE_POST_PROMPT_DELAY));
276
300
  console.log(`Claude initialized in ${Date.now() - startTime}ms`);
277
301
  initialized = true;
278
302
  break;
279
303
  }
304
+ // Issue #201: Detect trust dialog and auto-respond with Enter
305
+ // Condition order: CLAUDE_PROMPT_PATTERN (above) is checked first for shortest path
306
+ if (!trustDialogHandled && cli_patterns_1.CLAUDE_TRUST_DIALOG_PATTERN.test(cleanOutput)) {
307
+ await (0, tmux_1.sendKeys)(sessionName, '', true);
308
+ trustDialogHandled = true;
309
+ // TODO: Log output method unification (console.log vs createLogger) to be addressed in a separate Issue (SF-002)
310
+ console.log('Trust dialog detected, sending Enter to confirm');
311
+ // Continue polling to wait for prompt detection
312
+ }
280
313
  }
281
314
  catch {
282
315
  // Ignore capture errors during initialization
@@ -312,23 +345,26 @@ async function sendMessageToClaude(worktreeId, message) {
312
345
  throw new Error(`Claude session ${sessionName} does not exist. Start the session first.`);
313
346
  }
314
347
  // Verify prompt state before sending (CONS-006, DRY-001)
315
- // Use -50 lines to ensure we capture the prompt even with status bars
316
348
  const output = await (0, tmux_1.capturePane)(sessionName, { startLine: -50 });
317
- // Strip ANSI escape sequences before pattern matching (Issue #152)
318
349
  if (!cli_patterns_1.CLAUDE_PROMPT_PATTERN.test((0, cli_patterns_1.stripAnsi)(output))) {
319
- // Wait for prompt if not at prompt state
320
- // Use longer timeout (10s) to handle slow responses
321
- try {
322
- await waitForPrompt(sessionName, 10000);
323
- }
324
- catch {
325
- // Log warning but don't block - Claude might be in a special state
326
- console.warn(`[sendMessageToClaude] Prompt not detected, sending anyway`);
327
- }
350
+ // Path B: Prompt not detected - wait for it (P1: throw on timeout)
351
+ await waitForPrompt(sessionName, exports.CLAUDE_SEND_PROMPT_WAIT_TIMEOUT);
328
352
  }
353
+ // P0: Stability delay after prompt detection (both Path A and Path B)
354
+ // Same delay as startClaudeSession() to ensure Claude CLI input handler is ready
355
+ // NOTE: This 500ms delay also applies to 2nd+ messages. Currently acceptable since
356
+ // Claude CLI response time (seconds to tens of seconds) dwarfs this overhead.
357
+ // If future batch-send use cases arise, this could be optimized to first-message-only,
358
+ // but that optimization is deferred per YAGNI principle. (ref: F-3)
359
+ await new Promise((resolve) => setTimeout(resolve, exports.CLAUDE_POST_PROMPT_DELAY));
329
360
  // Send message using sendKeys consistently (CONS-001)
330
- await (0, tmux_1.sendKeys)(sessionName, message, false); // Message without Enter
331
- await (0, tmux_1.sendKeys)(sessionName, '', true); // Enter key
361
+ await (0, tmux_1.sendKeys)(sessionName, message, false);
362
+ await (0, tmux_1.sendKeys)(sessionName, '', true);
363
+ // Issue #212: Detect [Pasted text] and resend Enter for multi-line messages
364
+ // MF-001: Single-line messages skip detection (+0ms overhead)
365
+ if (message.includes('\n')) {
366
+ await (0, pasted_text_helper_1.detectAndResendIfPastedText)(sessionName);
367
+ }
332
368
  console.log(`Sent message to Claude session: ${sessionName}`);
333
369
  }
334
370
  /**
@@ -403,7 +439,6 @@ async function stopClaudeSession(worktreeId) {
403
439
  * await restartClaudeSession({
404
440
  * worktreeId: 'feature-foo',
405
441
  * worktreePath: '/path/to/worktree',
406
- * baseUrl: 'http://localhost:3000',
407
442
  * });
408
443
  * ```
409
444
  */
@@ -4,10 +4,11 @@
4
4
  * Shared between response-poller.ts and API routes
5
5
  */
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
- exports.GEMINI_PROMPT_PATTERN = exports.CODEX_SEPARATOR_PATTERN = exports.CODEX_PROMPT_PATTERN = exports.CLAUDE_SEPARATOR_PATTERN = exports.CLAUDE_PROMPT_PATTERN = exports.CODEX_THINKING_PATTERN = exports.CLAUDE_THINKING_PATTERN = exports.CLAUDE_SPINNER_CHARS = void 0;
7
+ exports.GEMINI_PROMPT_PATTERN = exports.MAX_PASTED_TEXT_RETRIES = exports.PASTED_TEXT_DETECT_DELAY = exports.PASTED_TEXT_PATTERN = exports.CODEX_SEPARATOR_PATTERN = exports.CODEX_PROMPT_PATTERN = exports.CLAUDE_TRUST_DIALOG_PATTERN = exports.CLAUDE_SEPARATOR_PATTERN = exports.CLAUDE_PROMPT_PATTERN = exports.CODEX_THINKING_PATTERN = exports.CLAUDE_THINKING_PATTERN = exports.CLAUDE_SPINNER_CHARS = void 0;
8
8
  exports.detectThinking = detectThinking;
9
9
  exports.getCliToolPatterns = getCliToolPatterns;
10
10
  exports.stripAnsi = stripAnsi;
11
+ exports.buildDetectPromptOptions = buildDetectPromptOptions;
11
12
  const logger_1 = require("./logger");
12
13
  const logger = (0, logger_1.createLogger)('cli-patterns');
13
14
  /**
@@ -23,8 +24,14 @@ exports.CLAUDE_SPINNER_CHARS = [
23
24
  * Claude thinking pattern
24
25
  * Matches spinner character followed by activity text ending with …
25
26
  * The text can contain spaces (e.g., "Verifying implementation (dead code detection)…")
27
+ *
28
+ * Alternative 2: "esc to interrupt" status bar text (Issue #XXX)
29
+ * Claude Code shows "esc to interrupt" in the terminal status bar during active processing.
30
+ * Previous pattern required closing paren `to interrupt\)` matching `(esc to interrupt)`,
31
+ * but Claude Code v2.x status bar format uses `· esc to interrupt ·` without parens.
32
+ * Updated to match `esc to interrupt` which covers both formats.
26
33
  */
27
- exports.CLAUDE_THINKING_PATTERN = new RegExp(`[${exports.CLAUDE_SPINNER_CHARS.join('')}]\\s+.+…|to interrupt\\)`, 'm');
34
+ exports.CLAUDE_THINKING_PATTERN = new RegExp(`[${exports.CLAUDE_SPINNER_CHARS.join('')}]\\s+.+…|esc to interrupt`, 'm');
28
35
  /**
29
36
  * Codex thinking pattern
30
37
  * Matches activity indicators like "• Planning", "• Searching", etc.
@@ -45,6 +52,19 @@ exports.CLAUDE_PROMPT_PATTERN = /^[>❯](\s*$|\s+\S)/m;
45
52
  * Claude separator pattern
46
53
  */
47
54
  exports.CLAUDE_SEPARATOR_PATTERN = /^─{10,}$/m;
55
+ /**
56
+ * Claude trust dialog pattern (Issue #201)
57
+ *
58
+ * Matches the "Quick safety check" dialog displayed by Claude CLI v2.x
59
+ * when accessing a workspace for the first time.
60
+ *
61
+ * Intentionally uses partial matching (no line-start anchor ^):
62
+ * Other pattern constants (CLAUDE_PROMPT_PATTERN, CLAUDE_SEPARATOR_PATTERN, etc.)
63
+ * use line-start anchors (^), but this pattern needs to match at any position
64
+ * within the tmux output buffer because the dialog text may appear after
65
+ * tmux padding or other output. (SF-001)
66
+ */
67
+ exports.CLAUDE_TRUST_DIALOG_PATTERN = /Yes, I trust this folder/m;
48
68
  /**
49
69
  * Codex prompt pattern
50
70
  * T1.2: Improved to detect empty prompts as well
@@ -54,6 +74,44 @@ exports.CODEX_PROMPT_PATTERN = /^›\s*/m;
54
74
  * Codex separator pattern
55
75
  */
56
76
  exports.CODEX_SEPARATOR_PATTERN = /^─.*Worked for.*─+$/m;
77
+ /**
78
+ * Pasted text pattern
79
+ *
80
+ * Claude CLI displays this when it detects multi-line text paste in the
81
+ * ink-based TextInput. The pattern matches the folded display format.
82
+ *
83
+ * @example "[Pasted text #1 +46 lines]"
84
+ * @see Issue #212, #163
85
+ * @designNote PASTE-001: Pattern matches the start of the indicator only.
86
+ * The line count (+XX lines) is variable, so we match the fixed prefix
87
+ * to minimize false negatives. False positive risk is low because
88
+ * "[Pasted text #" is a unique format generated by Claude CLI's ink renderer.
89
+ * @designNote PASTE-001-FP (SF-S4-002): When used in skipPatterns,
90
+ * line-level matching could filter legitimate response lines if Claude's
91
+ * answer text happens to contain "[Pasted text #". This is unlikely and
92
+ * acceptable -- only the affected line would be lost.
93
+ */
94
+ exports.PASTED_TEXT_PATTERN = /\[Pasted text #\d+/;
95
+ /**
96
+ * Pasted text detection delay (milliseconds)
97
+ *
98
+ * Wait time after sendKeys for tmux buffer to reflect [Pasted text] display.
99
+ *
100
+ * @see Issue #212
101
+ * @designNote PASTE-002: 500ms is the empirically measured time for
102
+ * Claude CLI's ink rendering to complete. capturePane({ startLine: -10 })
103
+ * reads only the last 10 lines since [Pasted text] appears in the most
104
+ * recent few lines.
105
+ */
106
+ exports.PASTED_TEXT_DETECT_DELAY = 500;
107
+ /**
108
+ * Pasted text detection max retries
109
+ *
110
+ * @see Issue #212
111
+ * @designNote PASTE-003: 3 retries x 500ms = max 1500ms additional delay.
112
+ * Typically resolves on the first attempt (+500ms).
113
+ */
114
+ exports.MAX_PASTED_TEXT_RETRIES = 3;
57
115
  /**
58
116
  * Gemini shell prompt pattern
59
117
  */
@@ -100,6 +158,7 @@ function getCliToolPatterns(cliToolId) {
100
158
  /^\s*Tip:/, // Tip lines
101
159
  /^\s*\?\s*for shortcuts/, // Shortcuts hint
102
160
  /to interrupt\)/, // Part of "esc to interrupt" message
161
+ exports.PASTED_TEXT_PATTERN, // [Pasted text #N +XX lines] (Issue #212)
103
162
  ],
104
163
  };
105
164
  case 'codex':
@@ -121,6 +180,7 @@ function getCliToolPatterns(cliToolId) {
121
180
  /^\s*└/, // Tree output (completion indicator)
122
181
  /^\s*│/, // Continuation lines
123
182
  /\(.*esc to interrupt\)/, // Interrupt hint
183
+ exports.PASTED_TEXT_PATTERN, // [Pasted text #N +XX lines] (Issue #212, defensive)
124
184
  ],
125
185
  };
126
186
  case 'gemini':
@@ -140,10 +200,46 @@ function getCliToolPatterns(cliToolId) {
140
200
  }
141
201
  }
142
202
  /**
143
- * Strip ANSI escape codes from a string
144
- * Optimized version at module level for performance
203
+ * Strip ANSI escape codes from a string.
204
+ * Optimized version at module level for performance.
205
+ *
206
+ * Covers:
207
+ * - SGR sequences: ESC[Nm (colors, bold, underline, etc.)
208
+ * - OSC sequences: ESC]...BEL (window title, hyperlinks, etc.)
209
+ * - CSI sequences: ESC[...letter (cursor movement, erase, etc.)
210
+ *
211
+ * Known limitations (SEC-002):
212
+ * - 8-bit CSI (0x9B): C1 control code form of CSI is not covered
213
+ * - DEC private modes: ESC[?25h and similar are not covered
214
+ * - Character set switching: ESC(0, ESC(B are not covered
215
+ * - Some RGB color forms: ESC[38;2;r;g;bm may not be fully matched
216
+ *
217
+ * In practice, tmux capture-pane output rarely contains these sequences,
218
+ * so the risk is low. Future consideration: adopt the `strip-ansi` npm package
219
+ * for more comprehensive coverage.
145
220
  */
146
221
  const ANSI_PATTERN = /\x1b\[[0-9;]*[a-zA-Z]|\x1b\][^\x07]*\x07|\[[0-9;]*m/g;
147
222
  function stripAnsi(str) {
148
223
  return str.replace(ANSI_PATTERN, '');
149
224
  }
225
+ /**
226
+ * Build DetectPromptOptions for a given CLI tool.
227
+ * Centralizes cliToolId-to-options mapping logic (DRY - MF-001).
228
+ *
229
+ * prompt-detector.ts remains CLI tool independent (Issue #161 principle);
230
+ * this function lives in cli-patterns.ts which already depends on CLIToolType.
231
+ *
232
+ * [Future extension memo (C-002)]
233
+ * If CLI tool count grows significantly (currently 3), consider migrating
234
+ * to a CLIToolConfig registry pattern where tool-specific settings
235
+ * (including promptDetectionOptions) are managed in a Record<CLIToolType, CLIToolConfig>.
236
+ *
237
+ * @param cliToolId - CLI tool identifier
238
+ * @returns DetectPromptOptions for the tool, or undefined for default behavior
239
+ */
240
+ function buildDetectPromptOptions(cliToolId) {
241
+ if (cliToolId === 'claude') {
242
+ return { requireDefaultIndicator: false };
243
+ }
244
+ return undefined; // Default behavior (requireDefaultIndicator = true)
245
+ }
@@ -9,7 +9,15 @@ const base_1 = require("./base");
9
9
  const tmux_1 = require("../tmux");
10
10
  const child_process_1 = require("child_process");
11
11
  const util_1 = require("util");
12
+ const pasted_text_helper_1 = require("../pasted-text-helper");
12
13
  const execAsync = (0, util_1.promisify)(child_process_1.exec);
14
+ /**
15
+ * Extract error message from unknown error type (DRY)
16
+ * Same pattern as claude-session.ts getErrorMessage()
17
+ */
18
+ function getErrorMessage(error) {
19
+ return error instanceof Error ? error.message : String(error);
20
+ }
13
21
  /**
14
22
  * Codex initialization timing constants
15
23
  * T2.6: Extracted as constants for maintainability
@@ -79,7 +87,7 @@ class CodexTool extends base_1.BaseCLITool {
79
87
  console.log(`✓ Started Codex session: ${sessionName}`);
80
88
  }
81
89
  catch (error) {
82
- const errorMessage = error instanceof Error ? error.message : String(error);
90
+ const errorMessage = getErrorMessage(error);
83
91
  throw new Error(`Failed to start Codex session: ${errorMessage}`);
84
92
  }
85
93
  }
@@ -105,10 +113,15 @@ class CodexTool extends base_1.BaseCLITool {
105
113
  await execAsync(`tmux send-keys -t "${sessionName}" C-m`);
106
114
  // Wait a moment for the message to be processed
107
115
  await new Promise((resolve) => setTimeout(resolve, 200));
116
+ // Issue #212: Detect [Pasted text] and resend Enter for multi-line messages
117
+ // MF-001: Single-line messages skip detection (+0ms overhead)
118
+ if (message.includes('\n')) {
119
+ await (0, pasted_text_helper_1.detectAndResendIfPastedText)(sessionName);
120
+ }
108
121
  console.log(`✓ Sent message to Codex session: ${sessionName}`);
109
122
  }
110
123
  catch (error) {
111
- const errorMessage = error instanceof Error ? error.message : String(error);
124
+ const errorMessage = getErrorMessage(error);
112
125
  throw new Error(`Failed to send message to Codex: ${errorMessage}`);
113
126
  }
114
127
  }
@@ -135,7 +148,7 @@ class CodexTool extends base_1.BaseCLITool {
135
148
  }
136
149
  }
137
150
  catch (error) {
138
- const errorMessage = error instanceof Error ? error.message : String(error);
151
+ const errorMessage = getErrorMessage(error);
139
152
  console.error(`Error stopping Codex session: ${errorMessage}`);
140
153
  throw error;
141
154
  }