commandmate 0.2.6 → 0.2.8

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 (40) hide show
  1. package/.env.example +11 -0
  2. package/.next/BUILD_ID +1 -1
  3. package/.next/app-build-manifest.json +1 -1
  4. package/.next/app-path-routes-manifest.json +1 -1
  5. package/.next/build-manifest.json +2 -2
  6. package/.next/cache/.tsbuildinfo +1 -1
  7. package/.next/cache/config.json +3 -3
  8. package/.next/cache/webpack/client-production/0.pack +0 -0
  9. package/.next/cache/webpack/client-production/1.pack +0 -0
  10. package/.next/cache/webpack/client-production/2.pack +0 -0
  11. package/.next/cache/webpack/client-production/index.pack +0 -0
  12. package/.next/cache/webpack/client-production/index.pack.old +0 -0
  13. package/.next/cache/webpack/edge-server-production/index.pack +0 -0
  14. package/.next/cache/webpack/server-production/0.pack +0 -0
  15. package/.next/cache/webpack/server-production/index.pack +0 -0
  16. package/.next/next-server.js.nft.json +1 -1
  17. package/.next/prerender-manifest.json +1 -1
  18. package/.next/required-server-files.json +1 -1
  19. package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  20. package/.next/server/app/api/app/update-check/route.js +1 -1
  21. package/.next/server/app/page_client-reference-manifest.js +1 -1
  22. package/.next/server/app/worktrees/[id]/files/[...path]/page_client-reference-manifest.js +1 -1
  23. package/.next/server/app/worktrees/[id]/page.js +3 -3
  24. package/.next/server/app/worktrees/[id]/page_client-reference-manifest.js +1 -1
  25. package/.next/server/app/worktrees/[id]/terminal/page_client-reference-manifest.js +1 -1
  26. package/.next/server/app-paths-manifest.json +7 -7
  27. package/.next/server/chunks/9367.js +2 -2
  28. package/.next/server/pages/500.html +1 -1
  29. package/.next/server/server-reference-manifest.json +1 -1
  30. package/.next/static/chunks/app/worktrees/[id]/page-a836cda4ee0339c5.js +1 -0
  31. package/.next/trace +5 -5
  32. package/README.md +6 -0
  33. package/dist/cli/utils/daemon.d.ts.map +1 -1
  34. package/dist/cli/utils/daemon.js +3 -0
  35. package/dist/server/src/lib/claude-session.js +225 -27
  36. package/dist/server/src/lib/cli-patterns.js +34 -1
  37. package/package.json +1 -1
  38. package/.next/static/chunks/app/worktrees/[id]/page-c050d6ec20487340.js +0 -1
  39. /package/.next/static/{SO7fkeggQuqHqumZAt_QA → yrO0L8bN6ioX1pKYKzlmV}/_buildManifest.js +0 -0
  40. /package/.next/static/{SO7fkeggQuqHqumZAt_QA → yrO0L8bN6ioX1pKYKzlmV}/_ssgManifest.js +0 -0
package/README.md CHANGED
@@ -132,6 +132,12 @@ A: It supports Claude Code and Codex CLI. Thanks to the extensible Strategy patt
132
132
  **Q: Can multiple people use it?**
133
133
  A: Currently designed for individual use. Simultaneous multi-user access is not supported.
134
134
 
135
+ **Q: Session start fails after updating Claude CLI. What should I do?**
136
+ A: If you switch between npm and standalone versions of Claude CLI, the path may change. CommandMate will automatically detect the new path on the next session start attempt. If you want to specify a custom path, set the `CLAUDE_PATH` environment variable in `.env` (e.g., `CLAUDE_PATH=/opt/homebrew/bin/claude`).
137
+
138
+ **Q: Sessions fail to start when launching CommandMate from within Claude Code. Why?**
139
+ A: Claude Code sets the `CLAUDECODE=1` environment variable to prevent nested sessions. CommandMate automatically removes this variable from tmux sessions, so sessions should start normally. If the issue persists, manually run `tmux set-environment -g -u CLAUDECODE` to clear it from tmux's global environment.
140
+
135
141
  ## Documentation
136
142
 
137
143
  | Document | Description |
