commandmate 0.3.1 → 0.3.3

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 (151) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/app-build-manifest.json +11 -11
  3. package/.next/app-path-routes-manifest.json +1 -1
  4. package/.next/build-manifest.json +4 -4
  5. package/.next/cache/.tsbuildinfo +1 -1
  6. package/.next/cache/config.json +3 -3
  7. package/.next/cache/webpack/client-production/0.pack +0 -0
  8. package/.next/cache/webpack/client-production/1.pack +0 -0
  9. package/.next/cache/webpack/client-production/2.pack +0 -0
  10. package/.next/cache/webpack/client-production/index.pack +0 -0
  11. package/.next/cache/webpack/client-production/index.pack.old +0 -0
  12. package/.next/cache/webpack/edge-server-production/0.pack +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/routes-manifest.json +1 -1
  20. package/.next/server/app/_not-found/page.js.nft.json +1 -1
  21. package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  22. package/.next/server/app/api/app/update-check/route.js +1 -1
  23. package/.next/server/app/api/external-apps/[id]/health/route.js +1 -1
  24. package/.next/server/app/api/external-apps/[id]/route.js +1 -1
  25. package/.next/server/app/api/external-apps/route.js +1 -1
  26. package/.next/server/app/api/hooks/claude-done/route.js +1 -1
  27. package/.next/server/app/api/ollama/models/route.js +1 -0
  28. package/.next/server/app/api/ollama/models/route.js.nft.json +1 -0
  29. package/.next/server/app/api/ollama/models.body +1 -0
  30. package/.next/server/app/api/ollama/models.meta +1 -0
  31. package/.next/server/app/api/repositories/clone/[jobId]/route.js +1 -1
  32. package/.next/server/app/api/repositories/clone/route.js +1 -1
  33. package/.next/server/app/api/repositories/excluded/route.js +7 -7
  34. package/.next/server/app/api/repositories/restore/route.js +3 -3
  35. package/.next/server/app/api/repositories/route.js +13 -11
  36. package/.next/server/app/api/repositories/route.js.nft.json +1 -1
  37. package/.next/server/app/api/repositories/scan/route.js +1 -1
  38. package/.next/server/app/api/repositories/sync/route.js +3 -3
  39. package/.next/server/app/api/worktrees/[id]/auto-yes/route.js +1 -1
  40. package/.next/server/app/api/worktrees/[id]/auto-yes/route.js.nft.json +1 -1
  41. package/.next/server/app/api/worktrees/[id]/cli-tool/route.js +1 -1
  42. package/.next/server/app/api/worktrees/[id]/current-output/route.js +1 -1
  43. package/.next/server/app/api/worktrees/[id]/current-output/route.js.nft.json +1 -1
  44. package/.next/server/app/api/worktrees/[id]/execution-logs/[logId]/route.js +1 -0
  45. package/.next/server/app/api/worktrees/[id]/execution-logs/[logId]/route.js.nft.json +1 -0
  46. package/.next/server/app/api/worktrees/[id]/execution-logs/route.js +9 -0
  47. package/.next/server/app/api/worktrees/[id]/execution-logs/route.js.nft.json +1 -0
  48. package/.next/server/app/api/worktrees/[id]/files/[...path]/route.js +1 -1
  49. package/.next/server/app/api/worktrees/[id]/interrupt/route.js +1 -1
  50. package/.next/server/app/api/worktrees/[id]/interrupt/route.js.nft.json +1 -1
  51. package/.next/server/app/api/worktrees/[id]/kill-session/route.js +1 -1
  52. package/.next/server/app/api/worktrees/[id]/kill-session/route.js.nft.json +1 -1
  53. package/.next/server/app/api/worktrees/[id]/logs/[filename]/route.js +1 -1
  54. package/.next/server/app/api/worktrees/[id]/logs/route.js +2 -2
  55. package/.next/server/app/api/worktrees/[id]/memos/[memoId]/route.js +1 -1
  56. package/.next/server/app/api/worktrees/[id]/memos/route.js +1 -1
  57. package/.next/server/app/api/worktrees/[id]/messages/route.js +1 -1
  58. package/.next/server/app/api/worktrees/[id]/prompt-response/route.js +1 -1
  59. package/.next/server/app/api/worktrees/[id]/prompt-response/route.js.nft.json +1 -1
  60. package/.next/server/app/api/worktrees/[id]/respond/route.js +1 -1
  61. package/.next/server/app/api/worktrees/[id]/respond/route.js.nft.json +1 -1
  62. package/.next/server/app/api/worktrees/[id]/route.js +1 -1
  63. package/.next/server/app/api/worktrees/[id]/route.js.nft.json +1 -1
  64. package/.next/server/app/api/worktrees/[id]/schedules/[scheduleId]/route.js +1 -0
  65. package/.next/server/app/api/worktrees/[id]/schedules/[scheduleId]/route.js.nft.json +1 -0
  66. package/.next/server/app/api/worktrees/[id]/schedules/route.js +4 -0
  67. package/.next/server/app/api/worktrees/[id]/schedules/route.js.nft.json +1 -0
  68. package/.next/server/app/api/worktrees/[id]/search/route.js +1 -1
  69. package/.next/server/app/api/worktrees/[id]/send/route.js +1 -1
  70. package/.next/server/app/api/worktrees/[id]/send/route.js.nft.json +1 -1
  71. package/.next/server/app/api/worktrees/[id]/slash-commands/route.js +1 -1
  72. package/.next/server/app/api/worktrees/[id]/start-polling/route.js +1 -1
  73. package/.next/server/app/api/worktrees/[id]/start-polling/route.js.nft.json +1 -1
  74. package/.next/server/app/api/worktrees/[id]/tree/[...path]/route.js +1 -1
  75. package/.next/server/app/api/worktrees/[id]/tree/route.js +1 -1
  76. package/.next/server/app/api/worktrees/[id]/upload/[...path]/route.js +1 -1
  77. package/.next/server/app/api/worktrees/[id]/viewed/route.js +1 -1
  78. package/.next/server/app/api/worktrees/route.js +1 -1
  79. package/.next/server/app/api/worktrees/route.js.nft.json +1 -1
  80. package/.next/server/app/login/page.js.nft.json +1 -1
  81. package/.next/server/app/login/page_client-reference-manifest.js +1 -1
  82. package/.next/server/app/page.js +1 -1
  83. package/.next/server/app/page.js.nft.json +1 -1
  84. package/.next/server/app/page_client-reference-manifest.js +1 -1
  85. package/.next/server/app/proxy/[...path]/route.js +1 -1
  86. package/.next/server/app/worktrees/[id]/files/[...path]/page.js.nft.json +1 -1
  87. package/.next/server/app/worktrees/[id]/files/[...path]/page_client-reference-manifest.js +1 -1
  88. package/.next/server/app/worktrees/[id]/page.js +8 -3
  89. package/.next/server/app/worktrees/[id]/page.js.nft.json +1 -1
  90. package/.next/server/app/worktrees/[id]/page_client-reference-manifest.js +1 -1
  91. package/.next/server/app/worktrees/[id]/terminal/page.js.nft.json +1 -1
  92. package/.next/server/app/worktrees/[id]/terminal/page_client-reference-manifest.js +1 -1
  93. package/.next/server/app-paths-manifest.json +10 -5
  94. package/.next/server/chunks/2314.js +1 -0
  95. package/.next/server/chunks/3860.js +1 -1
  96. package/.next/server/chunks/4559.js +1 -1
  97. package/.next/server/chunks/539.js +10 -10
  98. package/.next/server/chunks/5853.js +1 -1
  99. package/.next/server/chunks/6228.js +1 -0
  100. package/.next/server/chunks/7425.js +112 -37
  101. package/.next/server/chunks/7566.js +1 -1
  102. package/.next/server/chunks/8693.js +1 -1
  103. package/.next/server/chunks/9446.js +1 -0
  104. package/.next/server/functions-config-manifest.json +1 -1
  105. package/.next/server/middleware-build-manifest.js +1 -1
  106. package/.next/server/middleware-manifest.json +5 -5
  107. package/.next/server/pages/500.html +1 -1
  108. package/.next/server/server-reference-manifest.json +1 -1
  109. package/.next/static/chunks/8091-274bc0716106e7fc.js +1 -0
  110. package/.next/static/chunks/app/page-060057e02b841125.js +1 -0
  111. package/.next/static/chunks/app/worktrees/[id]/page-78580947c201d698.js +1 -0
  112. package/.next/static/chunks/{main-db79434ee4a6c931.js → main-2feda12a4d321111.js} +1 -1
  113. package/.next/static/css/e85de230ef5ddc40.css +3 -0
  114. package/.next/trace +5 -5
  115. package/.next/types/app/api/ollama/models/route.ts +343 -0
  116. package/.next/types/app/api/worktrees/[id]/execution-logs/[logId]/route.ts +343 -0
  117. package/.next/types/app/api/worktrees/[id]/execution-logs/route.ts +343 -0
  118. package/.next/types/app/api/worktrees/[id]/schedules/[scheduleId]/route.ts +343 -0
  119. package/.next/types/app/api/worktrees/[id]/schedules/route.ts +343 -0
  120. package/README.md +74 -76
  121. package/dist/cli/utils/docs-reader.d.ts.map +1 -1
  122. package/dist/cli/utils/docs-reader.js +1 -0
  123. package/dist/server/server.js +5 -0
  124. package/dist/server/src/config/cmate-constants.js +79 -0
  125. package/dist/server/src/config/schedule-config.js +60 -0
  126. package/dist/server/src/lib/auto-yes-manager.js +2 -2
  127. package/dist/server/src/lib/claude-executor.js +158 -0
  128. package/dist/server/src/lib/cli-patterns.js +73 -9
  129. package/dist/server/src/lib/cli-tools/gemini.js +81 -22
  130. package/dist/server/src/lib/cli-tools/manager.js +4 -2
  131. package/dist/server/src/lib/cli-tools/types.js +64 -2
  132. package/dist/server/src/lib/cli-tools/vibe-local.js +163 -0
  133. package/dist/server/src/lib/cmate-parser.js +262 -0
  134. package/dist/server/src/lib/db-instance.js +3 -0
  135. package/dist/server/src/lib/db-migrations.js +145 -2
  136. package/dist/server/src/lib/db.js +51 -1
  137. package/dist/server/src/lib/env-sanitizer.js +57 -0
  138. package/dist/server/src/lib/prompt-detector.js +4 -3
  139. package/dist/server/src/lib/response-poller.js +22 -11
  140. package/dist/server/src/lib/schedule-manager.js +401 -0
  141. package/dist/server/src/lib/selected-agents-validator.js +99 -0
  142. package/dist/server/src/types/cmate.js +6 -0
  143. package/dist/server/src/types/sidebar.js +9 -4
  144. package/package.json +2 -1
  145. package/.next/server/chunks/7536.js +0 -1
  146. package/.next/static/chunks/8091-925542bdfc843dce.js +0 -1
  147. package/.next/static/chunks/app/page-238b5a70d8c101e9.js +0 -1
  148. package/.next/static/chunks/app/worktrees/[id]/page-a556551ce5c69dec.js +0 -1
  149. package/.next/static/css/b9ea6a4fad17dc32.css +0 -3
  150. /package/.next/static/{hmAjbCPjxX_C0Os7rphI1 → O7EDFfAYQNe_HRbORxQAC}/_buildManifest.js +0 -0
  151. /package/.next/static/{hmAjbCPjxX_C0Os7rphI1 → O7EDFfAYQNe_HRbORxQAC}/_ssgManifest.js +0 -0
