commandmate 0.3.2 → 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 (111) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/app-build-manifest.json +13 -13
  3. package/.next/app-path-routes-manifest.json +1 -1
  4. package/.next/build-manifest.json +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_client-reference-manifest.js +1 -1
  21. package/.next/server/app/api/app/update-check/route.js +1 -1
  22. package/.next/server/app/api/ollama/models/route.js +1 -0
  23. package/.next/server/app/api/ollama/models/route.js.nft.json +1 -0
  24. package/.next/server/app/api/ollama/models.body +1 -0
  25. package/.next/server/app/api/ollama/models.meta +1 -0
  26. package/.next/server/app/api/repositories/route.js +3 -3
  27. package/.next/server/app/api/repositories/route.js.nft.json +1 -1
  28. package/.next/server/app/api/worktrees/[id]/auto-yes/route.js +1 -1
  29. package/.next/server/app/api/worktrees/[id]/auto-yes/route.js.nft.json +1 -1
  30. package/.next/server/app/api/worktrees/[id]/cli-tool/route.js +1 -1
  31. package/.next/server/app/api/worktrees/[id]/current-output/route.js +1 -1
  32. package/.next/server/app/api/worktrees/[id]/current-output/route.js.nft.json +1 -1
  33. package/.next/server/app/api/worktrees/[id]/execution-logs/[logId]/route.js +1 -1
  34. package/.next/server/app/api/worktrees/[id]/execution-logs/[logId]/route.js.nft.json +1 -1
  35. package/.next/server/app/api/worktrees/[id]/execution-logs/route.js +1 -1
  36. package/.next/server/app/api/worktrees/[id]/execution-logs/route.js.nft.json +1 -1
  37. package/.next/server/app/api/worktrees/[id]/interrupt/route.js +1 -1
  38. package/.next/server/app/api/worktrees/[id]/interrupt/route.js.nft.json +1 -1
  39. package/.next/server/app/api/worktrees/[id]/kill-session/route.js +1 -1
  40. package/.next/server/app/api/worktrees/[id]/kill-session/route.js.nft.json +1 -1
  41. package/.next/server/app/api/worktrees/[id]/messages/route.js +1 -1
  42. package/.next/server/app/api/worktrees/[id]/prompt-response/route.js +1 -1
  43. package/.next/server/app/api/worktrees/[id]/prompt-response/route.js.nft.json +1 -1
  44. package/.next/server/app/api/worktrees/[id]/respond/route.js +1 -1
  45. package/.next/server/app/api/worktrees/[id]/respond/route.js.nft.json +1 -1
  46. package/.next/server/app/api/worktrees/[id]/route.js +1 -1
  47. package/.next/server/app/api/worktrees/[id]/route.js.nft.json +1 -1
  48. package/.next/server/app/api/worktrees/[id]/schedules/[scheduleId]/route.js +1 -1
  49. package/.next/server/app/api/worktrees/[id]/schedules/[scheduleId]/route.js.nft.json +1 -1
  50. package/.next/server/app/api/worktrees/[id]/schedules/route.js +2 -2
  51. package/.next/server/app/api/worktrees/[id]/schedules/route.js.nft.json +1 -1
  52. package/.next/server/app/api/worktrees/[id]/send/route.js +1 -1
  53. package/.next/server/app/api/worktrees/[id]/send/route.js.nft.json +1 -1
  54. package/.next/server/app/api/worktrees/[id]/slash-commands/route.js +1 -1
  55. package/.next/server/app/api/worktrees/[id]/start-polling/route.js +1 -1
  56. package/.next/server/app/api/worktrees/[id]/start-polling/route.js.nft.json +1 -1
  57. package/.next/server/app/api/worktrees/route.js +1 -1
  58. package/.next/server/app/api/worktrees/route.js.nft.json +1 -1
  59. package/.next/server/app/login/page_client-reference-manifest.js +1 -1
  60. package/.next/server/app/page.js +1 -1
  61. package/.next/server/app/page_client-reference-manifest.js +1 -1
  62. package/.next/server/app/worktrees/[id]/files/[...path]/page_client-reference-manifest.js +1 -1
  63. package/.next/server/app/worktrees/[id]/page.js +4 -4
  64. package/.next/server/app/worktrees/[id]/page_client-reference-manifest.js +1 -1
  65. package/.next/server/app/worktrees/[id]/terminal/page_client-reference-manifest.js +1 -1
  66. package/.next/server/app-paths-manifest.json +7 -6
  67. package/.next/server/chunks/2314.js +1 -1
  68. package/.next/server/chunks/4559.js +1 -1
  69. package/.next/server/chunks/539.js +10 -10
  70. package/.next/server/chunks/5853.js +1 -1
  71. package/.next/server/chunks/6228.js +1 -1
  72. package/.next/server/chunks/7425.js +59 -39
  73. package/.next/server/chunks/7566.js +1 -1
  74. package/.next/server/chunks/8693.js +1 -1
  75. package/.next/server/chunks/9446.js +1 -0
  76. package/.next/server/functions-config-manifest.json +1 -1
  77. package/.next/server/middleware-build-manifest.js +1 -1
  78. package/.next/server/middleware-manifest.json +5 -5
  79. package/.next/server/pages/500.html +1 -1
  80. package/.next/server/server-reference-manifest.json +1 -1
  81. package/.next/static/chunks/8091-274bc0716106e7fc.js +1 -0
  82. package/.next/static/chunks/app/page-060057e02b841125.js +1 -0
  83. package/.next/static/chunks/app/worktrees/[id]/page-78580947c201d698.js +1 -0
  84. package/.next/static/chunks/{main-db79434ee4a6c931.js → main-2feda12a4d321111.js} +1 -1
  85. package/.next/static/css/{bd6065b03ddb3efd.css → e85de230ef5ddc40.css} +1 -1
  86. package/.next/trace +5 -5
  87. package/.next/types/app/api/ollama/models/route.ts +343 -0
  88. package/README.md +74 -76
  89. package/dist/server/src/config/schedule-config.js +7 -1
  90. package/dist/server/src/lib/auto-yes-manager.js +2 -2
  91. package/dist/server/src/lib/claude-executor.js +15 -4
  92. package/dist/server/src/lib/cli-patterns.js +73 -9
  93. package/dist/server/src/lib/cli-tools/gemini.js +81 -22
  94. package/dist/server/src/lib/cli-tools/manager.js +4 -2
  95. package/dist/server/src/lib/cli-tools/types.js +64 -2
  96. package/dist/server/src/lib/cli-tools/vibe-local.js +163 -0
  97. package/dist/server/src/lib/cmate-parser.js +25 -3
  98. package/dist/server/src/lib/db-migrations.js +50 -1
  99. package/dist/server/src/lib/db.js +51 -1
  100. package/dist/server/src/lib/prompt-detector.js +4 -3
  101. package/dist/server/src/lib/response-poller.js +22 -11
  102. package/dist/server/src/lib/schedule-manager.js +6 -2
  103. package/dist/server/src/lib/selected-agents-validator.js +99 -0
  104. package/dist/server/src/types/sidebar.js +9 -4
  105. package/package.json +1 -1
  106. package/.next/server/chunks/7536.js +0 -1
  107. package/.next/static/chunks/8091-925542bdfc843dce.js +0 -1
  108. package/.next/static/chunks/app/page-238b5a70d8c101e9.js +0 -1
  109. package/.next/static/chunks/app/worktrees/[id]/page-0c889ab3f30d5af7.js +0 -1
  110. /package/.next/static/{j8HFvzDZj7tHjAnhpXUno → O7EDFfAYQNe_HRbORxQAC}/_buildManifest.js +0 -0
  111. /package/.next/static/{j8HFvzDZj7tHjAnhpXUno → O7EDFfAYQNe_HRbORxQAC}/_ssgManifest.js +0 -0
