commandmate 0.1.12 → 0.2.1

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 (163) hide show
  1. package/.env.example +4 -9
  2. package/.next/BUILD_ID +1 -1
  3. package/.next/app-build-manifest.json +24 -24
  4. package/.next/app-path-routes-manifest.json +1 -1
  5. package/.next/build-manifest.json +7 -7
  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/0.pack +0 -0
  14. package/.next/cache/webpack/edge-server-production/index.pack +0 -0
  15. package/.next/cache/webpack/server-production/0.pack +0 -0
  16. package/.next/cache/webpack/server-production/index.pack +0 -0
  17. package/.next/next-server.js.nft.json +1 -1
  18. package/.next/prerender-manifest.json +1 -1
  19. package/.next/react-loadable-manifest.json +7 -7
  20. package/.next/required-server-files.json +1 -1
  21. package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  22. package/.next/server/app/_not-found.html +1 -1
  23. package/.next/server/app/_not-found.rsc +2 -2
  24. package/.next/server/app/api/hooks/claude-done/route.js +1 -19
  25. package/.next/server/app/api/hooks/claude-done/route.js.nft.json +1 -1
  26. package/.next/server/app/api/repositories/clone/[jobId]/route.js +1 -1
  27. package/.next/server/app/api/repositories/clone/[jobId]/route.js.nft.json +1 -1
  28. package/.next/server/app/api/repositories/clone/route.js +1 -1
  29. package/.next/server/app/api/repositories/clone/route.js.nft.json +1 -1
  30. package/.next/server/app/api/repositories/excluded/route.js +36 -0
  31. package/.next/server/app/api/repositories/excluded/route.js.nft.json +1 -0
  32. package/.next/server/app/api/repositories/excluded.body +1 -0
  33. package/.next/server/app/api/repositories/excluded.meta +1 -0
  34. package/.next/server/app/api/repositories/restore/route.js +36 -0
  35. package/.next/server/app/api/repositories/restore/route.js.nft.json +1 -0
  36. package/.next/server/app/api/repositories/route.js +36 -1
  37. package/.next/server/app/api/repositories/route.js.nft.json +1 -1
  38. package/.next/server/app/api/repositories/scan/route.js +1 -1
  39. package/.next/server/app/api/repositories/sync/route.js +36 -1
  40. package/.next/server/app/api/slash-commands/route.js +1 -1
  41. package/.next/server/app/api/slash-commands.body +1 -1
  42. package/.next/server/app/api/worktrees/[id]/auto-yes/route.js +1 -1
  43. package/.next/server/app/api/worktrees/[id]/auto-yes/route.js.nft.json +1 -1
  44. package/.next/server/app/api/worktrees/[id]/capture/route.js +2 -2
  45. package/.next/server/app/api/worktrees/[id]/current-output/route.js +1 -1
  46. package/.next/server/app/api/worktrees/[id]/current-output/route.js.nft.json +1 -1
  47. package/.next/server/app/api/worktrees/[id]/interrupt/route.js +1 -1
  48. package/.next/server/app/api/worktrees/[id]/interrupt/route.js.nft.json +1 -1
  49. package/.next/server/app/api/worktrees/[id]/kill-session/route.js +1 -1
  50. package/.next/server/app/api/worktrees/[id]/kill-session/route.js.nft.json +1 -1
  51. package/.next/server/app/api/worktrees/[id]/logs/[filename]/route.js +1 -1
  52. package/.next/server/app/api/worktrees/[id]/logs/route.js +1 -1
  53. package/.next/server/app/api/worktrees/[id]/prompt-response/route.js +1 -1
  54. package/.next/server/app/api/worktrees/[id]/prompt-response/route.js.nft.json +1 -1
  55. package/.next/server/app/api/worktrees/[id]/respond/route.js +1 -1
  56. package/.next/server/app/api/worktrees/[id]/respond/route.js.nft.json +1 -1
  57. package/.next/server/app/api/worktrees/[id]/route.js +1 -1
  58. package/.next/server/app/api/worktrees/[id]/route.js.nft.json +1 -1
  59. package/.next/server/app/api/worktrees/[id]/search/route.js +1 -1
  60. package/.next/server/app/api/worktrees/[id]/send/route.js +1 -1
  61. package/.next/server/app/api/worktrees/[id]/send/route.js.nft.json +1 -1
  62. package/.next/server/app/api/worktrees/[id]/slash-commands/route.js +1 -1
  63. package/.next/server/app/api/worktrees/[id]/start-polling/route.js +1 -1
  64. package/.next/server/app/api/worktrees/[id]/start-polling/route.js.nft.json +1 -1
  65. package/.next/server/app/api/worktrees/[id]/terminal/route.js +1 -1
  66. package/.next/server/app/api/worktrees/route.js +1 -1
  67. package/.next/server/app/api/worktrees/route.js.nft.json +1 -1
  68. package/.next/server/app/index.html +2 -2
  69. package/.next/server/app/index.rsc +3 -3
  70. package/.next/server/app/page.js +7 -7
  71. package/.next/server/app/page.js.nft.json +1 -1
  72. package/.next/server/app/page_client-reference-manifest.js +1 -1
  73. package/.next/server/app/proxy/[...path]/route.js +2 -2
  74. package/.next/server/app/worktrees/[id]/files/[...path]/page_client-reference-manifest.js +1 -1
  75. package/.next/server/app/worktrees/[id]/page.js +4 -4
  76. package/.next/server/app/worktrees/[id]/page.js.nft.json +1 -1
  77. package/.next/server/app/worktrees/[id]/page_client-reference-manifest.js +1 -1
  78. package/.next/server/app/worktrees/[id]/simple-terminal/page_client-reference-manifest.js +1 -1
  79. package/.next/server/app/worktrees/[id]/terminal/page_client-reference-manifest.js +1 -1
  80. package/.next/server/app-paths-manifest.json +10 -8
  81. package/.next/server/chunks/5488.js +36 -0
  82. package/.next/server/chunks/6550.js +1 -1
  83. package/.next/server/chunks/7425.js +53 -50
  84. package/.next/server/chunks/7536.js +1 -0
  85. package/.next/server/chunks/8174.js +23 -0
  86. package/.next/server/chunks/9367.js +19 -0
  87. package/.next/server/middleware-build-manifest.js +1 -1
  88. package/.next/server/middleware-manifest.json +2 -28
  89. package/.next/server/middleware-react-loadable-manifest.js +1 -1
  90. package/.next/server/pages/404.html +1 -1
  91. package/.next/server/pages/500.html +1 -1
  92. package/.next/server/server-reference-manifest.json +1 -1
  93. package/.next/static/chunks/4327.740cc7fe2d0b5049.js +60 -0
  94. package/.next/static/chunks/4343-ebe884a2a80eb033.js +1 -0
  95. package/.next/static/chunks/6568-38a33aa67d82e12b.js +1 -0
  96. package/.next/static/chunks/816-c254f4e2406e696a.js +1 -0
  97. package/.next/static/chunks/app/layout-4804cfba519283cf.js +1 -0
  98. package/.next/static/chunks/app/page-3926224c4cdf315b.js +1 -0
  99. package/.next/static/chunks/app/worktrees/[id]/page-8bd88bdc29607413.js +1 -0
  100. package/.next/static/chunks/main-b6d727aa9248d4f2.js +1 -0
  101. package/.next/static/chunks/{webpack-3fc79fab9bb738d7.js → webpack-4f85dcef6279c6ee.js} +1 -1
  102. package/.next/static/css/28be35e4727ae7ef.css +3 -0
  103. package/.next/trace +5 -5
  104. package/.next/types/app/api/repositories/excluded/route.ts +343 -0
  105. package/.next/types/app/api/repositories/restore/route.ts +343 -0
  106. package/README.md +2 -2
  107. package/dist/cli/commands/init.d.ts.map +1 -1
  108. package/dist/cli/commands/init.js +2 -13
  109. package/dist/cli/commands/start.d.ts.map +1 -1
  110. package/dist/cli/commands/start.js +3 -7
  111. package/dist/cli/config/security-messages.d.ts +11 -0
  112. package/dist/cli/config/security-messages.d.ts.map +1 -0
  113. package/dist/cli/config/security-messages.js +29 -0
  114. package/dist/cli/types/index.d.ts +0 -1
  115. package/dist/cli/types/index.d.ts.map +1 -1
  116. package/dist/cli/utils/daemon.d.ts.map +1 -1
  117. package/dist/cli/utils/daemon.js +3 -7
  118. package/dist/cli/utils/env-setup.d.ts +0 -4
  119. package/dist/cli/utils/env-setup.d.ts.map +1 -1
  120. package/dist/cli/utils/env-setup.js +0 -14
  121. package/dist/cli/utils/security-logger.d.ts.map +1 -1
  122. package/dist/cli/utils/security-logger.js +1 -2
  123. package/dist/server/server.js +25 -2
  124. package/dist/server/src/lib/auto-yes-manager.js +100 -11
  125. package/dist/server/src/lib/claude-poller.js +341 -0
  126. package/dist/server/src/lib/claude-session.js +48 -19
  127. package/dist/server/src/lib/cli-patterns.js +69 -6
  128. package/dist/server/src/lib/cli-tools/base.js +7 -1
  129. package/dist/server/src/lib/cli-tools/codex.js +14 -2
  130. package/dist/server/src/lib/cli-tools/manager.js +27 -0
  131. package/dist/server/src/lib/cli-tools/types.js +7 -0
  132. package/dist/server/src/lib/cli-tools/validation.js +41 -0
  133. package/dist/server/src/lib/db-repository.js +482 -0
  134. package/dist/server/src/lib/db.js +23 -0
  135. package/dist/server/src/lib/env.js +0 -17
  136. package/dist/server/src/lib/logger.js +0 -4
  137. package/dist/server/src/lib/prompt-detector.js +297 -109
  138. package/dist/server/src/lib/response-poller.js +73 -27
  139. package/dist/server/src/lib/tmux.js +48 -0
  140. package/dist/server/src/lib/ws-server.js +12 -1
  141. package/dist/server/src/types/sidebar.js +16 -31
  142. package/dist/server/src/types/slash-commands.js +2 -0
  143. package/package.json +1 -1
  144. package/.next/server/chunks/1318.js +0 -29
  145. package/.next/server/chunks/2597.js +0 -1
  146. package/.next/server/chunks/2648.js +0 -1
  147. package/.next/server/chunks/9703.js +0 -31
  148. package/.next/server/chunks/9723.js +0 -19
  149. package/.next/server/edge-runtime-webpack.js +0 -2
  150. package/.next/server/edge-runtime-webpack.js.map +0 -1
  151. package/.next/server/src/middleware.js +0 -14
  152. package/.next/server/src/middleware.js.map +0 -1
  153. package/.next/static/chunks/2853-d11a80b03c9a1640.js +0 -1
  154. package/.next/static/chunks/4327.3b84aa049900fdeb.js +0 -60
  155. package/.next/static/chunks/816-7e340dad784be28c.js +0 -1
  156. package/.next/static/chunks/9365-733d8c05712d2888.js +0 -1
  157. package/.next/static/chunks/app/layout-37e55f11dcc8b1bf.js +0 -1
  158. package/.next/static/chunks/app/page-fe35d61f14b90a51.js +0 -1
  159. package/.next/static/chunks/app/worktrees/[id]/page-58fcf2e63c056743.js +0 -1
  160. package/.next/static/chunks/main-a960f4a5e1a2f598.js +0 -1
  161. package/.next/static/css/376b339640084689.css +0 -3
  162. /package/.next/static/{564GHwluX5xIv9qpqLJV2 → oUD-A998xeBoez6zsrTH3}/_buildManifest.js +0 -0
  163. /package/.next/static/{564GHwluX5xIv9qpqLJV2 → oUD-A998xeBoez6zsrTH3}/_ssgManifest.js +0 -0