@@ -0,0 +1,60 @@
1
+ "use strict";
2
+ /**
3
+ * Schedule Execution Configuration Constants
4
+ * Issue #294: Centralized constants for schedule-related API routes
5
+ *
6
+ * Eliminates duplication of validation constants and UUID validation
7
+ * across schedules/route.ts, schedules/[scheduleId]/route.ts,
8
+ * and execution-logs/[logId]/route.ts.
9
+ *
10
+ * [S4-014] UUID v4 format validation
11
+ */
12
+ Object.defineProperty(exports, "__esModule", { value: true });
13
+ exports.UUID_V4_PATTERN = exports.DEFAULT_PERMISSIONS = exports.VIBE_LOCAL_PERMISSIONS = exports.GEMINI_PERMISSIONS = exports.CODEX_SANDBOXES = exports.CLAUDE_PERMISSIONS = exports.MAX_SCHEDULE_CRON_LENGTH = exports.MAX_SCHEDULE_MESSAGE_LENGTH = exports.MAX_SCHEDULE_NAME_LENGTH = void 0;
14
+ exports.isValidUuidV4 = isValidUuidV4;
15
+ // =============================================================================
16
+ // Validation Constants
17
+ // =============================================================================
18
+ /** Maximum schedule name length */
19
+ exports.MAX_SCHEDULE_NAME_LENGTH = 100;
20
+ /** Maximum message length for schedule execution */
21
+ exports.MAX_SCHEDULE_MESSAGE_LENGTH = 10000;
22
+ /** Maximum cron expression length */
23
+ exports.MAX_SCHEDULE_CRON_LENGTH = 100;
24
+ // =============================================================================
25
+ // Permission Constants
26
+ // =============================================================================
27
+ /** Allowed permission values for claude CLI (--permission-mode) */
28
+ exports.CLAUDE_PERMISSIONS = ['default', 'acceptEdits', 'plan', 'dontAsk', 'bypassPermissions'];
29
+ /** Allowed sandbox values for codex CLI (--sandbox) */
30
+ exports.CODEX_SANDBOXES = ['read-only', 'workspace-write', 'danger-full-access'];
31
+ /** Allowed permission values for gemini CLI (no permission flags) */
32
+ exports.GEMINI_PERMISSIONS = [];
33
+ /** Allowed permission values for vibe-local CLI (no permission flags) */
34
+ exports.VIBE_LOCAL_PERMISSIONS = [];
35
+ /** Default permission per CLI tool */
36
+ exports.DEFAULT_PERMISSIONS = {
37
+ claude: 'acceptEdits',
38
+ codex: 'workspace-write',
39
+ gemini: '',
40
+ 'vibe-local': '',
41
+ };
42
+ // =============================================================================
43
+ // UUID Validation
44
+ // =============================================================================
45
+ /**
46
+ * UUID v4 validation pattern.
47
+ * Matches standard UUID v4 format: xxxxxxxx-xxxx-4xxx-[89ab]xxx-xxxxxxxxxxxx
48
+ *
49
+ * [S4-014] Used to validate schedule IDs and execution log IDs
50
+ */
51
+ exports.UUID_V4_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
52
+ /**
53
+ * Validate that a string is a valid UUID v4 format.
54
+ *
55
+ * @param id - String to validate
56
+ * @returns true if the string matches UUID v4 format
57
+ */
58
+ function isValidUuidV4(id) {
59
+ return exports.UUID_V4_PATTERN.test(id);
60
+ }
@@ -403,7 +403,7 @@ async function captureAndCleanOutput(worktreeId, cliToolId) {
403
403
  // captureSessionOutput() default is 1000 lines, but tmux buffer capture
404
404
  // requires 5000 to avoid truncating long outputs.
405
405
  const output = await (0, cli_session_1.captureSessionOutput)(worktreeId, cliToolId, 5000);
406
- return (0, cli_patterns_1.stripAnsi)(output);
406
+ return (0, cli_patterns_1.stripBoxDrawing)((0, cli_patterns_1.stripAnsi)(output));
407
407
  }