@@ -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._:/-]*$/;
@@ -0,0 +1,163 @@
1
+ "use strict";
2
+ /**
3
+ * Vibe Local CLI tool implementation
4
+ * Provides integration with vibe-local (vibe-coder) in interactive mode
5
+ *
6
+ * @remarks Issue #368: Rewritten from non-interactive pipe mode to interactive REPL mode.
7
+ * Previous implementation used `echo 'msg' | vibe-local` which caused the process to exit
8
+ * immediately with "(Cancelled)" + "Goodbye!", making response polling impossible.
9
+ * Now launches `vibe-local -y` in interactive mode within tmux (same approach as Claude/Codex/Gemini).
10
+ */
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.VibeLocalTool = void 0;
13
+ const base_1 = require("./base");
14
+ const types_1 = require("./types");
15
+ const tmux_1 = require("../tmux");
16
+ const pasted_text_helper_1 = require("../pasted-text-helper");
17
+ const db_instance_1 = require("../db-instance");
18
+ const db_1 = require("../db");
19
+ /**
20
+ * Extract error message from unknown error type (DRY)
21
+ */
22
+ function getErrorMessage(error) {
23
+ return error instanceof Error ? error.message : String(error);
24
+ }
25
+ /**
26
+ * Wait for vibe-local to initialize after launch.
27
+ * vibe-local shows a permission check prompt, banner, and model loading.
28
+ */
29
+ const VIBE_LOCAL_INIT_WAIT_MS = 5000;
30
+ /**
31
+ * Vibe Local CLI tool implementation
32
+ * Manages vibe-local interactive sessions using tmux
33
+ */
34
+ class VibeLocalTool extends base_1.BaseCLITool {
35
+ id = 'vibe-local';
36
+ name = 'Vibe Local';
37
+ command = 'vibe-local';
38
+ /**
39
+ * Check if vibe-local session is running for a worktree
40
+ */
41
+ async isRunning(worktreeId) {
42
+ const sessionName = this.getSessionName(worktreeId);
43
+ return await (0, tmux_1.hasSession)(sessionName);
44
+ }
45
+ /**
46
+ * Start a new vibe-local session for a worktree
47
+ * Launches `vibe-local -y` in interactive mode within tmux
48
+ *
49
+ * @param worktreeId - Worktree ID
50
+ * @param worktreePath - Worktree path
51
+ */
52
+ async startSession(worktreeId, worktreePath) {
53
+ const vibeLocalAvailable = await this.isInstalled();
54
+ if (!vibeLocalAvailable) {
55
+ throw new Error('vibe-local is not installed or not in PATH');
56
+ }
57
+ const sessionName = this.getSessionName(worktreeId);
58
+ const exists = await (0, tmux_1.hasSession)(sessionName);
59
+ if (exists) {
60
+ console.log(`Vibe Local session ${sessionName} already exists`);
61
+ return;
62
+ }
63
+ try {
64
+ // Create tmux session with large history buffer
65
+ await (0, tmux_1.createSession)({
66
+ sessionName,
67
+ workingDirectory: worktreePath,
68
+ historyLimit: 50000,
69
+ });
70
+ // Wait a moment for the session to be created
71
+ await new Promise((resolve) => setTimeout(resolve, 100));
72
+ // Read Ollama model preference from DB
73
+ // [SEC-001] Re-validate model name at point of use (defense-in-depth)
74
+ let vibeLocalCommand = 'vibe-local -y';
75
+ try {
76
+ const db = (0, db_instance_1.getDbInstance)();
77
+ const wt = (0, db_1.getWorktreeById)(db, worktreeId);
78
+ if (wt?.vibeLocalModel && types_1.OLLAMA_MODEL_PATTERN.test(wt.vibeLocalModel)) {
79
+ vibeLocalCommand = `vibe-local -y -m ${wt.vibeLocalModel}`;
80
+ }
81
+ }
82
+ catch {
83
+ // DB read failure is non-fatal; use default model
84
+ }
85
+ // Start vibe-local in interactive mode with auto-approve (-y)
86
+ // -y flag skips the permission confirmation prompt
87
+ await (0, tmux_1.sendKeys)(sessionName, vibeLocalCommand, true);
88
+ // Wait for vibe-local to initialize (banner + model loading)
89
+ await new Promise((resolve) => setTimeout(resolve, VIBE_LOCAL_INIT_WAIT_MS));
90
+ console.log(`✓ Started Vibe Local session: ${sessionName}`);
91
+ }
92
+ catch (error) {
93
+ const errorMessage = getErrorMessage(error);
94
+ throw new Error(`Failed to start Vibe Local session: ${errorMessage}`);
95
+ }
96
+ }
97
+ /**
98
+ * Send a message to vibe-local interactive session
99
+ *
100
+ * @param worktreeId - Worktree ID
101
+ * @param message - Message to send
102
+ */
103
+ async sendMessage(worktreeId, message) {
104
+ const sessionName = this.getSessionName(worktreeId);
105
+ const exists = await (0, tmux_1.hasSession)(sessionName);
106
+ if (!exists) {
107
+ throw new Error(`Vibe Local session ${sessionName} does not exist. Start the session first.`);
108
+ }
109
+ try {
110
+ // Send message to vibe-local (without Enter)
111
+ await (0, tmux_1.sendKeys)(sessionName, message, false);
112
+ // Wait a moment for the text to be typed
113
+ await new Promise((resolve) => setTimeout(resolve, 100));
114
+ // vibe-local uses IME mode: first Enter creates a new line,
115
+ // second Enter on empty line submits the message.
116
+ // Send Enter twice with a short delay between.
117
+ await (0, tmux_1.sendSpecialKey)(sessionName, 'C-m');
118
+ await new Promise((resolve) => setTimeout(resolve, 200));
119
+ await (0, tmux_1.sendSpecialKey)(sessionName, 'C-m');
120
+ // Wait a moment for the message to be processed
121
+ await new Promise((resolve) => setTimeout(resolve, 200));
122
+ // Detect [Pasted text] and resend Enter for multi-line messages
123
+ if (message.includes('\n')) {
124
+ await (0, pasted_text_helper_1.detectAndResendIfPastedText)(sessionName);
125
+ }
126
+ console.log(`✓ Sent message to Vibe Local session: ${sessionName}`);
127
+ }
128
+ catch (error) {
129
+ const errorMessage = getErrorMessage(error);
130
+ throw new Error(`Failed to send message to Vibe Local: ${errorMessage}`);
131
+ }
132
+ }
133
+ /**
134
+ * Kill vibe-local session
135
+ *
136
+ * @param worktreeId - Worktree ID
137
+ */
138
+ async killSession(worktreeId) {
139
+ const sessionName = this.getSessionName(worktreeId);
140
+ try {
141
+ const exists = await (0, tmux_1.hasSession)(sessionName);
142
+ if (exists) {
143
+ // Send Ctrl+C to interrupt any running operation
144
+ await (0, tmux_1.sendSpecialKey)(sessionName, 'C-c');
145
+ await new Promise((resolve) => setTimeout(resolve, 300));
146
+ // Send Ctrl+C again to ensure exit
147
+ await (0, tmux_1.sendSpecialKey)(sessionName, 'C-c');
148
+ await new Promise((resolve) => setTimeout(resolve, 500));
149
+ }
150
+ // Kill the tmux session
151
+ const killed = await (0, tmux_1.killSession)(sessionName);
152
+ if (killed) {
153
+ console.log(`✓ Stopped Vibe Local session: ${sessionName}`);
154
+ }
155
+ }
156
+ catch (error) {
157
+ const errorMessage = getErrorMessage(error);
158
+ console.error(`Error stopping Vibe Local session: ${errorMessage}`);
159
+ throw error;
160
+ }
161
+ }
162
+ }
163
+ exports.VibeLocalTool = VibeLocalTool;
@@ -24,6 +24,7 @@ exports.parseSchedulesSection = parseSchedulesSection;
24
24
  exports.readCmateFile = readCmateFile;