@@ -0,0 +1,341 @@
1
+ "use strict";
2
+ /**
3
+ * Claude response polling
4
+ * Periodically checks tmux sessions for Claude responses
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.startPolling = startPolling;
8
+ exports.stopPolling = stopPolling;
9
+ exports.stopAllPolling = stopAllPolling;
10
+ exports.getActivePollers = getActivePollers;
11
+ const claude_session_1 = require("./claude-session");
12
+ const db_instance_1 = require("./db-instance");
13
+ const db_1 = require("./db");
14
+ const ws_server_1 = require("./ws-server");
15
+ const prompt_detector_1 = require("./prompt-detector");
16
+ const conversation_logger_1 = require("./conversation-logger");
17
+ /**
18
+ * Polling interval in milliseconds (default: 2 seconds)
19
+ */
20
+ const POLLING_INTERVAL = 2000;
21
+ /**
22
+ * Maximum polling duration in milliseconds (default: 5 minutes)
23
+ */
24
+ const MAX_POLLING_DURATION = 5 * 60 * 1000;
25
+ /**
26
+ * Active pollers map: worktreeId -> NodeJS.Timeout
27
+ */
28
+ const activePollers = new Map();
29
+ /**
30
+ * Polling start times map: worktreeId -> timestamp
31
+ */
32
+ const pollingStartTimes = new Map();
33
+ /**
34
+ * Extract Claude response from tmux output
35
+ * Detects when Claude has completed a response by looking for the prompt
36
+ *
37
+ * @param output - Full tmux output
38
+ * @param lastCapturedLine - Number of lines previously captured
39
+ * @returns Extracted response or null if incomplete
40
+ */
41
+ function extractClaudeResponse(output, lastCapturedLine) {
42
+ // Trim trailing empty lines from the output before processing
43
+ // This prevents the "last 20 lines" from being all empty due to tmux buffer padding
44
+ const rawLines = output.split('\n');
45
+ let trimmedLength = rawLines.length;
46
+ while (trimmedLength > 0 && rawLines[trimmedLength - 1].trim() === '') {
47
+ trimmedLength--;
48
+ }
49
+ const lines = rawLines.slice(0, trimmedLength);
50
+ const totalLines = lines.length;
51
+ // No new output (with buffer to handle newline inconsistencies)
52
+ if (totalLines < lastCapturedLine - 5) {
53
+ return null;
54
+ }
55
+ // Always check the last 20 lines for completion pattern (more robust than tracking line numbers)
56
+ const checkLineCount = 20;
57
+ const startLine = Math.max(0, totalLines - checkLineCount);
58
+ const linesToCheck = lines.slice(startLine);
59
+ const outputToCheck = linesToCheck.join('\n');
60
+ // Check if Claude has returned to prompt (indicated by the prompt symbols)
61
+ // Claude shows "> " or "❯ " or "─────" when waiting for input
62
+ // Supports both legacy '>' and new '❯' (U+276F) prompt characters
63
+ // Issue #132: Also matches prompts with recommended commands (e.g., "❯ /work-plan")
64
+ const promptPattern = /^[>❯](\s*$|\s+\S)/m;
65
+ const separatorPattern = /^─{50,}$/m;
66
+ // Check for thinking/processing indicators
67
+ // Claude shows various animations while thinking: ✻ Herding…, · Choreographing…, ∴ Thinking…, ✢ Doing…, ✳ Cascading…, etc.
68
+ // Match lines that contain: symbol + word + … OR just the symbol alone
69
+ const thinkingPattern = /[✻✽⏺·∴✢✳]/m;
70
+ const hasPrompt = promptPattern.test(outputToCheck);
71
+ const hasSeparator = separatorPattern.test(outputToCheck);
72
+ const isThinking = thinkingPattern.test(outputToCheck);
73
+ // Only consider complete if we have prompt + separator AND Claude is NOT thinking
74
+ if (hasPrompt && hasSeparator && !isThinking) {
75
+ // Claude has completed response
76
+ // Extract the response content from lastCapturedLine to the separator (not just last 20 lines)
77
+ const responseLines = [];
78
+ // Handle tmux buffer scrolling: if lastCapturedLine >= totalLines, the buffer has scrolled
79
+ // In this case, we need to find the response in the current visible buffer
80
+ let startIndex;
81
+ if (lastCapturedLine >= totalLines - 5) {
82
+ // Buffer may have scrolled - look for the start of the new response
83
+ // Find the last user input prompt ("> ...") to identify where the response starts
84
+ let foundUserPrompt = -1;
85
+ for (let i = totalLines - 1; i >= Math.max(0, totalLines - 50); i--) {
86
+ // Look for user input line (starts with "> " or "❯ " followed by content)
87
+ if (/^[>❯]\s+\S/.test(lines[i])) {
88
+ foundUserPrompt = i;
89
+ break;
90
+ }
91
+ }
92
+ // Start extraction from after the user prompt, or from a safe earlier point
93
+ startIndex = foundUserPrompt >= 0 ? foundUserPrompt + 1 : Math.max(0, totalLines - 40);
94
+ }
95
+ else {
96
+ // Normal case: start from lastCapturedLine
97
+ startIndex = Math.max(0, lastCapturedLine);
98
+ }
99
+ for (let i = startIndex; i < totalLines; i++) {
100
+ const line = lines[i];
101
+ // Skip separator lines
102
+ if (/^─{50,}$/.test(line)) {
103
+ continue;
104
+ }
105
+ // Stop at new prompt (supports both '>' and '❯')
106
+ if (/^[>❯]\s*$/.test(line)) {
107
+ break;
108
+ }
109
+ // Skip thinking/processing status lines (spinner char + activity text ending with …)
110
+ // Note: ⏺ is also used as a response marker, so we only skip if it looks like a thinking line
111
+ // Thinking line example: "✳ UIからジョブ再実行中… (esc to interrupt · 33m 44s · thinking)"
112
+ // Response line example: "⏺ 何かお手伝いできることはありますか?" (should NOT be skipped)
113
+ if (/[✻✽·∴✢✳⦿◉●○◌◎⊙⊚⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏]\s*\S+…/.test(line) || /to interrupt\)/.test(line)) {
114
+ continue;
115
+ }
116
+ // Skip tip lines and decoration lines
117
+ if (/^\s*[⎿⏋]\s+Tip:/.test(line) || /^\s*Tip:/.test(line)) {
118
+ continue;
119
+ }
120
+ responseLines.push(line);
121
+ }
122
+ const response = responseLines.join('\n').trim();
123
+ // Additional check: ensure response doesn't contain thinking indicators
124
+ // This prevents saving intermediate states as final responses
125
+ if (thinkingPattern.test(response)) {
126
+ console.warn(`[Poller] Response contains thinking indicators, treating as incomplete`);
127
+ return {
128
+ response: '',
129
+ isComplete: false,
130
+ lineCount: totalLines,
131
+ };
132
+ }
133
+ return {
134
+ response,
135
+ isComplete: true,
136
+ lineCount: totalLines,
137
+ };
138
+ }
139
+ // Check if this is an interactive prompt (yes/no or multiple choice)
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.
143
+ if (!isThinking) {
144
+ const fullOutput = lines.join('\n');
145
+ const promptDetection = (0, prompt_detector_1.detectPrompt)(fullOutput);
146
+ if (promptDetection.isPrompt) {
147
+ // This is an interactive prompt - consider it complete
148
+ return {
149
+ response: fullOutput,
150
+ isComplete: true,
151
+ lineCount: totalLines,
152
+ };
153
+ }
154
+ }
155
+ // Response not yet complete
156
+ return {
157
+ response: '',
158
+ isComplete: false,
159
+ lineCount: totalLines,
160
+ };
161
+ }
162
+ /**
163
+ * Check for Claude response once
164
+ *
165
+ * @param worktreeId - Worktree ID
166
+ * @returns True if response was found and processed
167
+ */
168
+ async function checkForResponse(worktreeId) {
169
+ const db = (0, db_instance_1.getDbInstance)();
170
+ try {
171
+ // Get worktree to retrieve CLI tool ID
172
+ const worktree = (0, db_1.getWorktreeById)(db, worktreeId);
173
+ if (!worktree) {
174
+ console.error(`Worktree ${worktreeId} not found, stopping poller`);
175
+ stopPolling(worktreeId);
176
+ return false;
177
+ }
178
+ const cliToolId = worktree.cliToolId || 'claude';
179
+ // Check if Claude session is running
180
+ const running = await (0, claude_session_1.isClaudeRunning)(worktreeId);
181
+ if (!running) {
182
+ console.log(`Claude session not running for ${worktreeId}, stopping poller`);
183
+ stopPolling(worktreeId);
184
+ return false;
185
+ }
186
+ // Get session state (last captured line count)
187
+ const sessionState = (0, db_1.getSessionState)(db, worktreeId, cliToolId);
188
+ const lastCapturedLine = sessionState?.lastCapturedLine || 0;
189
+ // Capture current output
190
+ const output = await (0, claude_session_1.captureClaudeOutput)(worktreeId, 10000);
191
+ // Extract response
192
+ const result = extractClaudeResponse(output, lastCapturedLine);
193
+ if (!result) {
194
+ // No new output
195
+ return false;
196
+ }
197
+ if (!result.isComplete) {
198
+ return false;
199
+ }
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.
203
+ const promptDetection = (0, prompt_detector_1.detectPrompt)(result.response);
204
+ if (promptDetection.isPrompt) {
205
+ // This is a prompt - save as prompt message
206
+ console.log(`✓ Detected prompt for ${worktreeId}:`, promptDetection.promptData?.question);
207
+ const message = (0, db_1.createMessage)(db, {
208
+ worktreeId,
209
+ role: 'assistant',
210
+ content: promptDetection.cleanContent,
211
+ messageType: 'prompt',
212
+ promptData: promptDetection.promptData,
213
+ timestamp: new Date(),
214
+ cliToolId,
215
+ });
216
+ // Update session state
217
+ (0, db_1.updateSessionState)(db, worktreeId, cliToolId, result.lineCount);
218
+ // Broadcast to WebSocket
219
+ (0, ws_server_1.broadcastMessage)('message', {
220
+ worktreeId,
221
+ message,
222
+ });
223
+ console.log(`✓ Saved prompt message for ${worktreeId}`);
224
+ // Stop polling - waiting for user response
225
+ stopPolling(worktreeId);
226
+ return true;
227
+ }
228
+ // Normal response (not a prompt)
229
+ console.log(`✓ Detected Claude response for ${worktreeId}`);
230
+ // Validate response content is not empty
231
+ if (!result.response || result.response.trim() === '') {
232
+ console.warn(`⚠ Empty response detected for ${worktreeId}, continuing polling...`);
233
+ // Update session state but don't save the message
234
+ // Continue polling in case a prompt appears next
235
+ (0, db_1.updateSessionState)(db, worktreeId, cliToolId, result.lineCount);
236
+ return false;
237
+ }
238
+ // Create Markdown log file for the conversation pair
239
+ if (result.response) {
240
+ await (0, conversation_logger_1.recordClaudeConversation)(db, worktreeId, result.response, 'claude');
241
+ }
242
+ // Create Claude message in database
243
+ const message = (0, db_1.createMessage)(db, {
244
+ worktreeId,
245
+ role: 'assistant',
246
+ content: result.response,
247
+ messageType: 'normal',
248
+ timestamp: new Date(),
249
+ cliToolId,
250
+ });
251
+ // Update session state
252
+ (0, db_1.updateSessionState)(db, worktreeId, cliToolId, result.lineCount);
253
+ // Broadcast message to WebSocket clients
254
+ (0, ws_server_1.broadcastMessage)('message', {
255
+ worktreeId,
256
+ message,
257
+ });
258
+ console.log(`✓ Saved Claude response for ${worktreeId}`);
259
+ // Stop polling since we got the response
260
+ stopPolling(worktreeId);
261
+ return true;
262
+ }
263
+ catch (error) {
264
+ const errorMessage = error instanceof Error ? error.message : String(error);
265
+ console.error(`Error checking for response (${worktreeId}):`, errorMessage);
266
+ return false;
267
+ }
268
+ }
269
+ /**
270
+ * Start polling for Claude response
271
+ *
272
+ * @param worktreeId - Worktree ID
273
+ *
274
+ * @example
275
+ * ```typescript
276
+ * startPolling('feature-foo');
277
+ * ```
278
+ */
279
+ function startPolling(worktreeId) {
280
+ // Stop existing poller if any
281
+ stopPolling(worktreeId);
282
+ console.log(`Starting poller for ${worktreeId}`);
283
+ // Record start time
284
+ pollingStartTimes.set(worktreeId, Date.now());
285
+ // Start polling
286
+ const interval = setInterval(async () => {
287
+ console.log(`[Poller] Checking for response: ${worktreeId}`);
288
+ const startTime = pollingStartTimes.get(worktreeId);
289
+ // Check if max duration exceeded
290
+ if (startTime && Date.now() - startTime > MAX_POLLING_DURATION) {
291
+ console.log(`Polling timeout for ${worktreeId}, stopping`);
292
+ stopPolling(worktreeId);
293
+ return;
294
+ }
295
+ // Check for response
296
+ try {
297
+ await checkForResponse(worktreeId);
298
+ }
299
+ catch (error) {
300
+ console.error(`[Poller] Error in checkForResponse:`, error);
301
+ }
302
+ }, POLLING_INTERVAL);
303
+ activePollers.set(worktreeId, interval);
304
+ }
305
+ /**
306
+ * Stop polling for a worktree
307
+ *
308
+ * @param worktreeId - Worktree ID
309
+ *
310
+ * @example
311
+ * ```typescript
312
+ * stopPolling('feature-foo');
313
+ * ```
314
+ */
315
+ function stopPolling(worktreeId) {
316
+ const interval = activePollers.get(worktreeId);
317
+ if (interval) {
318
+ clearInterval(interval);
319
+ activePollers.delete(worktreeId);
320
+ pollingStartTimes.delete(worktreeId);
321
+ console.log(`Stopped poller for ${worktreeId}`);
322
+ }
323
+ }
324
+ /**
325
+ * Stop all active pollers
326
+ * Used for cleanup on server shutdown
327
+ */
328
+ function stopAllPolling() {
329
+ console.log(`Stopping all pollers (${activePollers.size} active)`);
330
+ for (const worktreeId of activePollers.keys()) {
331
+ stopPolling(worktreeId);
332
+ }
333
+ }
334
+ /**
335
+ * Get list of active pollers
336
+ *
337
+ * @returns Array of worktree IDs currently being polled
338
+ */
339
+ function getActivePollers() {
340
+ return Array.from(activePollers.keys());
341
+ }
@@ -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;
@@ -42,6 +42,9 @@ function getErrorMessage(error) {
42
42
  * - Authenticate with Anthropic servers (if needed)
43
43
  * - Display the interactive prompt
44
44
  *
45
+ * This timeout also covers trust dialog auto-response time (typically <1s).
46
+ * When reducing this value, consider dialog response overhead.
47
+ *
45
48
  * 15 seconds provides headroom for slower networks or cold starts.
46
49
  */
47
50
  exports.CLAUDE_INIT_TIMEOUT = 15000;
@@ -75,6 +78,24 @@ exports.CLAUDE_POST_PROMPT_DELAY = 500;
75
78
  * if it's still processing a previous request.
76
79
  */
77
80
  exports.CLAUDE_PROMPT_WAIT_TIMEOUT = 5000;
81
+ /**
82
+ * Prompt wait timeout before message send (milliseconds).
83
+ *
84
+ * Used exclusively by sendMessageToClaude() to limit how long we wait
85
+ * for Claude to return to a prompt state before sending a user message.
86
+ * This is separate from CLAUDE_PROMPT_WAIT_TIMEOUT (5000ms, the default
87
+ * for waitForPrompt()) because sendMessageToClaude() may be called
88
+ * shortly after session initialization, where Claude CLI needs additional
89
+ * time to become ready.
90
+ *
91
+ * Relationship to other timeout constants:
92
+ * - CLAUDE_PROMPT_WAIT_TIMEOUT (5000ms): Default for waitForPrompt()
93
+ * - CLAUDE_SEND_PROMPT_WAIT_TIMEOUT (10000ms): sendMessageToClaude() specific
94
+ * - CLAUDE_INIT_TIMEOUT (15000ms): Session initialization timeout
95
+ *
96
+ * @see Issue #187 - Constant unification for sendMessageToClaude timeout
97
+ */
98
+ exports.CLAUDE_SEND_PROMPT_WAIT_TIMEOUT = 10000;
78
99
  /**
79
100
  * Prompt wait polling interval (milliseconds)
80
101
  *
@@ -227,7 +248,6 @@ async function waitForPrompt(sessionName, timeout = exports.CLAUDE_PROMPT_WAIT_T
227
248
  * await startClaudeSession({
228
249
  * worktreeId: 'feature-foo',
229
250
  * worktreePath: '/path/to/worktree',
230
- * baseUrl: 'http://localhost:3000',
231
251
  * });
232
252
  * ```
233
253
  */
@@ -262,21 +282,33 @@ async function startClaudeSession(options) {
262
282
  const pollInterval = exports.CLAUDE_INIT_POLL_INTERVAL;
263
283
  const startTime = Date.now();
264
284
  let initialized = false;
285
+ let trustDialogHandled = false;
265
286
  while (Date.now() - startTime < maxWaitTime) {
266
287
  await new Promise((resolve) => setTimeout(resolve, pollInterval));
267
288
  try {
268
289
  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
290
+ // Claude is ready when we see the prompt (DRY-001)
291
+ // Use CLAUDE_PROMPT_PATTERN from cli-patterns.ts for consistency
271
292
  // Strip ANSI escape sequences before pattern matching (Issue #152)
293
+ // Note: CLAUDE_SEPARATOR_PATTERN was removed from initialization check (Issue #187, P1-1)
294
+ // because separator early-detection caused premature returns before prompt was ready
272
295
  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)) {
296
+ if (cli_patterns_1.CLAUDE_PROMPT_PATTERN.test(cleanOutput)) {
274
297
  // Wait for stability after prompt detection (CONS-007, DOC-001)
275
298
  await new Promise((resolve) => setTimeout(resolve, exports.CLAUDE_POST_PROMPT_DELAY));
276
299
  console.log(`Claude initialized in ${Date.now() - startTime}ms`);
277
300
  initialized = true;
278
301
  break;
279
302
  }
303
+ // Issue #201: Detect trust dialog and auto-respond with Enter
304
+ // Condition order: CLAUDE_PROMPT_PATTERN (above) is checked first for shortest path
305
+ if (!trustDialogHandled && cli_patterns_1.CLAUDE_TRUST_DIALOG_PATTERN.test(cleanOutput)) {
306
+ await (0, tmux_1.sendKeys)(sessionName, '', true);
307
+ trustDialogHandled = true;
308
+ // TODO: Log output method unification (console.log vs createLogger) to be addressed in a separate Issue (SF-002)
309
+ console.log('Trust dialog detected, sending Enter to confirm');
310
+ // Continue polling to wait for prompt detection
311
+ }
280
312
  }
281
313
  catch {
282
314
  // Ignore capture errors during initialization
@@ -312,23 +344,21 @@ async function sendMessageToClaude(worktreeId, message) {
312
344
  throw new Error(`Claude session ${sessionName} does not exist. Start the session first.`);
313
345
  }
314
346
  // Verify prompt state before sending (CONS-006, DRY-001)
315
- // Use -50 lines to ensure we capture the prompt even with status bars
316
347
  const output = await (0, tmux_1.capturePane)(sessionName, { startLine: -50 });
317
- // Strip ANSI escape sequences before pattern matching (Issue #152)
318
348
  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
- }
349
+ // Path B: Prompt not detected - wait for it (P1: throw on timeout)
350
+ await waitForPrompt(sessionName, exports.CLAUDE_SEND_PROMPT_WAIT_TIMEOUT);
328
351
  }
352
+ // P0: Stability delay after prompt detection (both Path A and Path B)
353
+ // Same delay as startClaudeSession() to ensure Claude CLI input handler is ready
354
+ // NOTE: This 500ms delay also applies to 2nd+ messages. Currently acceptable since
355
+ // Claude CLI response time (seconds to tens of seconds) dwarfs this overhead.
356
+ // If future batch-send use cases arise, this could be optimized to first-message-only,
357
+ // but that optimization is deferred per YAGNI principle. (ref: F-3)
358
+ await new Promise((resolve) => setTimeout(resolve, exports.CLAUDE_POST_PROMPT_DELAY));
329
359
  // 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
360
+ await (0, tmux_1.sendKeys)(sessionName, message, false);
361
+ await (0, tmux_1.sendKeys)(sessionName, '', true);
332
362
  console.log(`Sent message to Claude session: ${sessionName}`);
333
363
  }