408
408
  /**
409
409
  * Process stop condition check using delta-based approach.
@@ -465,7 +465,7 @@ async function detectAndRespondToPrompt(worktreeId, pollerState, cliToolId, clea
465
465
  try {
466
466
  // 1. Detect prompt
467
467
  const promptOptions = (0, cli_patterns_1.buildDetectPromptOptions)(cliToolId);
468
- const promptDetection = (0, prompt_detector_1.detectPrompt)(cleanOutput, promptOptions);
468
+ const promptDetection = (0, prompt_detector_1.detectPrompt)((0, cli_patterns_1.stripBoxDrawing)(cleanOutput), promptOptions);
469
469
  if (!promptDetection.isPrompt || !promptDetection.promptData) {
470
470
  // No prompt detected - reset lastAnsweredPromptKey (Issue #306)
471
471
  pollerState.lastAnsweredPromptKey = null;
@@ -0,0 +1,158 @@
1
+ "use strict";
2
+ /**
3
+ * Claude CLI Executor
4
+ * Issue #294: Executes claude -p commands for scheduled executions
5
+ *
6
+ * Security:
7
+ * - Uses execFile (not exec) to prevent shell injection
8
+ * - Sanitizes environment variables via env-sanitizer.ts
9
+ * - Limits output size to prevent memory exhaustion
10
+ * - Enforces execution timeout
11
+ */
12
+ Object.defineProperty(exports, "__esModule", { value: true });
13
+ exports.ALLOWED_CLI_TOOLS = exports.MAX_MESSAGE_LENGTH = exports.EXECUTION_TIMEOUT_MS = exports.MAX_STORED_OUTPUT_SIZE = exports.MAX_OUTPUT_SIZE = void 0;
14
+ exports.truncateOutput = truncateOutput;
15
+ exports.buildCliArgs = buildCliArgs;
16
+ exports.executeClaudeCommand = executeClaudeCommand;
17
+ exports.getActiveProcesses = getActiveProcesses;
18
+ const child_process_1 = require("child_process");
19
+ const env_sanitizer_1 = require("./env-sanitizer");
20
+ const cli_patterns_1 = require("./cli-patterns");
21
+ // =============================================================================
22
+ // Constants
23
+ // =============================================================================
24
+ /** Maximum output buffer size for execFile (1MB) */
25
+ exports.MAX_OUTPUT_SIZE = 1 * 1024 * 1024;
26
+ /** Maximum output size stored in DB (100KB) */
27
+ exports.MAX_STORED_OUTPUT_SIZE = 100 * 1024;
28
+ /** Execution timeout in milliseconds (5 minutes) */
29
+ exports.EXECUTION_TIMEOUT_MS = 5 * 60 * 1000;
30
+ /** Maximum message length sent to claude -p */
31
+ exports.MAX_MESSAGE_LENGTH = 10000;
32
+ /** Allowed CLI tool identifiers for scheduled execution */
33
+ exports.ALLOWED_CLI_TOOLS = new Set(['claude', 'codex', 'gemini', 'vibe-local']);
34
+ // =============================================================================
35
+ // Executor
36
+ // =============================================================================
37
+ /**
38
+ * Truncate output to MAX_STORED_OUTPUT_SIZE bytes.
39
+ * Appends a truncation notice if truncated.
40
+ *
41
+ * @param output - Raw output string
42
+ * @returns Truncated output string
43
+ */
44
+ function truncateOutput(output) {
45
+ if (Buffer.byteLength(output, 'utf-8') <= exports.MAX_STORED_OUTPUT_SIZE) {
46
+ return output;
47
+ }
48
+ // Truncate to MAX_STORED_OUTPUT_SIZE bytes
49
+ const buffer = Buffer.from(output, 'utf-8');
50
+ const truncated = buffer.subarray(0, exports.MAX_STORED_OUTPUT_SIZE).toString('utf-8');
51
+ return truncated + '\n\n--- Output truncated (exceeded 100KB limit) ---';
52
+ }
53
+ /**
54
+ * Build CLI arguments for non-interactive execution based on CLI tool type.
55
+ *
56
+ * - claude: -p <message> --output-format text --permission-mode <permission>
57
+ * - codex: exec <message> --sandbox <permission>
58
+ * - gemini: -p <message>
59
+ * - vibe-local: [-p <message> -y] or [--model <model> -p <message> -y]
60
+ * - others: -p <message> (fallback)
61
+ *
62
+ * @param message - Prompt message
63
+ * @param cliToolId - CLI tool identifier
64
+ * @param permission - Permission mode (claude: --permission-mode, codex: --sandbox)
65
+ * @param options - Additional options (e.g., model for vibe-local)
66
+ * @returns Array of CLI arguments
67
+ */
68
+ function buildCliArgs(message, cliToolId, permission, options) {
69
+ switch (cliToolId) {
70
+ case 'codex':
71
+ return ['exec', message, '--sandbox', permission ?? 'workspace-write'];
72
+ case 'gemini':
73
+ return ['-p', message];
74
+ case 'vibe-local':
75
+ if (options?.model) {
76
+ return ['--model', options.model, '-p', message, '-y'];
77
+ }
78
+ return ['-p', message, '-y'];
79
+ case 'claude':
80
+ default:
81
+ return ['-p', message, '--output-format', 'text', '--permission-mode', permission ?? 'acceptEdits'];
82
+ }
83
+ }
84
+ /**
85
+ * Execute a CLI command in a worktree directory.
86
+ *
87
+ * @param message - Prompt message to send
88
+ * @param cwd - Working directory (worktree path from DB)
89
+ * @param cliToolId - CLI tool to use (default: 'claude')
90
+ * @param permission - Permission mode (claude: --permission-mode, codex: --sandbox)
91
+ * @param options - Additional options (e.g., model for vibe-local)
92
+ * @returns Execution result with output and status
93
+ */
94
+ async function executeClaudeCommand(message, cwd, cliToolId = 'claude', permission, options) {
95
+ // Validate cliToolId against whitelist [SEC-001]
96
+ if (!exports.ALLOWED_CLI_TOOLS.has(cliToolId)) {
97
+ return {
98
+ output: '',
99
+ exitCode: null,
100
+ status: 'failed',
101
+ error: `Invalid CLI tool: ${cliToolId}`,
102
+ };
103
+ }
104
+ // Validate message length
105
+ const truncatedMessage = message.length > exports.MAX_MESSAGE_LENGTH
106
+ ? message.substring(0, exports.MAX_MESSAGE_LENGTH)
107
+ : message;
108
+ const args = buildCliArgs(truncatedMessage, cliToolId, permission, options);
109
+ return new Promise((resolve) => {
110
+ const child = (0, child_process_1.execFile)(cliToolId, args, {
111
+ cwd,
112
+ env: (0, env_sanitizer_1.sanitizeEnvForChildProcess)(),
113
+ maxBuffer: exports.MAX_OUTPUT_SIZE,
114
+ timeout: exports.EXECUTION_TIMEOUT_MS,
115
+ }, (error, stdout, stderr) => {
116
+ if (error) {
117
+ const isTimeout = error.killed || error.code === 'ETIMEDOUT';
118
+ const rawOutput = (0, cli_patterns_1.stripAnsi)(stdout || stderr || error.message);
119
+ const output = truncateOutput(rawOutput);
120
+ resolve({
121
+ output,
122
+ exitCode: error.code ? parseInt(String(error.code), 10) || null : null,
123
+ status: isTimeout ? 'timeout' : 'failed',
124
+ error: error.message,
125
+ });
126
+ return;
127
+ }
128
+ const rawOutput = (0, cli_patterns_1.stripAnsi)(stdout || '');
129
+ const output = truncateOutput(rawOutput);
130
+ resolve({
131
+ output,
132
+ exitCode: 0,
133
+ status: 'completed',
134
+ });
135
+ });
136
+ // Close stdin immediately to prevent hanging on yes/no prompts
137
+ child.stdin?.end();
138
+ // Return the child process PID for tracking
139
+ if (child.pid) {
140
+ // Store PID in global active processes for cleanup on shutdown
141
+ const activeProcesses = getActiveProcesses();
142
+ activeProcesses.set(child.pid, child);
143
+ child.on('exit', () => {
144
+ activeProcesses.delete(child.pid);
145
+ });
146
+ }
147
+ });
148
+ }
149
+ /**
150
+ * Get the global active processes map.
151
+ * Uses globalThis for hot reload persistence.
152
+ */
153
+ function getActiveProcesses() {
154
+ if (!globalThis.__scheduleActiveProcesses) {
155
+ globalThis.__scheduleActiveProcesses = new Map();
156
+ }
157
+ return globalThis.__scheduleActiveProcesses;
158
+ }
@@ -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.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;
7
+ exports.CLAUDE_SESSION_ERROR_REGEX_PATTERNS = exports.CLAUDE_SESSION_ERROR_PATTERNS = exports.VIBE_LOCAL_THINKING_PATTERN = exports.VIBE_LOCAL_PROMPT_PATTERN = exports.GEMINI_THINKING_PATTERN = exports.GEMINI_PROMPT_PATTERN = exports.MAX_PASTED_TEXT_RETRIES = exports.PASTED_TEXT_DETECT_DELAY = exports.PASTED_TEXT_PATTERN = exports.CODEX_SEPARATOR_PATTERN = exports.CODEX_PROMPT_PATTERN = exports.CLAUDE_TRUST_DIALOG_PATTERN = exports.CLAUDE_SEPARATOR_PATTERN = exports.CLAUDE_PROMPT_PATTERN = exports.CODEX_THINKING_PATTERN = exports.CLAUDE_THINKING_PATTERN = exports.CLAUDE_SPINNER_CHARS = void 0;
8
8
  exports.detectThinking = detectThinking;
