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.
- package/.env.example +2 -1
- package/.next/BUILD_ID +1 -1
- package/.next/app-build-manifest.json +12 -12
- package/.next/app-path-routes-manifest.json +1 -1
- package/.next/build-manifest.json +2 -2
- package/.next/cache/.tsbuildinfo +1 -1
- package/.next/cache/config.json +3 -3
- package/.next/cache/webpack/client-production/0.pack +0 -0
- package/.next/cache/webpack/client-production/1.pack +0 -0
- package/.next/cache/webpack/client-production/2.pack +0 -0
- package/.next/cache/webpack/client-production/index.pack +0 -0
- package/.next/cache/webpack/client-production/index.pack.old +0 -0
- package/.next/cache/webpack/edge-server-production/index.pack +0 -0
- package/.next/cache/webpack/server-production/0.pack +0 -0
- package/.next/cache/webpack/server-production/index.pack +0 -0
- package/.next/next-minimal-server.js.nft.json +1 -1
- package/.next/next-server.js.nft.json +1 -1
- package/.next/prerender-manifest.json +1 -1
- package/.next/required-server-files.json +1 -1
- package/.next/routes-manifest.json +1 -1
- package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/server/app/api/app/update-check/route.js +1 -1
- package/.next/server/app/api/repositories/clone/[jobId]/route.js +1 -1
- package/.next/server/app/api/repositories/clone/route.js +1 -1
- package/.next/server/app/api/repositories/route.js +10 -10
- package/.next/server/app/api/repositories/route.js.nft.json +1 -1
- package/.next/server/app/api/worktrees/[id]/auto-yes/route.js +1 -1
- package/.next/server/app/api/worktrees/[id]/auto-yes/route.js.nft.json +1 -1
- package/.next/server/app/api/worktrees/[id]/current-output/route.js +1 -1
- package/.next/server/app/api/worktrees/[id]/current-output/route.js.nft.json +1 -1
- package/.next/server/app/api/worktrees/[id]/files/[...path]/route.js +1 -1
- package/.next/server/app/api/worktrees/[id]/prompt-response/route.js +1 -1
- package/.next/server/app/api/worktrees/[id]/search/route.js +1 -1
- package/.next/server/app/api/worktrees/[id]/upload/[...path]/route.js +1 -1
- package/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/server/app/worktrees/[id]/files/[...path]/page.js +1 -1
- package/.next/server/app/worktrees/[id]/files/[...path]/page_client-reference-manifest.js +1 -1
- package/.next/server/app/worktrees/[id]/page.js +3 -3
- package/.next/server/app/worktrees/[id]/page_client-reference-manifest.js +1 -1
- package/.next/server/app/worktrees/[id]/terminal/page_client-reference-manifest.js +1 -1
- package/.next/server/app-paths-manifest.json +5 -5
- package/.next/server/chunks/5488.js +4 -4
- package/.next/server/chunks/7536.js +1 -1
- package/.next/server/chunks/8693.js +1 -0
- package/.next/server/chunks/9238.js +14 -14
- package/.next/server/chunks/9367.js +2 -2
- package/.next/server/pages/500.html +1 -1
- package/.next/server/server-reference-manifest.json +1 -1
- package/.next/static/chunks/5970-2e18108d0cabd8af.js +1 -0
- package/.next/static/chunks/app/worktrees/[id]/files/[...path]/page-4a3c0861367e0391.js +1 -0
- package/.next/static/chunks/app/worktrees/[id]/page-dc0dde49ed95076f.js +1 -0
- package/.next/static/css/7c6675f6f65b4990.css +3 -0
- package/.next/trace +5 -5
- package/README.md +232 -46
- package/dist/server/src/lib/auto-yes-manager.js +124 -84
- package/dist/server/src/lib/claude-session.js +60 -21
- package/dist/server/src/lib/prompt-answer-sender.js +89 -0
- package/dist/server/src/lib/prompt-detector.js +9 -0
- package/dist/server/src/lib/prompt-key.js +30 -0
- package/package.json +12 -1
- package/.next/static/chunks/5970-9e999084275f2995.js +0 -1
- package/.next/static/chunks/app/worktrees/[id]/files/[...path]/page-ce9ac3658f2b7d91.js +0 -1
- package/.next/static/chunks/app/worktrees/[id]/page-5abc1e9b59430db1.js +0 -1
- package/.next/static/css/a69d9c70fce558b4.css +0 -3
- /package/.next/static/{5sR3jeFe8ymnzcEan6Rdt → ZTtC8-q8xrYUSilnXmMHl}/_buildManifest.js +0 -0
- /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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
*
|
|
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
|
|
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
|
|
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 #
|
|
265
|
-
//
|
|
266
|
-
//
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
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
|
-
|
|
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
|
-
},
|
|
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
|
|
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
|
-
|
|
265
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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.
|
|
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"
|