334
364
  /**
@@ -403,7 +433,6 @@ async function stopClaudeSession(worktreeId) {
403
433
  * await restartClaudeSession({
404
434
  * worktreeId: 'feature-foo',
405
435
  * worktreePath: '/path/to/worktree',
406
- * baseUrl: 'http://localhost:3000',
407
436
  * });
408
437
  * ```
409
438
  */
@@ -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.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,13 +24,20 @@ 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.
38
+ * T1.1: Extended to include "Ran" and "Deciding"
31
39
  */
32
- exports.CODEX_THINKING_PATTERN = /•\s*(Planning|Searching|Exploring|Running|Thinking|Working|Reading|Writing|Analyzing)/m;
40
+ exports.CODEX_THINKING_PATTERN = /•\s*(Planning|Searching|Exploring|Running|Thinking|Working|Reading|Writing|Analyzing|Ran|Deciding)/m;
33
41
  /**
34
42
  * Claude prompt pattern (waiting for input)
35
43
  * Supports both legacy '>' and new '❯' (U+276F) prompt characters
@@ -44,10 +52,24 @@ exports.CLAUDE_PROMPT_PATTERN = /^[>❯](\s*$|\s+\S)/m;
44
52
  * Claude separator pattern
45
53
  */
46
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;
47
68
  /**
48
69
  * Codex prompt pattern
70
+ * T1.2: Improved to detect empty prompts as well
49
71
  */