9
9
  exports.getCliToolPatterns = getCliToolPatterns;
10
10
  exports.stripAnsi = stripAnsi;
11
+ exports.stripBoxDrawing = stripBoxDrawing;
11
12
  exports.buildDetectPromptOptions = buildDetectPromptOptions;
12
13
  const logger_1 = require("./logger");
13
14
  const logger = (0, logger_1.createLogger)('cli-patterns');
@@ -113,9 +114,29 @@ exports.PASTED_TEXT_DETECT_DELAY = 500;
113
114
  */
114
115
  exports.MAX_PASTED_TEXT_RETRIES = 3;
115
116
  /**
116
- * Gemini shell prompt pattern
117
+ * Gemini interactive REPL prompt pattern
118
+ * Gemini CLI shows a `>` or `❯` prompt when waiting for user input in interactive mode.
119
+ * Also matches shell prompts as fallback for session initialization phase.
117
120
  */
118
- exports.GEMINI_PROMPT_PATTERN = /^(%|\$|.*@.*[%$#])\s*$/m;
121
+ exports.GEMINI_PROMPT_PATTERN = /^[>❯]\s*$/m;
122
+ /**
123
+ * Gemini thinking/processing pattern
124
+ * Gemini CLI shows braille spinner characters and status text while processing.
125
+ */
126
+ exports.GEMINI_THINKING_PATTERN = /[\u2800-\u28FF]|Thinking\.\.\./;
127
+ /**
128
+ * Vibe Local prompt pattern
129
+ * vibe-local (vibe-coder) shows `ctx:N% ❯` prompt when waiting for user input.
130
+ * The prompt line includes a context usage percentage prefix.
131
+ * Examples: "ctx:9% ❯", "ctx:30% ❯", "ctx:9% ❯ /model"
132
+ */
133
+ exports.VIBE_LOCAL_PROMPT_PATTERN = /ctx:\d+%\s*[>❯]/m;
134
+ /**
135
+ * Vibe Local thinking/processing pattern
136
+ * vibe-local shows spinner characters and status text while processing.
137
+ * Matches braille spinners, "Thinking", and tool execution indicators.
138
+ */
139
+ exports.VIBE_LOCAL_THINKING_PATTERN = /[\u2800-\u28FF]|Thinking|⠋|⠙|⠹|⠸|⠼|⠴|⠦|⠧|⠇|⠏|Running|Executing/;
119
140
  /**
120
141
  * Detect if CLI tool is showing "thinking" indicator
121
142
  */
@@ -131,8 +152,10 @@ function detectThinking(cliToolId, content) {
131
152
  result = exports.CODEX_THINKING_PATTERN.test(content);
132
153
  break;
133
154
  case 'gemini':
134
- // Gemini doesn't have a thinking indicator in one-shot mode
135
- result = false;
155
+ result = exports.GEMINI_THINKING_PATTERN.test(content);
156
+ break;
157
+ case 'vibe-local':
158
+ result = exports.VIBE_LOCAL_THINKING_PATTERN.test(content);
136
159
  break;
137
160
  default:
138
161
  result = exports.CLAUDE_THINKING_PATTERN.test(content);
@@ -186,12 +209,36 @@ function getCliToolPatterns(cliToolId) {
186
209
  case 'gemini':
187
210
  return {
188
211
  promptPattern: exports.GEMINI_PROMPT_PATTERN,
189
- separatorPattern: /^gemini\s+--\s+/m,
190
- thinkingPattern: /(?!)/m, // Never matches - one-shot execution
212
+ separatorPattern: /^[─━]{3,}$/m,
213
+ thinkingPattern: exports.GEMINI_THINKING_PATTERN,
214
+ skipPatterns: [
215
+ /^[>❯]\s*$/, // Prompt line
216
+ exports.GEMINI_THINKING_PATTERN, // Thinking indicators
217
+ /^\s*$/, // Empty lines
218
+ /Gemini\s+\d+\.\d+/, // Version line
219
+ exports.PASTED_TEXT_PATTERN, // [Pasted text #N +XX lines]
220
+ ],
221
+ };
222
+ case 'vibe-local':
223
+ return {
224
+ promptPattern: exports.VIBE_LOCAL_PROMPT_PATTERN,
225
+ separatorPattern: /^[·]{10,}$/m, // vibe-local uses middle dot separators
226
+ thinkingPattern: exports.VIBE_LOCAL_THINKING_PATTERN,
191
227
  skipPatterns: [
192
- /^gemini\s+--\s+/, // Command line itself
193
- exports.GEMINI_PROMPT_PATTERN, // Shell prompt lines
228
+ exports.VIBE_LOCAL_PROMPT_PATTERN, // Prompt line (ctx:N% ❯)
229
+ exports.VIBE_LOCAL_THINKING_PATTERN, // Thinking indicators
194
230
  /^\s*$/, // Empty lines
231
+ /vibe-local|vibe-coder/, // Version/banner lines
232
+ /ctx:\s*\d+%/, // Context usage indicator
233
+ /Model\s+\w/, // Model info line
234
+ /Engine\s+\w/, // Engine info line
235
+ /Mode\s+/, // Mode info line
236
+ /RAM\s+/, // RAM info line
237
+ /CWD\s+/, // Working directory line
238
+ /^[·]{10,}$/, // Middle dot separator lines
239
+ /✦\s*Ready/, // Status bar "Ready" indicator
240
+ /ESC:\s*stop/, // Status bar "ESC: stop" hint
241
+ exports.PASTED_TEXT_PATTERN, // [Pasted text #N +XX lines]
195
242
  ],
196
243
  };
197
244
  default:
@@ -222,6 +269,23 @@ const ANSI_PATTERN = /\x1b\[[0-9;]*[a-zA-Z]|\x1b\][^\x07]*\x07|\[[0-9;]*m/g;
222
269
  function stripAnsi(str) {
223
270
  return str.replace(ANSI_PATTERN, '');
224
271
  }
272
+ /**
273
+ * Strip box-drawing border characters from CLI output.
274
+ * Gemini CLI wraps Action Required prompts in ╭─╮│╰─╯ borders.
275
+ * Removes │ (U+2502) prefix/suffix and border-only lines (╭╮╰╯─).
276
+ *
277
+ * @param str - Input string (typically after stripAnsi())
278
+ * @returns String with box-drawing borders removed
279
+ */
280
+ function stripBoxDrawing(str) {
281
+ return str.split('\n').map(line => {
282
+ // Remove border-only lines (╭──╮, ╰──╯, │ only, etc.)
283
+ if (/^[\u2502\u256D\u256E\u256F\u2570\u2500\s]+$/.test(line))
284
+ return '';
285
+ // Strip leading │ + optional space, trailing space + │
286
+ return line.replace(/^\u2502\s?/, '').replace(/\s*\u2502$/, '');
287
+ }).join('\n');
288
+ }
225
289
  /**
226
290
  * Build DetectPromptOptions for a given CLI tool.
227
291
  * Centralizes cliToolId-to-options mapping logic (DRY - MF-001).
@@ -1,18 +1,33 @@
1
1
  "use strict";
2
2
  /**
3
3
  * Gemini CLI tool implementation
4
- * Provides integration with Google's Gemini CLI
4
+ * Provides integration with Google's Gemini CLI in interactive mode
5
+ *
6
+ * @remarks Issue #368: Rewritten from non-interactive pipe mode to interactive REPL mode.
7
+ * Previous implementation used `echo 'msg' | gemini` which caused the process to exit
8
+ * immediately, making response polling impossible. Now launches `gemini` in interactive
9
+ * mode within tmux (same approach as Claude/Codex).
5
10
  */
6
11
  Object.defineProperty(exports, "__esModule", { value: true });
7
12
  exports.GeminiTool = void 0;
8
13
  const base_1 = require("./base");
9
14
  const tmux_1 = require("../tmux");
10
- const child_process_1 = require("child_process");
11
- const util_1 = require("util");
12
- const execAsync = (0, util_1.promisify)(child_process_1.exec);
15
+ const pasted_text_helper_1 = require("../pasted-text-helper");
16
+ /**
17
+ * Extract error message from unknown error type (DRY)
18
+ */
19
+ function getErrorMessage(error) {
20
+ return error instanceof Error ? error.message : String(error);
21
+ }
22
+ /** Wait for Gemini CLI to initialize after launch (banner + auth + dialog) */
23
+ const GEMINI_INIT_WAIT_MS = 6000;
24
+ /** Interval for polling trust dialog detection */
25
+ const TRUST_DIALOG_POLL_INTERVAL_MS = 1000;
26
+ /** Max attempts to detect trust dialog (10 * 1000ms = 10s polling window) */
27
+ const TRUST_DIALOG_MAX_ATTEMPTS = 10;
13
28
  /**
14
29
  * Gemini CLI tool implementation
15
- * Manages Gemini sessions using tmux
30
+ * Manages Gemini interactive sessions using tmux
16
31
  */
17
32
  class GeminiTool extends base_1.BaseCLITool {
18
33
  id = 'gemini';
@@ -30,8 +45,7 @@ class GeminiTool extends base_1.BaseCLITool {
30
45
  }
31
46
  /**
32
47
  * Start a new Gemini session for a worktree
33
- * Note: Gemini uses non-interactive mode, so we just create a tmux session
34
- * for running one-shot commands
48
+ * Launches `gemini` in interactive REPL mode within tmux
35
49
  *
36
50
  * @param worktreeId - Worktree ID
37
51
  * @param worktreePath - Worktree path
@@ -50,22 +64,59 @@ class GeminiTool extends base_1.BaseCLITool {
50
64
  return;
51
65
  }
52
66
  try {
53
- // Create tmux session for running Gemini commands
67
+ // Create tmux session with large history buffer
54
68
  await (0, tmux_1.createSession)({
55
69
  sessionName,
56
70
  workingDirectory: worktreePath,
57
71
  historyLimit: 50000,
58
72
  });
73
+ // Wait a moment for the session to be created
74
+ await new Promise((resolve) => setTimeout(resolve, 100));
75
+ // Start Gemini CLI in interactive mode (no flags = interactive REPL)
76
+ await (0, tmux_1.sendKeys)(sessionName, 'gemini', true);
77
+ // Wait for Gemini to initialize
78
+ await new Promise((resolve) => setTimeout(resolve, GEMINI_INIT_WAIT_MS));
79
+ // Auto-handle "Do you trust this folder?" dialog on first run
80
+ await this.handleTrustDialog(sessionName);
59
81
  console.log(`✓ Started Gemini session: ${sessionName}`);
60
82
  }
61
83
  catch (error) {
62
- const errorMessage = error instanceof Error ? error.message : String(error);
84
+ const errorMessage = getErrorMessage(error);
63
85
  throw new Error(`Failed to start Gemini session: ${errorMessage}`);
64
86
  }
65
87
  }
66
88
  /**
67
- * Send a message to Gemini session (non-interactive mode)
68
- * Executes a one-shot Gemini command and captures the output
89
+ * Handle Gemini "Do you trust this folder?" dialog
90
+ * On first run in a new directory, Gemini shows a trust confirmation.
91
+ * Auto-selects "1. Trust folder" to allow execution.
92
+ */
93
+ async handleTrustDialog(sessionName) {
94
+ for (let i = 0; i < TRUST_DIALOG_MAX_ATTEMPTS; i++) {
95
+ try {
96
+ const output = await (0, tmux_1.capturePane)(sessionName, 50);
97
+ if (output.includes('Do you trust this folder?')) {
98
+ // Option 1 "Trust folder" is pre-selected (● marker).
99
+ // Send Enter to confirm the selection.
100
+ await (0, tmux_1.sendSpecialKey)(sessionName, 'Enter');
101
+ await new Promise((resolve) => setTimeout(resolve, 2000));
102
+ console.log('✓ Auto-trusted folder for Gemini session');
103
+ return;
104
+ }
105
+ // Check if Gemini interactive prompt is already showing (no dialog needed)
106
+ if (output.match(/^[>❯]\s*$/m)) {
107
+ console.log('✓ Gemini prompt detected - no trust dialog needed');
108
+ return;
109
+ }
110
+ }
111
+ catch {
112
+ // Capture may fail during initialization - continue polling
113
+ }
114
+ await new Promise((resolve) => setTimeout(resolve, TRUST_DIALOG_POLL_INTERVAL_MS));
115
+ }
116
+ console.log('⚠ Trust dialog detection timed out - proceeding anyway');
117
+ }
118
+ /**
119
+ * Send a message to Gemini interactive session
69
120
  *
70
121
  * @param worktreeId - Worktree ID
71
122
  * @param message - Message to send
@@ -78,15 +129,22 @@ class GeminiTool extends base_1.BaseCLITool {
78
129
  throw new Error(`Gemini session ${sessionName} does not exist. Start the session first.`);
79
130
  }
80
131
  try {
81
- // Escape the message for shell execution
82
- const escapedMessage = message.replace(/'/g, "'\\''");
83
- // Execute Gemini in non-interactive mode using stdin piping
84
- // This approach bypasses the TUI and executes in one-shot mode
85
- await (0, tmux_1.sendKeys)(sessionName, `echo '${escapedMessage}' | gemini`, true);
132
+ // Send message to Gemini (without Enter)
133
+ await (0, tmux_1.sendKeys)(sessionName, message, false);
134
+ // Wait a moment for the text to be typed
135
+ await new Promise((resolve) => setTimeout(resolve, 100));
136
+ // Send Enter key separately
137
+ await (0, tmux_1.sendSpecialKey)(sessionName, 'C-m');
138
+ // Wait a moment for the message to be processed
139
+ await new Promise((resolve) => setTimeout(resolve, 200));
140
+ // Detect [Pasted text] and resend Enter for multi-line messages
141
+ if (message.includes('\n')) {
142
+ await (0, pasted_text_helper_1.detectAndResendIfPastedText)(sessionName);
143
+ }
86
144
  console.log(`✓ Sent message to Gemini session: ${sessionName}`);
87
145
  }
88
146
  catch (error) {
89
- const errorMessage = error instanceof Error ? error.message : String(error);
147
+ const errorMessage = getErrorMessage(error);
90
148
  throw new Error(`Failed to send message to Gemini: ${errorMessage}`);
91
149
  }
92
150
  }
@@ -98,12 +156,13 @@ class GeminiTool extends base_1.BaseCLITool {
98
156
  async killSession(worktreeId) {
99
157
  const sessionName = this.getSessionName(worktreeId);
100
158
  try {
101
- // Send Ctrl+D to exit Gemini gracefully
102
159
  const exists = await (0, tmux_1.hasSession)(sessionName);
103
160
  if (exists) {
104
- // Send Ctrl+D (ASCII 4)
105
- await execAsync(`tmux send-keys -t "${sessionName}" C-d`);
106
- // Wait a moment for Gemini to exit
161
+ // Send Ctrl+C to interrupt any running operation
162
+ await (0, tmux_1.sendSpecialKey)(sessionName, 'C-c');
163
+ await new Promise((resolve) => setTimeout(resolve, 300));
164
+ // Send /quit to exit Gemini gracefully
165
+ await (0, tmux_1.sendKeys)(sessionName, '/quit', true);
107
166
  await new Promise((resolve) => setTimeout(resolve, 500));
108
167
  }
109
168
  // Kill the tmux session
@@ -113,7 +172,7 @@ class GeminiTool extends base_1.BaseCLITool {
113
172
  }
114
173
  }
115
174
  catch (error) {
116
- const errorMessage = error instanceof Error ? error.message : String(error);
175
+ const errorMessage = getErrorMessage(error);
117
176
  console.error(`Error stopping Gemini session: ${errorMessage}`);
118
177
  throw error;
119
178
  }
@@ -1,17 +1,18 @@
1
1
  "use strict";
2
2
  /**
3
3
  * CLI Tool Manager
4
- * Singleton class to manage multiple CLI tools (Claude, Codex, Gemini)
4
+ * Singleton class to manage multiple CLI tools (Claude, Codex, Gemini, Vibe Local)
5
5
  */
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
7
  exports.CLIToolManager = void 0;
8
8
  const claude_1 = require("./claude");
9
9
  const codex_1 = require("./codex");
10
10
  const gemini_1 = require("./gemini");
11
+ const vibe_local_1 = require("./vibe-local");
11
12
  const response_poller_1 = require("../response-poller");
12
13
  /**
13
14
  * CLI Tool Manager (Singleton)
14
- * Provides centralized access to all CLI tools
15
+ * Provides centralized access to all CLI tools (Issue #368: includes Vibe Local)
15
16
  */
16
17
  class CLIToolManager {
17
18
  static instance;
@@ -25,6 +26,7 @@ class CLIToolManager {
25
26
  this.tools.set('claude', new claude_1.ClaudeTool());
26
27
  this.tools.set('codex', new codex_1.CodexTool());
27
28
  this.tools.set('gemini', new gemini_1.GeminiTool());
29
+ this.tools.set('vibe-local', new vibe_local_1.VibeLocalTool());
28
30
  }
29
31
  /**
30
32
  * Get singleton instance
@@ -3,10 +3,72 @@
3
3
  * Type definitions and interfaces for CLI tools
4
4
  */
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.CLI_TOOL_IDS = void 0;
6
+ exports.OLLAMA_MODEL_PATTERN = exports.CLI_TOOL_DISPLAY_NAMES = exports.CLI_TOOL_IDS = void 0;
7
+ exports.isCliToolType = isCliToolType;
8
+ exports.getCliToolDisplayName = getCliToolDisplayName;
9
+ exports.getCliToolDisplayNameSafe = getCliToolDisplayNameSafe;
7
10
  /**
8
11
  * CLI Tool IDs constant array
9
12
  * T2.1: Single source of truth for CLI tool IDs
10
13
  * CLIToolType is derived from this constant (DRY principle)
11
14
  */
12
- exports.CLI_TOOL_IDS = ['claude', 'codex', 'gemini'];
15
+ exports.CLI_TOOL_IDS = ['claude', 'codex', 'gemini', 'vibe-local'];
16
+ /**
17
+ * CLI tool display names for UI rendering
18
+ * Issue #368: Centralized display name mapping
19
+ *
20
+ * Usage: UI display (tab headers, message lists, settings).
21
+ * For internal logs/debug, use tool.name (BaseCLITool.name) instead.
22
+ */
23
+ exports.CLI_TOOL_DISPLAY_NAMES = {
24
+ claude: 'Claude',
25
+ codex: 'Codex',
26
+ gemini: 'Gemini',
27
+ 'vibe-local': 'Vibe Local',
28
+ };
29
+ /**
30
+ * Check if a string is a valid CLIToolType
31
+ * Issue #368: Type guard for safe casting of untrusted CLI tool ID strings
32
+ *
33
+ * @param value - String to check
34
+ * @returns True if value is a valid CLIToolType
35
+ */
36
+ function isCliToolType(value) {
37
+ return exports.CLI_TOOL_IDS.includes(value);
38
+ }
39
+ /**
40
+ * Get the display name for a CLI tool ID
41
+ * Issue #368: Centralized display name function for DRY compliance
42
+ *
43
+ * @param id - CLI tool type identifier
44
+ * @returns Human-readable display name
45
+ */
46
+ function getCliToolDisplayName(id) {
47
+ return exports.CLI_TOOL_DISPLAY_NAMES[id] ?? id;
48
+ }
49
+ /**
50
+ * Get the display name for a CLI tool ID string, with fallback for unknown IDs
51
+ * Issue #368: Safe wrapper for UI components receiving untyped cliToolId strings
52
+ *
53
+ * Unlike getCliToolDisplayName(), this accepts optional/untyped strings and
54
+ * returns a fallback value ('Assistant') for null, undefined, or unknown IDs.
55
+ *
56
+ * @param cliToolId - Optional CLI tool ID string (may be untyped)
57
+ * @param fallback - Fallback display name for missing/unknown IDs (default: 'Assistant')
58
+ * @returns Human-readable display name or fallback
59
+ */
60
+ function getCliToolDisplayNameSafe(cliToolId, fallback = 'Assistant') {
61
+ if (!cliToolId)
62
+ return fallback;
63
+ if (isCliToolType(cliToolId))
64
+ return getCliToolDisplayName(cliToolId);
65
+ return fallback;
66
+ }
67
+ /**
68
+ * Ollama model name validation pattern.
69
+ * Allows: alphanumeric start, followed by alphanumeric, dots, underscores, colons, slashes, hyphens.
70
+ * Max 100 characters. Used for defense-in-depth validation at point of use.
71
+ *
72
+ * [SEC-001] Shared between API route validation and CLI command construction
73
+ */
74
+ exports.OLLAMA_MODEL_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9._:/-]*$/;