@@ -1 +1 @@
1
- {"version":3,"file":"daemon.d.ts","sourceRoot":"","sources":["../../../src/cli/utils/daemon.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAOtD;;GAEG;AACH,qBAAa,aAAa;IACxB,OAAO,CAAC,UAAU,CAAa;IAC/B,OAAO,CAAC,MAAM,CAAY;gBAEd,WAAW,EAAE,MAAM;IAK/B;;;;;OAKG;IACG,KAAK,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,MAAM,CAAC;IA6EnD;;;;OAIG;IACG,IAAI,CAAC,KAAK,GAAE,OAAe,GAAG,OAAO,CAAC,OAAO,CAAC;IA8BpD;;;OAGG;IACG,SAAS,IAAI,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC;IA6B/C;;OAEG;IACG,SAAS,IAAI,OAAO,CAAC,OAAO,CAAC;IAInC;;OAEG;YACW,WAAW;CAiB1B"}
1
+ {"version":3,"file":"daemon.d.ts","sourceRoot":"","sources":["../../../src/cli/utils/daemon.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAOtD;;GAEG;AACH,qBAAa,aAAa;IACxB,OAAO,CAAC,UAAU,CAAa;IAC/B,OAAO,CAAC,MAAM,CAAY;gBAEd,WAAW,EAAE,MAAM;IAK/B;;;;;OAKG;IACG,KAAK,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,MAAM,CAAC;IAiFnD;;;;OAIG;IACG,IAAI,CAAC,KAAK,GAAE,OAAe,GAAG,OAAO,CAAC,OAAO,CAAC;IA8BpD;;;OAGG;IACG,SAAS,IAAI,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC;IA6B/C;;OAEG;IACG,SAAS,IAAI,OAAO,CAAC,OAAO,CAAC;IAInC;;OAEG;YACW,WAAW;CAiB1B"}
@@ -53,6 +53,9 @@ class DaemonManager {
53
53
  ...process.env,
54
54
  ...(envResult.parsed || {}),
55
55
  };
56
+ // SF-003: Remove CLAUDECODE from env object to prevent nested session detection
57
+ // Uses env object (not process.env) to avoid global side effects (DIP)
58
+ delete env.CLAUDECODE;
56
59
  // Command line options override .env values
57
60
  if (options.port) {
58
61
  env.CM_PORT = String(options.port);
@@ -5,6 +5,7 @@
5
5
  */
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
+ exports.clearCachedClaudePath = clearCachedClaudePath;
8
9
  exports.getSessionName = getSessionName;
9
10
  exports.isClaudeInstalled = isClaudeInstalled;
10
11
  exports.isClaudeRunning = isClaudeRunning;
@@ -20,6 +21,7 @@ const cli_patterns_1 = require("./cli-patterns");
20
21
  const pasted_text_helper_1 = require("./pasted-text-helper");
21
22
  const child_process_1 = require("child_process");
22
23
  const util_1 = require("util");
24
+ const promises_1 = require("fs/promises");
23
25
  const execAsync = (0, util_1.promisify)(child_process_1.exec);
24
26
  // ----- Helper Functions -----
25
27
  /**
@@ -32,6 +34,24 @@ const execAsync = (0, util_1.promisify)(child_process_1.exec);
32
34
  function getErrorMessage(error) {
33
35
  return error instanceof Error ? error.message : String(error);
34
36
  }
37
+ // ----- Shell Prompt Detection Constants -----
38
+ /**
39
+ * Shell prompt ending characters for detecting shell-only tmux sessions
40
+ * Extensible array to support multiple shell types (MF-002: OCP)
41
+ * Placed in claude-session.ts as private constant (SF-S2-002: ISP - used only by isSessionHealthy())
42
+ * - '$': bash/sh default prompt
43
+ * - '%': zsh default prompt
44
+ * - '#': root prompt (bash/zsh)
45
+ *
46
+ * C-S2-002: False positive risk assessment:
47
+ * These characters are checked only at the END of trimmed output. This limits false
48
+ * positives to cases where Claude CLI output ends with one of these characters
49
+ * (e.g., output containing "$" at end of a code block). The risk is acceptable because:
50
+ * (1) Claude CLI output typically ends with a prompt (❯) or thinking indicator, not shell chars
51
+ * (2) A false positive triggers session recreation, which is a safe recovery action
52
+ * (3) The check is combined with error pattern detection for multiple signals
53
+ */
54
+ const SHELL_PROMPT_ENDINGS = ['$', '%', '#'];
35
55
  // ----- Timeout and Polling Constants (OCP-001) -----
36
56
  // These constants are exported to allow configuration and testing.
37
57
  // Changing these values affects Claude CLI session startup behavior.
@@ -108,19 +128,67 @@ exports.CLAUDE_PROMPT_POLL_INTERVAL = 200;
108
128
  * Cached Claude CLI path
109
129
  */
110
130
  let cachedClaudePath = null;
131
+ /**
132
+ * Clear cached Claude CLI path
133
+ * Called when session start fails to allow path re-resolution
134
+ * on next attempt (e.g., after CLI update or path change)
135
+ * @internal Exported for testing purposes only.
136
+ * Follows the same pattern as version-checker.ts resetCacheForTesting().
137
+ * Function name clearCachedClaudePath() is retained (without ForTesting suffix)
138
+ * because it is also called in production code (catch block), not only in tests.
139
+ * (SF-S2-005: Consistent @internal usage with version-checker.ts precedent)
140
+ */
141
+ function clearCachedClaudePath() {
142
+ cachedClaudePath = null;
143
+ }
144
+ /**
145
+ * Validate CLAUDE_PATH environment variable to prevent command injection
146
+ * SEC-MF-001: OWASP A03:2021 - Injection prevention
147
+ *
148
+ * @param claudePath - Value from process.env.CLAUDE_PATH
149
+ * @returns true if the path is safe to use
150
+ */
151
+ function isValidClaudePath(claudePath) {
152
+ // (1) Whitelist validation: only allow alphanumeric, path separators, dots, hyphens, underscores
153
+ // SEC-MF-001: Rejects shell metacharacters (;, |, &, $, `, newlines, spaces in dangerous positions, etc.)
154
+ const SAFE_PATH_PATTERN = /^[/a-zA-Z0-9._-]+$/;
155
+ if (!SAFE_PATH_PATTERN.test(claudePath)) {
156
+ console.log(`[claude-session] CLAUDE_PATH contains invalid characters, ignoring: ${claudePath.substring(0, 50)}`);
157
+ return false;
158
+ }
159
+ // (2) Path traversal prevention: reject ../ sequences
160
+ // SEC-MF-001: Prevents path traversal attacks
161
+ if (claudePath.includes('..')) {
162
+ console.log('[claude-session] CLAUDE_PATH contains path traversal sequence, ignoring');
163
+ return false;
164
+ }
165
+ return true;
166
+ }
111
167
  /**
112
168
  * Get Claude CLI path dynamically
113
169
  * Uses CLAUDE_PATH environment variable if set, otherwise finds via 'which'
170
+ * SEC-MF-001: Validates CLAUDE_PATH before caching
114
171
  */
115
172
  async function getClaudePath() {
116
173
  // Return cached path if available
117
174
  if (cachedClaudePath) {
118
175
  return cachedClaudePath;
119
176
  }
120
- // Check environment variable first
121
- if (process.env.CLAUDE_PATH) {
122
- cachedClaudePath = process.env.CLAUDE_PATH;
123
- return cachedClaudePath;
177
+ // Check environment variable first with validation (SEC-MF-001)
178
+ const envClaudePath = process.env.CLAUDE_PATH;
179
+ if (envClaudePath) {
180
+ if (isValidClaudePath(envClaudePath)) {
181
+ try {
182
+ await (0, promises_1.access)(envClaudePath, promises_1.constants.X_OK);
183
+ cachedClaudePath = envClaudePath;
184
+ return cachedClaudePath;
185
+ }
186
+ catch {
187
+ console.log(`[claude-session] CLAUDE_PATH is not executable: ${envClaudePath}`);
188
+ // Fall through to fallback paths
189
+ }
190
+ }
191
+ // If validation fails, ignore CLAUDE_PATH and proceed with fallback resolution
124
192
  }
125
193
  // Find claude via 'which' command
126
194
  try {
@@ -148,6 +216,111 @@ async function getClaudePath() {
148
216
  throw new Error('Claude CLI not found. Set CLAUDE_PATH environment variable or install Claude CLI.');
149
217
  }
150
218
  }
219
+ // ----- Common Helper Functions (SF-001) -----
220
+ /**
221
+ * Capture tmux pane output and strip ANSI escape sequences
222
+ * Consolidates the common capturePane + stripAnsi pattern (SF-001: DRY)
223
+ *
224
+ * @param sessionName - tmux session name
225
+ * @param lines - Number of lines to capture (default: 50, captures from -lines)
226
+ * @returns Clean pane output with ANSI codes removed
227
+ */
228
+ async function getCleanPaneOutput(sessionName, lines = 50) {
229
+ const output = await (0, tmux_1.capturePane)(sessionName, { startLine: -lines });
230
+ return (0, cli_patterns_1.stripAnsi)(output);
231
+ }
232
+ // ----- Health Check Functions (Bug 2) -----
233
+ /**
234
+ * Verify that Claude CLI is actually running inside a tmux session
235
+ * Detects broken sessions where tmux exists but Claude failed to start
236
+ *
237
+ * @param sessionName - tmux session name
238
+ * @returns true if Claude CLI is responsive (prompt detected or initializing)
239
+ */
240
+ async function isSessionHealthy(sessionName) {
241
+ try {
242
+ // SF-001: Use shared helper instead of inline capturePane + stripAnsi
243
+ 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
+ // MF-002: Check shell prompt endings from extensible array (OCP)
256
+ const trimmed = cleanOutput.trim();
257
+ // C-S2-001: Empty output means tmux session exists but Claude CLI has no output.
258
+ // This is treated as unhealthy because a properly running Claude CLI always
259
+ // produces output (prompt, spinner, or response). An empty pane indicates
260
+ // the CLI process has exited or failed to start.
261
+ if (trimmed === '') {
262
+ return false;
263
+ }
264
+ if (SHELL_PROMPT_ENDINGS.some(ending => trimmed.endsWith(ending))) {
265
+ return false;
266
+ }
267
+ return true;
268
+ }
269
+ catch {
270
+ return false;
271
+ }
272
+ }
273
+ /**
274
+ * Ensure the existing tmux session has a healthy Claude CLI process
275
+ * If unhealthy, kill the session so it can be recreated
276
+ * (SF-002: SRP - session health management separated from session creation)
277
+ *
278
+ * @param sessionName - tmux session name
279
+ * @returns true if session is healthy and can be reused, false if it was killed
280
+ */
281
+ async function ensureHealthySession(sessionName) {
282
+ const healthy = await isSessionHealthy(sessionName);
283
+ if (!healthy) {
284
+ await (0, tmux_1.killSession)(sessionName);
285
+ return false;
286
+ }
287
+ return true;
288
+ }
289
+ // ----- Environment Sanitization (Bug 3) -----
290
+ /**
291
+ * Remove CLAUDECODE environment variable from tmux session environment
292
+ * Prevents Claude Code from detecting nested session and refusing to start
293
+ * (SF-002: SRP - environment sanitization separated from session creation)
294
+ *
295
+ * MF-S3-002: tmux set-environment -g -u operates on the global tmux environment.
296
+ * Impact analysis:
297
+ * - CLAUDECODE is a Claude Code-specific variable, so Codex/Gemini sessions
298
+ * (CLI_TOOL_IDS: ['claude', 'codex', 'gemini']) are NOT affected by its removal.
299
+ * - Multiple Claude sessions concurrently calling unset (-g -u) is safe because
300
+ * the unset operation is idempotent (unsetting an already-unset variable is a no-op).
301
+ *
302
+ * SEC-SF-001: sessionName is validated by the caller chain:
303
+ * ClaudeTool.startSession() -> BaseCLITool.getSessionName() -> validateSessionName()
304
+ * This ensures sessionName contains only safe characters (alphanumeric + hyphen).
305
+ *
306
+ * SEC-SF-003: Migration trigger for session-scoped set-environment (without -g flag):
307
+ * - When sanitization of additional environment variables (e.g., CODEX_*, GEMINI_*)
308
+ * is required, migrate to session-scoped operations to prevent cross-session side effects.
309
+ * - Current scope (CLAUDECODE only) is safe with global scope due to idempotent unset.
310
+ *
311
+ * @param sessionName - tmux session name
312
+ */
313
+ async function sanitizeSessionEnvironment(sessionName) {
314
+ // 3-1: Remove from tmux global environment
315
+ // MF-S3-002: -g flag affects global tmux environment.
316
+ // Safe because: (1) CLAUDECODE is Claude-specific, (2) unset is idempotent.
317
+ await execAsync('tmux set-environment -g -u CLAUDECODE 2>/dev/null || true');
318
+ // 3-2: Unset inside the session shell (safety net)
319
+ // 100ms wait: empirically determined time for sendKeys command to reach the shell
320
+ // SF-S3-004: 100ms is 0.67% of CLAUDE_INIT_TIMEOUT (15000ms), acceptable overhead
321
+ await (0, tmux_1.sendKeys)(sessionName, 'unset CLAUDECODE', true);
322
+ await new Promise(resolve => setTimeout(resolve, 100));
323
+ }
151
324
  /**
152
325
  * Get tmux session name for a worktree
153
326
  *
@@ -178,9 +351,15 @@ async function isClaudeInstalled() {
178
351
  }
179
352
  /**
180
353
  * Check if Claude session is running
354
+ * MF-S3-001: Includes health check to prevent reporting broken sessions as running.
355
+ * Without this, API routes (especially send/route.ts) would skip startSession()
356
+ * for broken sessions and attempt to send messages to a non-functional CLI.
357
+ *
358
+ * Performance: adds ~50ms overhead (capturePane + pattern match) per call.
359
+ * This is acceptable given that API route response times are typically 100-500ms.
181
360
  *
182
361
  * @param worktreeId - Worktree ID
183
- * @returns True if Claude session exists and is running
362
+ * @returns True if Claude session exists AND Claude CLI is healthy
184
363
  *
185
364
  * @example
186
365
  * ```typescript
@@ -192,13 +371,27 @@ async function isClaudeInstalled() {
192
371
  */
193
372
  async function isClaudeRunning(worktreeId) {
194
373
  const sessionName = getSessionName(worktreeId);
195
- return await (0, tmux_1.hasSession)(sessionName);
374
+ const exists = await (0, tmux_1.hasSession)(sessionName);
375
+ if (!exists) {
376
+ return false;
377
+ }
378
+ // MF-S3-001: Verify session health to avoid reporting broken sessions as running
379
+ return isSessionHealthy(sessionName);
196
380
  }
197
381
  /**
198
382
  * Get Claude session state
199
383
  *
384
+ * C-S3-002: This function checks tmux session existence via hasSession() but
385
+ * does NOT perform health checks (unlike isClaudeRunning()). This is intentional:
386
+ * getClaudeSessionState() is a lightweight status query for UI display purposes,
387
+ * while isClaudeRunning() performs the more expensive health check for operational
388
+ * decisions (e.g., whether to recreate a session).
389
+ *
390
+ * If health-aware state is needed, callers should use isClaudeRunning() instead
391
+ * or call ensureHealthySession() separately.
392
+ *
200
393
  * @param worktreeId - Worktree ID
201
- * @returns Session state information
394
+ * @returns Session state information (existence-based, not health-based)
202
395
  */
203
396
  async function getClaudeSessionState(worktreeId) {
204
397
  const sessionName = getSessionName(worktreeId);
@@ -227,11 +420,10 @@ async function waitForPrompt(sessionName, timeout = exports.CLAUDE_PROMPT_WAIT_T
227
420
  const startTime = Date.now();
228
421
  const pollInterval = exports.CLAUDE_PROMPT_POLL_INTERVAL;
229
422
  while (Date.now() - startTime < timeout) {
230
- // Use -50 lines to capture more context including status bars
231
- const output = await (0, tmux_1.capturePane)(sessionName, { startLine: -50 });
423
+ // SF-001: Use getCleanPaneOutput helper (DRY)
424
+ const cleanOutput = await getCleanPaneOutput(sessionName);
232
425
  // DRY-001: Use CLAUDE_PROMPT_PATTERN from cli-patterns.ts
233
- // Strip ANSI escape sequences before pattern matching (Issue #152)
234
- if (cli_patterns_1.CLAUDE_PROMPT_PATTERN.test((0, cli_patterns_1.stripAnsi)(output))) {
426
+ if (cli_patterns_1.CLAUDE_PROMPT_PATTERN.test(cleanOutput)) {
235
427
  return; // Prompt detected
236
428
  }
237
429
  await new Promise((resolve) => setTimeout(resolve, pollInterval));
@@ -263,8 +455,15 @@ async function startClaudeSession(options) {
263
455
  // Check if session already exists
264
456
  const exists = await (0, tmux_1.hasSession)(sessionName);
265
457
  if (exists) {
266
- console.log(`Claude session ${sessionName} already exists`);
267
- return;
458
+ // SF-S2-004: Health check on existing session
459
+ const healthy = await ensureHealthySession(sessionName);
460
+ if (healthy) {
461
+ console.log(`Claude session ${sessionName} already exists and is healthy`);
462
+ return;
463
+ }
464
+ // If not healthy, ensureHealthySession() already killed the session.
465
+ // Fall through to the session creation logic below.
466
+ // (SF-S2-004: Explicit fall-through instead of hidden re-entry)
268
467
  }
269
468
  try {
270
469
  // Create tmux session with large history buffer for Claude output
@@ -273,6 +472,8 @@ async function startClaudeSession(options) {
273
472
  workingDirectory: worktreePath,
274
473
  historyLimit: 50000,
275
474
  });
475
+ // SF-S2-003: Sanitize environment after createSession, before launching Claude CLI
476
+ await sanitizeSessionEnvironment(sessionName);
276
477
  // Get Claude CLI path dynamically
277
478
  const claudePath = await getClaudePath();
278
479
  // Start Claude CLI in interactive mode using dynamically resolved path
@@ -287,13 +488,11 @@ async function startClaudeSession(options) {
287
488
  while (Date.now() - startTime < maxWaitTime) {
288
489
  await new Promise((resolve) => setTimeout(resolve, pollInterval));
289
490
  try {
290
- const output = await (0, tmux_1.capturePane)(sessionName, { startLine: -50 });
491
+ // SF-001: Use getCleanPaneOutput helper (DRY)
492
+ const cleanOutput = await getCleanPaneOutput(sessionName);
291
493
  // Claude is ready when we see the prompt (DRY-001)
292
494
  // Use CLAUDE_PROMPT_PATTERN from cli-patterns.ts for consistency
293
- // Strip ANSI escape sequences before pattern matching (Issue #152)
294
495
  // 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
296
- const cleanOutput = (0, cli_patterns_1.stripAnsi)(output);
297
496
  if (cli_patterns_1.CLAUDE_PROMPT_PATTERN.test(cleanOutput)) {
298
497
  // Wait for stability after prompt detection (CONS-007, DOC-001)
299
498
  await new Promise((resolve) => setTimeout(resolve, exports.CLAUDE_POST_PROMPT_DELAY));
@@ -306,7 +505,6 @@ async function startClaudeSession(options) {
306
505
  if (!trustDialogHandled && cli_patterns_1.CLAUDE_TRUST_DIALOG_PATTERN.test(cleanOutput)) {
307
506
  await (0, tmux_1.sendKeys)(sessionName, '', true);
308
507
  trustDialogHandled = true;
309
- // TODO: Log output method unification (console.log vs createLogger) to be addressed in a separate Issue (SF-002)
310
508
  console.log('Trust dialog detected, sending Enter to confirm');
311
509
  // Continue polling to wait for prompt detection
312
510
  }
@@ -322,7 +520,11 @@ async function startClaudeSession(options) {
322
520
  console.log(`Started Claude session: ${sessionName}`);
323
521
  }
324
522
  catch (error) {
325
- throw new Error(`Failed to start Claude session: ${getErrorMessage(error)}`);
523
+ // MF-S2-002: Clear cached path on all failures (harmless for non-path failures)
524
+ clearCachedClaudePath();
525
+ // SEC-SF-002: Log detailed error server-side, throw generic message to client
526
+ console.log(`[claude-session] Session start failed: ${getErrorMessage(error)}`);
527
+ throw new Error('Failed to start Claude session');
326
528
  }
327
529
  }
328
530
  /**
@@ -345,17 +547,13 @@ async function sendMessageToClaude(worktreeId, message) {
345
547
  throw new Error(`Claude session ${sessionName} does not exist. Start the session first.`);
346
548
  }
347
549
  // Verify prompt state before sending (CONS-006, DRY-001)
348
- const output = await (0, tmux_1.capturePane)(sessionName, { startLine: -50 });
349
- if (!cli_patterns_1.CLAUDE_PROMPT_PATTERN.test((0, cli_patterns_1.stripAnsi)(output))) {
550
+ // SF-001: Use getCleanPaneOutput helper (DRY)
551
+ const cleanOutput = await getCleanPaneOutput(sessionName);
552
+ if (!cli_patterns_1.CLAUDE_PROMPT_PATTERN.test(cleanOutput)) {
350
553
  // Path B: Prompt not detected - wait for it (P1: throw on timeout)
351
554
  await waitForPrompt(sessionName, exports.CLAUDE_SEND_PROMPT_WAIT_TIMEOUT);
352
555
  }
353
556
  // 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
557
  await new Promise((resolve) => setTimeout(resolve, exports.CLAUDE_POST_PROMPT_DELAY));
360
558
  // Send message using sendKeys consistently (CONS-001)
361
559
  await (0, tmux_1.sendKeys)(sessionName, message, false);
@@ -420,7 +618,7 @@ async function stopClaudeSession(worktreeId) {
420
618
  // Kill the tmux session
421
619
  const killed = await (0, tmux_1.killSession)(sessionName);
422
620
  if (killed) {
423
- console.log(`✓ Stopped Claude session: ${sessionName}`);
621
+ console.log(`Stopped Claude session: ${sessionName}`);
424
622
  }
425
623
  return killed;
426
624
  }
@@ -4,7 +4,7 @@
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.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;
7
+ exports.CLAUDE_SESSION_ERROR_REGEX_PATTERNS = exports.CLAUDE_SESSION_ERROR_PATTERNS = 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;
@@ -237,6 +237,39 @@ function stripAnsi(str) {
237
237
  * @param cliToolId - CLI tool identifier
238
238
  * @returns DetectPromptOptions for the tool, or undefined for default behavior
239
239
  */
240
+ /**
241
+ * Error patterns that indicate a Claude session failed to start properly
242
+ * Used by isSessionHealthy() to detect broken sessions (MF-001: SRP)
243
+ * Style: readonly + as const for type safety (SF-S2-001: follows response-poller.ts precedent)
244
+ *
245
+ * SEC-SF-004: Pattern maintenance process:
246
+ * - When Claude CLI is updated, verify that error messages still match these patterns.
247
+ * - Test procedure: Intentionally trigger each error condition (e.g., nested session launch)
248
+ * and confirm the error message is captured by the patterns.
249
+ * - If Claude CLI introduces localized error messages, add locale-aware patterns or
250
+ * consider switching to exit code-based detection as a more robust alternative.
251
+ * - Pattern additions should be accompanied by corresponding test cases in
252
+ * claude-session.test.ts.
253
+ *
254
+ * C-S3-001: Codex/Gemini monitoring note:
255
+ * These patterns are currently Claude-specific. If Codex or Gemini exhibit similar
256
+ * "nested session" or startup failure behaviors, analogous error patterns should be
257
+ * added to their respective tool configurations (codex.ts, gemini.ts) rather than
258
+ * extending these arrays, to maintain SRP per CLI tool type.
259
+ */
260
+ exports.CLAUDE_SESSION_ERROR_PATTERNS = [
261
+ 'Claude Code cannot be launched inside another Claude Code session',
262
+ ];
263
+ /**
264
+ * Regex patterns for Claude session errors requiring context matching
265
+ * Used by isSessionHealthy() for multi-condition error detection (MF-001: SRP)
266
+ * Style: readonly + as const for type safety (SF-S2-001: follows response-poller.ts precedent)
267
+ *
268
+ * SEC-SF-004: See CLAUDE_SESSION_ERROR_PATTERNS JSDoc for pattern maintenance process.
269
+ */
270
+ exports.CLAUDE_SESSION_ERROR_REGEX_PATTERNS = [
271
+ /Error:.*Claude/,
272
+ ];
240
273
  function buildDetectPromptOptions(cliToolId) {
241
274
  if (cliToolId === 'claude') {
242
275
  return { requireDefaultIndicator: false };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "commandmate",
3
- "version": "0.2.6",
3
+ "version": "0.2.8",
4
4
  "description": "Git worktree management with Claude CLI and tmux sessions",
5
5
  "repository": {
6
6
  "type": "git",