50
- exports.CODEX_PROMPT_PATTERN = /^›\s+.+/m;
72
+ exports.CODEX_PROMPT_PATTERN = /^›\s*/m;
51
73
  /**
52
74
  * Codex separator pattern
53
75
  */
@@ -114,6 +136,11 @@ function getCliToolPatterns(cliToolId) {
114
136
  /^\s*for shortcuts$/, // Shortcuts hint
115
137
  /╭─+╮/, // Box drawing (top)
116
138
  /╰─+╯/, // Box drawing (bottom)
139
+ // T1.3: Additional skip patterns for Codex
140
+ /•\s*Ran\s+/, // Command execution lines
141
+ /^\s*└/, // Tree output (completion indicator)
142
+ /^\s*│/, // Continuation lines
143
+ /\(.*esc to interrupt\)/, // Interrupt hint
117
144
  ],
118
145
  };
119
146
  case 'gemini':
@@ -133,10 +160,46 @@ function getCliToolPatterns(cliToolId) {
133
160
  }
134
161
  }
135
162
  /**
136
- * Strip ANSI escape codes from a string
137
- * Optimized version at module level for performance
163
+ * Strip ANSI escape codes from a string.
164
+ * Optimized version at module level for performance.
165
+ *
166
+ * Covers:
167
+ * - SGR sequences: ESC[Nm (colors, bold, underline, etc.)
168
+ * - OSC sequences: ESC]...BEL (window title, hyperlinks, etc.)
169
+ * - CSI sequences: ESC[...letter (cursor movement, erase, etc.)
170
+ *
171
+ * Known limitations (SEC-002):
172
+ * - 8-bit CSI (0x9B): C1 control code form of CSI is not covered
173
+ * - DEC private modes: ESC[?25h and similar are not covered
174
+ * - Character set switching: ESC(0, ESC(B are not covered
175
+ * - Some RGB color forms: ESC[38;2;r;g;bm may not be fully matched
176
+ *
177
+ * In practice, tmux capture-pane output rarely contains these sequences,
178
+ * so the risk is low. Future consideration: adopt the `strip-ansi` npm package
179
+ * for more comprehensive coverage.
138
180
  */