25
25
  const fs_1 = require("fs");
26
26
  const path_1 = __importDefault(require("path"));
27
+ const types_1 = require("../lib/cli-tools/types");
27
28
  const schedule_config_1 = require("../config/schedule-config");
28
29
  const cmate_constants_1 = require("../config/cmate-constants");
29
30
  Object.defineProperty(exports, "CMATE_FILENAME", { enumerable: true, get: function () { return cmate_constants_1.CMATE_FILENAME; } });
@@ -193,13 +194,34 @@ function parseSchedulesSection(rows) {
193
194
  const enabled = enabledStr === undefined ||
194
195
  enabledStr === '' ||
195
196
  enabledStr.toLowerCase() === 'true';
196
- // Parse permission with validation
197
+ // Parse and validate CLI tool ID [SEC-002]
197
198
  const resolvedCliToolId = cliToolId?.trim() || 'claude';
199
+ if (!(0, types_1.isCliToolType)(resolvedCliToolId)) {
200
+ console.warn(`[cmate-parser] Skipping entry "${sanitizedName}" with invalid CLI tool: "${resolvedCliToolId}"`);
201
+ continue;
202
+ }
198
203
  const defaultPermission = schedule_config_1.DEFAULT_PERMISSIONS[resolvedCliToolId] ?? '';
199
204
  let permission = permissionStr?.trim() || defaultPermission;
200
205
  // Validate permission against allowed values
201
- const allowedValues = resolvedCliToolId === 'codex' ? schedule_config_1.CODEX_SANDBOXES : schedule_config_1.CLAUDE_PERMISSIONS;
202
- if (permission && !allowedValues.includes(permission)) {
206
+ let allowedValues;
207
+ switch (resolvedCliToolId) {
208
+ case 'codex':
209
+ allowedValues = schedule_config_1.CODEX_SANDBOXES;
210
+ break;
211
+ case 'gemini':
212
+ case 'vibe-local':
213
+ // No permission flags for gemini/vibe-local; only empty string is valid
214
+ allowedValues = [];
215
+ if (permission) {
216
+ console.warn(`[cmate-parser] Permission "${permission}" ignored for ${resolvedCliToolId} in entry "${sanitizedName}" (no permission flags supported)`);
217
+ permission = '';
218
+ }
219
+ break;
220
+ default:
221
+ allowedValues = schedule_config_1.CLAUDE_PERMISSIONS;
222
+ break;
223
+ }
224
+ if (allowedValues.length > 0 && permission && !allowedValues.includes(permission)) {
203
225
  console.warn(`[cmate-parser] Invalid permission "${permission}" for ${resolvedCliToolId} in entry "${sanitizedName}", using default "${defaultPermission}"`);
204
226
  permission = defaultPermission;
205
227
  }
@@ -19,7 +19,7 @@ const db_1 = require("./db");
19
19
  * Current schema version
20
20
  * Increment this when adding new migrations
21
21
  */
22
- exports.CURRENT_SCHEMA_VERSION = 17;
22
+ exports.CURRENT_SCHEMA_VERSION = 19;
23
23
  /**
24
24
  * Migration registry
25
25
  * All migrations should be added to this array in order
@@ -811,6 +811,55 @@ const migrations = [
811
811
  db.exec('DROP TABLE IF EXISTS scheduled_executions');
812
812
  console.log('✓ Dropped scheduled_executions and execution_logs tables');
813
813
  }
814
+ },
815
+ {
816
+ version: 18,
817
+ name: 'add-selected-agents-column',
818
+ up: (db) => {
819
+ // Issue #368: Add selected_agents column for agent selection persistence
820
+ // NOTE (R1-010): The literal values 'claude', 'codex' in the SQL CASE below
821
+ // are fixed at migration time and do NOT sync with TypeScript CLI_TOOL_IDS.
822
+ // Changes to CLI_TOOL_IDS will not retroactively affect already-migrated data.
823
+ // Migration tests cover all CLIToolType values to catch sync issues.
824
+ // Step 1: Add column
825
+ db.exec(`
826
+ ALTER TABLE worktrees ADD COLUMN selected_agents TEXT;
827
+ `);
828
+ // Step 2: Initialize existing data based on cli_tool_id
829
+ // - If cli_tool_id is 'claude' or 'codex' -> default ["claude","codex"]
830
+ // - Otherwise (e.g. 'gemini', 'vibe-local') -> [cli_tool_id, "claude"]
831
+ db.exec(`
832
+ UPDATE worktrees SET selected_agents =
833
+ CASE
834
+ WHEN cli_tool_id NOT IN ('claude', 'codex')
835
+ THEN json_array(cli_tool_id, 'claude')
836
+ ELSE '["claude","codex"]'
837
+ END;
838
+ `);
839
+ console.log('✓ Added selected_agents column to worktrees table');
840
+ console.log('✓ Initialized selected_agents based on cli_tool_id');
841
+ },
842
+ down: () => {
843
+ // selected_agents is a nullable TEXT column; dropping it requires table recreation
844
+ // which is disproportionate for a rollback. The column is harmless if unused.
845
+ console.log('No rollback for selected_agents column (SQLite limitation)');
846
+ }
847
+ },
848
+ {
849
+ version: 19,
850
+ name: 'add-vibe-local-model-column',
851
+ up: (db) => {
852
+ // Issue #368: Add vibe_local_model column for Ollama model selection
853
+ // NULL means use the default model (vibe-local decides)
854
+ db.exec(`
855
+ ALTER TABLE worktrees ADD COLUMN vibe_local_model TEXT DEFAULT NULL;
856
+ `);
857
+ console.log('✓ Added vibe_local_model column to worktrees table');
858
+ },
859
+ down: () => {
860
+ // vibe_local_model is a nullable TEXT column; harmless if unused
861
+ console.log('No rollback for vibe_local_model column (SQLite limitation)');
862
+ }
814
863
  }
815
864
  ];
816
865
  /**