139
181
  const ANSI_PATTERN = /\x1b\[[0-9;]*[a-zA-Z]|\x1b\][^\x07]*\x07|\[[0-9;]*m/g;
140
182
  function stripAnsi(str) {
141
183
  return str.replace(ANSI_PATTERN, '');
142
184
  }
185
+ /**
186
+ * Build DetectPromptOptions for a given CLI tool.
187
+ * Centralizes cliToolId-to-options mapping logic (DRY - MF-001).
188
+ *
189
+ * prompt-detector.ts remains CLI tool independent (Issue #161 principle);
190
+ * this function lives in cli-patterns.ts which already depends on CLIToolType.
191
+ *
192
+ * [Future extension memo (C-002)]
193
+ * If CLI tool count grows significantly (currently 3), consider migrating
194
+ * to a CLIToolConfig registry pattern where tool-specific settings
195
+ * (including promptDetectionOptions) are managed in a Record<CLIToolType, CLIToolConfig>.
196
+ *
197
+ * @param cliToolId - CLI tool identifier
198
+ * @returns DetectPromptOptions for the tool, or undefined for default behavior
199
+ */
200
+ function buildDetectPromptOptions(cliToolId) {
201
+ if (cliToolId === 'claude') {
202
+ return { requireDefaultIndicator: false };
203
+ }
204
+ return undefined; // Default behavior (requireDefaultIndicator = true)
205
+ }
@@ -7,6 +7,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
7
7
  exports.BaseCLITool = void 0;
8
8
  const child_process_1 = require("child_process");
9
9
  const util_1 = require("util");
10
+ const validation_1 = require("./validation");
10
11
  const tmux_1 = require("../tmux");
11
12
  const execAsync = (0, util_1.promisify)(child_process_1.exec);
12
13
  /**
@@ -31,11 +32,16 @@ class BaseCLITool {
31
32
  * Generate session name for a worktree
32
33
  * Format: mcbd-{cli_tool_id}-{worktree_id}
33
34
  *
35
+ * T2.3: Added validation to prevent command injection (MF4-001)
36
+ *
34
37
  * @param worktreeId - Worktree ID
35
38
  * @returns Session name
39
+ * @throws Error if the resulting session name is invalid
36
40
  */
37
41
  getSessionName(worktreeId) {
38
- return `mcbd-${this.id}-${worktreeId}`;
42
+ const sessionName = `mcbd-${this.id}-${worktreeId}`;
43
+ (0, validation_1.validateSessionName)(sessionName);
44
+ return sessionName;
39
45
  }
40
46
  /**
41
47
  * Interrupt processing by sending Escape key
@@ -10,6 +10,12 @@ const tmux_1 = require("../tmux");
10
10
  const child_process_1 = require("child_process");
11
11
  const util_1 = require("util");
12
12
  const execAsync = (0, util_1.promisify)(child_process_1.exec);
13
+ /**
14
+ * Codex initialization timing constants
15
+ * T2.6: Extracted as constants for maintainability
16
+ */
17
+ const CODEX_INIT_WAIT_MS = 3000; // Wait for Codex to start
18
+ const CODEX_MODEL_SELECT_WAIT_MS = 200; // Wait between model selection keystrokes
13
19
  /**
14
20
  * Codex CLI tool implementation
15
21
  * Manages Codex sessions using tmux
@@ -59,11 +65,17 @@ class CodexTool extends base_1.BaseCLITool {
59
65
  // Start Codex CLI in interactive mode
60
66
  await (0, tmux_1.sendKeys)(sessionName, 'codex', true);
61
67
  // Wait for Codex to initialize (and potentially show update notification)
62
- await new Promise((resolve) => setTimeout(resolve, 3000));
68
+ await new Promise((resolve) => setTimeout(resolve, CODEX_INIT_WAIT_MS));
63
69
  // Auto-skip update notification if present (select option 2: Skip)
64
70
  await (0, tmux_1.sendKeys)(sessionName, '2', true);
65
71
  // Wait a moment for the selection to process
66
- await new Promise((resolve) => setTimeout(resolve, 500));
72
+ await new Promise((resolve) => setTimeout(resolve, CODEX_MODEL_SELECT_WAIT_MS));
73
+ // T2.6: Skip model selection dialog by sending Down arrow + Enter
74
+ // This selects the default model and proceeds to the prompt
75
+ await execAsync(`tmux send-keys -t "${sessionName}" Down`);
76
+ await new Promise((resolve) => setTimeout(resolve, CODEX_MODEL_SELECT_WAIT_MS));
77
+ await execAsync(`tmux send-keys -t "${sessionName}" Enter`);
78
+ await new Promise((resolve) => setTimeout(resolve, CODEX_MODEL_SELECT_WAIT_MS));
67
79
  console.log(`✓ Started Codex session: ${sessionName}`);
68
80
  }
69
81
  catch (error) {