commandmate 0.3.4 → 0.3.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.next/BUILD_ID +1 -1
- package/.next/app-build-manifest.json +12 -12
- package/.next/app-path-routes-manifest.json +1 -1
- package/.next/build-manifest.json +5 -5
- package/.next/cache/.tsbuildinfo +1 -1
- package/.next/cache/config.json +3 -3
- package/.next/cache/webpack/client-production/0.pack +0 -0
- package/.next/cache/webpack/client-production/1.pack +0 -0
- package/.next/cache/webpack/client-production/2.pack +0 -0
- package/.next/cache/webpack/client-production/index.pack +0 -0
- package/.next/cache/webpack/client-production/index.pack.old +0 -0
- package/.next/cache/webpack/edge-server-production/0.pack +0 -0
- package/.next/cache/webpack/edge-server-production/index.pack +0 -0
- package/.next/cache/webpack/server-production/0.pack +0 -0
- package/.next/cache/webpack/server-production/index.pack +0 -0
- package/.next/next-server.js.nft.json +1 -1
- package/.next/prerender-manifest.json +1 -1
- package/.next/react-loadable-manifest.json +13 -7
- package/.next/required-server-files.json +1 -1
- package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/server/app/api/app/update-check/route.js +1 -1
- package/.next/server/app/api/repositories/route.js +2 -2
- package/.next/server/app/api/repositories/route.js.nft.json +1 -1
- package/.next/server/app/api/worktrees/[id]/auto-yes/route.js +1 -1
- package/.next/server/app/api/worktrees/[id]/auto-yes/route.js.nft.json +1 -1
- package/.next/server/app/api/worktrees/[id]/current-output/route.js +1 -1
- package/.next/server/app/api/worktrees/[id]/current-output/route.js.nft.json +1 -1
- package/.next/server/app/api/worktrees/[id]/execution-logs/[logId]/route.js +1 -1
- package/.next/server/app/api/worktrees/[id]/execution-logs/[logId]/route.js.nft.json +1 -1
- package/.next/server/app/api/worktrees/[id]/execution-logs/route.js +1 -1
- package/.next/server/app/api/worktrees/[id]/execution-logs/route.js.nft.json +1 -1
- package/.next/server/app/api/worktrees/[id]/interrupt/route.js +1 -1
- package/.next/server/app/api/worktrees/[id]/interrupt/route.js.nft.json +1 -1
- package/.next/server/app/api/worktrees/[id]/kill-session/route.js +1 -1
- package/.next/server/app/api/worktrees/[id]/kill-session/route.js.nft.json +1 -1
- package/.next/server/app/api/worktrees/[id]/prompt-response/route.js +1 -1
- package/.next/server/app/api/worktrees/[id]/prompt-response/route.js.nft.json +1 -1
- package/.next/server/app/api/worktrees/[id]/respond/route.js +1 -1
- package/.next/server/app/api/worktrees/[id]/respond/route.js.nft.json +1 -1
- package/.next/server/app/api/worktrees/[id]/route.js +1 -1
- package/.next/server/app/api/worktrees/[id]/route.js.nft.json +1 -1
- package/.next/server/app/api/worktrees/[id]/schedules/[scheduleId]/route.js +1 -1
- package/.next/server/app/api/worktrees/[id]/schedules/[scheduleId]/route.js.nft.json +1 -1
- package/.next/server/app/api/worktrees/[id]/schedules/route.js +1 -1
- package/.next/server/app/api/worktrees/[id]/schedules/route.js.nft.json +1 -1
- package/.next/server/app/api/worktrees/[id]/send/route.js +1 -1
- package/.next/server/app/api/worktrees/[id]/send/route.js.nft.json +1 -1
- package/.next/server/app/api/worktrees/[id]/slash-commands/route.js +1 -1
- package/.next/server/app/api/worktrees/[id]/start-polling/route.js +1 -1
- package/.next/server/app/api/worktrees/[id]/start-polling/route.js.nft.json +1 -1
- package/.next/server/app/api/worktrees/route.js +1 -1
- package/.next/server/app/api/worktrees/route.js.nft.json +1 -1
- package/.next/server/app/login/page.js +1 -1
- package/.next/server/app/login/page_client-reference-manifest.js +1 -1
- package/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/server/app/worktrees/[id]/files/[...path]/page_client-reference-manifest.js +1 -1
- package/.next/server/app/worktrees/[id]/page.js +4 -4
- package/.next/server/app/worktrees/[id]/page_client-reference-manifest.js +1 -1
- package/.next/server/app/worktrees/[id]/terminal/page_client-reference-manifest.js +1 -1
- package/.next/server/app-paths-manifest.json +7 -7
- package/.next/server/chunks/3074.js +1 -1
- package/.next/server/chunks/4952.js +1 -0
- package/.next/server/chunks/539.js +3 -3
- package/.next/server/chunks/5795.js +1 -1
- package/.next/server/chunks/7425.js +28 -25
- package/.next/server/chunks/7566.js +1 -1
- package/.next/server/chunks/8693.js +1 -1
- package/.next/server/middleware-build-manifest.js +1 -1
- package/.next/server/middleware-manifest.json +5 -5
- package/.next/server/middleware-react-loadable-manifest.js +1 -1
- package/.next/server/pages/500.html +1 -1
- package/.next/server/pages-manifest.json +1 -1
- package/.next/server/server-reference-manifest.json +1 -1
- package/.next/static/chunks/{4327.740cc7fe2d0b5049.js → 4327.157a4c226d919531.js} +14 -14
- package/.next/static/chunks/5970.0df906ad5a9c9147.js +1 -0
- package/.next/static/chunks/{8091-c0e955616dd86f82.js → 8091-d65d2ab6daed23c6.js} +1 -1
- package/.next/static/chunks/app/login/page-010f02fd4b0dbc48.js +1 -0
- package/.next/static/chunks/app/worktrees/[id]/page-8fb4dc30b58a5681.js +1 -0
- package/.next/static/chunks/webpack-81c97591dd5567ac.js +1 -0
- package/.next/static/css/45b3a41370668314.css +3 -0
- package/.next/trace +5 -5
- package/dist/server/src/lib/claude-executor.js +14 -3
- package/dist/server/src/lib/cli-patterns.js +99 -20
- package/dist/server/src/lib/cli-tools/manager.js +5 -3
- package/dist/server/src/lib/cli-tools/opencode-config.js +236 -0
- package/dist/server/src/lib/cli-tools/opencode.js +188 -0
- package/dist/server/src/lib/cli-tools/types.js +12 -5
- package/dist/server/src/lib/db.js +18 -0
- package/dist/server/src/lib/response-poller.js +367 -19
- package/package.json +2 -1
- package/.next/server/chunks/9446.js +0 -1
- package/.next/static/chunks/app/login/page-2d42204ba87cd136.js +0 -1
- package/.next/static/chunks/app/worktrees/[id]/page-9c0c64488c17db3c.js +0 -1
- package/.next/static/chunks/webpack-3c0ee3ce5b546818.js +0 -1
- package/.next/static/css/fa3df0e6f437f2ba.css +0 -3
- /package/.next/static/{BiyH3zkbySg7ZWTeZuXqj → p3hosTZoJ22r35fWwUoLr}/_buildManifest.js +0 -0
- /package/.next/static/{BiyH3zkbySg7ZWTeZuXqj → p3hosTZoJ22r35fWwUoLr}/_ssgManifest.js +0 -0
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
4
|
-
* Issue #294: Executes
|
|
3
|
+
* CLI Command Executor (non-interactive mode)
|
|
4
|
+
* Issue #294: Executes CLI tool commands for scheduled executions
|
|
5
|
+
* Issue #379: Added OpenCode support (opencode run)
|
|
6
|
+
*
|
|
7
|
+
* Supported tools: claude, codex, gemini, vibe-local, opencode
|
|
5
8
|
*
|
|
6
9
|
* Security:
|
|
7
10
|
* - Uses execFile (not exec) to prevent shell injection
|
|
8
11
|
* - Sanitizes environment variables via env-sanitizer.ts
|
|
9
12
|
* - Limits output size to prevent memory exhaustion
|
|
10
13
|
* - Enforces execution timeout
|
|
14
|
+
* - Validates cliToolId against ALLOWED_CLI_TOOLS whitelist [SEC-001]
|
|
11
15
|
*/
|
|
12
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
17
|
exports.ALLOWED_CLI_TOOLS = exports.MAX_MESSAGE_LENGTH = exports.EXECUTION_TIMEOUT_MS = exports.MAX_STORED_OUTPUT_SIZE = exports.MAX_OUTPUT_SIZE = void 0;
|
|
@@ -30,7 +34,7 @@ exports.EXECUTION_TIMEOUT_MS = 5 * 60 * 1000;
|
|
|
30
34
|
/** Maximum message length sent to claude -p */
|
|
31
35
|
exports.MAX_MESSAGE_LENGTH = 10000;
|
|
32
36
|
/** Allowed CLI tool identifiers for scheduled execution */
|
|
33
|
-
exports.ALLOWED_CLI_TOOLS = new Set(['claude', 'codex', 'gemini', 'vibe-local']);
|
|
37
|
+
exports.ALLOWED_CLI_TOOLS = new Set(['claude', 'codex', 'gemini', 'vibe-local', 'opencode']);
|
|
34
38
|
// =============================================================================
|
|
35
39
|
// Executor
|
|
36
40
|
// =============================================================================
|
|
@@ -57,6 +61,7 @@ function truncateOutput(output) {
|
|
|
57
61
|
* - codex: exec <message> --sandbox <permission>
|
|
58
62
|
* - gemini: -p <message>
|
|
59
63
|
* - vibe-local: [-p <message> -y] or [--model <model> -p <message> -y]
|
|
64
|
+
* - opencode: [run <message>] or [run -m ollama/<model> <message>]
|
|
60
65
|
* - others: -p <message> (fallback)
|
|
61
66
|
*
|
|
62
67
|
* @param message - Prompt message
|
|
@@ -76,6 +81,12 @@ function buildCliArgs(message, cliToolId, permission, options) {
|
|
|
76
81
|
return ['--model', options.model, '-p', message, '-y'];
|
|
77
82
|
}
|
|
78
83
|
return ['-p', message, '-y'];
|
|
84
|
+
case 'opencode':
|
|
85
|
+
// [D2-007] When model is not specified, OpenCode uses opencode.json default model
|
|
86
|
+
if (options?.model) {
|
|
87
|
+
return ['run', '-m', `ollama/${options.model}`, message];
|
|
88
|
+
}
|
|
89
|
+
return ['run', message];
|
|
79
90
|
case 'claude':
|
|
80
91
|
default:
|
|
81
92
|
return ['-p', message, '--output-format', 'text', '--permission-mode', permission ?? 'acceptEdits'];
|
|
@@ -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.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;
|
|
7
|
+
exports.CLAUDE_SESSION_ERROR_REGEX_PATTERNS = exports.CLAUDE_SESSION_ERROR_PATTERNS = exports.VIBE_LOCAL_THINKING_PATTERN = exports.VIBE_LOCAL_PROMPT_PATTERN = exports.OPENCODE_SKIP_PATTERNS = exports.OPENCODE_SEPARATOR_PATTERN = exports.OPENCODE_PROCESSING_INDICATOR = exports.OPENCODE_RESPONSE_COMPLETE = exports.OPENCODE_LOADING_PATTERN = exports.OPENCODE_THINKING_PATTERN = exports.OPENCODE_PROMPT_AFTER_RESPONSE = exports.OPENCODE_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;
|
|
@@ -124,6 +124,66 @@ exports.GEMINI_PROMPT_PATTERN = /^[>❯]\s*$/m;
|
|
|
124
124
|
* Gemini CLI shows braille spinner characters and status text while processing.
|
|
125
125
|
*/
|
|
126
126
|
exports.GEMINI_THINKING_PATTERN = /[\u2800-\u28FF]|Thinking\.\.\./;
|
|
127
|
+
/**
|
|
128
|
+
* OpenCode prompt pattern (Issue #379)
|
|
129
|
+
* OpenCode TUI shows "Ask anything..." in the input area when waiting for user input.
|
|
130
|
+
* Unlike Claude/Codex (which use > or ❯), OpenCode uses a text-based prompt indicator.
|
|
131
|
+
*/
|
|
132
|
+
exports.OPENCODE_PROMPT_PATTERN = /Ask anything\.\.\./;
|
|
133
|
+
/**
|
|
134
|
+
* OpenCode prompt pattern after response completion (Issue #379)
|
|
135
|
+
* Shows "tab agents ctrl+p commands" in the TUI status bar after a response finishes.
|
|
136
|
+
* Used as extraction stop condition in response-poller.ts [D2-003].
|
|
137
|
+
*/
|
|
138
|
+
exports.OPENCODE_PROMPT_AFTER_RESPONSE = /tab agents\s+ctrl\+p commands/;
|
|
139
|
+
/**
|
|
140
|
+
* OpenCode thinking/processing pattern (Issue #379)
|
|
141
|
+
* OpenCode TUI shows "Thinking:" prefix while the Ollama model is generating a response.
|
|
142
|
+
* Used by detectThinking() to determine if the tool is actively processing.
|
|
143
|
+
*/
|
|
144
|
+
exports.OPENCODE_THINKING_PATTERN = /Thinking:/;
|
|
145
|
+
/**
|
|
146
|
+
* OpenCode loading indicator pattern (Issue #379)
|
|
147
|
+
* Shows a series of 4+ filled square characters (U+2B1D) during initial loading/model warm-up.
|
|
148
|
+
* Filtered from response extraction via OPENCODE_SKIP_PATTERNS.
|
|
149
|
+
*/
|
|
150
|
+
exports.OPENCODE_LOADING_PATTERN = /\u2B1D{4,}/;
|
|
151
|
+
/**
|
|
152
|
+
* OpenCode response completion pattern (Issue #379)
|
|
153
|
+
* Matches the action summary line: "▣ {Action} · model" with optional timing "· Ns".
|
|
154
|
+
* (U+25A3 square + action word + middle dot + model name [+ middle dot + timing]).
|
|
155
|
+
* Action can be "Build", "Compaction", or other OpenCode action names.
|
|
156
|
+
* Short responses may omit the timing portion (e.g., "▣ Build · qwen3.5:27b").
|
|
157
|
+
* This is the primary completion signal for OpenCode [D2-002].
|
|
158
|
+
*/
|
|
159
|
+
exports.OPENCODE_RESPONSE_COMPLETE = /\u25A3\s+\w+\s+\u00b7\s+\S+(?:\s+\u00b7\s+(?:[\d]+h\s*)?(?:[\d]+m\s*)?[\d.]+s)?/;
|
|
160
|
+
/**
|
|
161
|
+
* OpenCode processing indicator pattern (Issue #379)
|
|
162
|
+
* Shows "esc interrupt" in the TUI status bar during active model processing.
|
|
163
|
+
* Filtered from response extraction via OPENCODE_SKIP_PATTERNS.
|
|
164
|
+
*/
|
|
165
|
+
exports.OPENCODE_PROCESSING_INDICATOR = /esc interrupt/;
|
|
166
|
+
/**
|
|
167
|
+
* OpenCode TUI separator pattern (Issue #379)
|
|
168
|
+
* Matches lines composed entirely of box-drawing / TUI decoration characters.
|
|
169
|
+
* Covers: vertical lines (U+2503), box corners, horizontal lines, and other TUI elements.
|
|
170
|
+
*/
|
|
171
|
+
exports.OPENCODE_SEPARATOR_PATTERN = /^[\u2503\u2579\u25A3\u2580\u2500\u250C\u2510\u2514\u2518\u251C\u2524\u252C\u2534\u253C]+$/;
|
|
172
|
+
/**
|
|
173
|
+
* OpenCode skip patterns for response cleaning (Issue #379)
|
|
174
|
+
* Lines matching any of these patterns are filtered from extracted responses.
|
|
175
|
+
* Includes: TUI separators, loading indicators, Build summary prefix,
|
|
176
|
+
* status bar prompts, processing indicators, input prompt, and pasted text markers.
|
|
177
|
+
*/
|
|
178
|
+
exports.OPENCODE_SKIP_PATTERNS = [
|
|
179
|
+
exports.OPENCODE_SEPARATOR_PATTERN,
|
|
180
|
+
exports.OPENCODE_LOADING_PATTERN,
|
|
181
|
+
/^Build\s+/,
|
|
182
|
+
exports.OPENCODE_PROMPT_AFTER_RESPONSE,
|
|
183
|
+
exports.OPENCODE_PROCESSING_INDICATOR,
|
|
184
|
+
exports.OPENCODE_PROMPT_PATTERN,
|
|
185
|
+
exports.PASTED_TEXT_PATTERN,
|
|
186
|
+
];
|
|
127
187
|
/**
|
|
128
188
|
* Vibe Local prompt pattern
|
|
129
189
|
* vibe-local (vibe-coder) shows `ctx:N% ❯` prompt when waiting for user input.
|
|
@@ -157,6 +217,9 @@ function detectThinking(cliToolId, content) {
|
|
|
157
217
|
case 'vibe-local':
|
|
158
218
|
result = exports.VIBE_LOCAL_THINKING_PATTERN.test(content);
|
|
159
219
|
break;
|
|
220
|
+
case 'opencode':
|
|
221
|
+
result = exports.OPENCODE_THINKING_PATTERN.test(content);
|
|
222
|
+
break;
|
|
160
223
|
default:
|
|
161
224
|
result = exports.CLAUDE_THINKING_PATTERN.test(content);
|
|
162
225
|
}
|
|
@@ -241,6 +304,13 @@ function getCliToolPatterns(cliToolId) {
|
|
|
241
304
|
exports.PASTED_TEXT_PATTERN, // [Pasted text #N +XX lines]
|
|
242
305
|
],
|
|
243
306
|
};
|
|
307
|
+
case 'opencode':
|
|
308
|
+
return {
|
|
309
|
+
promptPattern: exports.OPENCODE_PROMPT_PATTERN,
|
|
310
|
+
separatorPattern: exports.OPENCODE_SEPARATOR_PATTERN,
|
|
311
|
+
thinkingPattern: exports.OPENCODE_THINKING_PATTERN,
|
|
312
|
+
skipPatterns: [...exports.OPENCODE_SKIP_PATTERNS],
|
|
313
|
+
};
|
|
244
314
|
default:
|
|
245
315
|
// Default to Claude patterns
|
|
246
316
|
return getCliToolPatterns('claude');
|
|
@@ -279,28 +349,16 @@ function stripAnsi(str) {
|
|
|
279
349
|
*/
|
|
280
350
|
function stripBoxDrawing(str) {
|
|
281
351
|
return str.split('\n').map(line => {
|
|
282
|
-
// Remove border-only lines (╭──╮, ╰──╯, │ only, etc.)
|
|
283
|
-
|
|
352
|
+
// Remove border-only lines (╭──╮, ╰──╯, │ only, ┃ only, ╹▀▀▀, etc.)
|
|
353
|
+
// U+2502 │ (light vertical), U+2503 ┃ (heavy vertical - OpenCode TUI)
|
|
354
|
+
// U+2579 ╹ (heavy up), U+2580 ▀ (upper half block - OpenCode separator)
|
|
355
|
+
if (/^[\u2502\u2503\u256D\u256E\u256F\u2570\u2500\u2579\u2580\s]+$/.test(line))
|
|
284
356
|
return '';
|
|
285
|
-
// Strip leading
|
|
286
|
-
|
|
357
|
+
// Strip leading whitespace + │/┃ + optional space, trailing space + │/┃
|
|
358
|
+
// OpenCode TUI adds 2-space padding before ┃ borders (e.g., " ┃ content")
|
|
359
|
+
return line.replace(/^\s*[\u2502\u2503]\s?/, '').replace(/\s*[\u2502\u2503]$/, '');
|
|
287
360
|
}).join('\n');
|
|
288
361
|
}
|
|
289
|
-
/**
|
|
290
|
-
* Build DetectPromptOptions for a given CLI tool.
|
|
291
|
-
* Centralizes cliToolId-to-options mapping logic (DRY - MF-001).
|
|
292
|
-
*
|
|
293
|
-
* prompt-detector.ts remains CLI tool independent (Issue #161 principle);
|
|
294
|
-
* this function lives in cli-patterns.ts which already depends on CLIToolType.
|
|
295
|
-
*
|
|
296
|
-
* [Future extension memo (C-002)]
|
|
297
|
-
* If CLI tool count grows significantly (currently 3), consider migrating
|
|
298
|
-
* to a CLIToolConfig registry pattern where tool-specific settings
|
|
299
|
-
* (including promptDetectionOptions) are managed in a Record<CLIToolType, CLIToolConfig>.
|
|
300
|
-
*
|
|
301
|
-
* @param cliToolId - CLI tool identifier
|
|
302
|
-
* @returns DetectPromptOptions for the tool, or undefined for default behavior
|
|
303
|
-
*/
|
|
304
362
|
/**
|
|
305
363
|
* Error patterns that indicate a Claude session failed to start properly
|
|
306
364
|
* Used by isSessionHealthy() to detect broken sessions (MF-001: SRP)
|
|
@@ -334,9 +392,30 @@ exports.CLAUDE_SESSION_ERROR_PATTERNS = [
|
|
|
334
392
|
exports.CLAUDE_SESSION_ERROR_REGEX_PATTERNS = [
|
|
335
393
|
/^Error:.*Claude Code/,
|
|
336
394
|
];
|
|
395
|
+
/**
|
|
396
|
+
* Build DetectPromptOptions for a given CLI tool.
|
|
397
|
+
* Centralizes cliToolId-to-options mapping logic (DRY - MF-001).
|
|
398
|
+
*
|
|
399
|
+
* prompt-detector.ts remains CLI tool independent (Issue #161 principle);
|
|
400
|
+
* this function lives in cli-patterns.ts which already depends on CLIToolType.
|
|
401
|
+
*
|
|
402
|
+
* [Future extension memo (C-002)]
|
|
403
|
+
* If CLI tool count grows significantly (currently 5), consider migrating
|
|
404
|
+
* to a CLIToolConfig registry pattern where tool-specific settings
|
|
405
|
+
* (including promptDetectionOptions) are managed in a Record<CLIToolType, CLIToolConfig>.
|
|
406
|
+
* Migration threshold: 6th tool addition triggers registry pattern migration [D1-003].
|
|
407
|
+
*
|
|
408
|
+
* @param cliToolId - CLI tool identifier
|
|
409
|
+
* @returns DetectPromptOptions for the tool, or undefined for default behavior
|
|
410
|
+
*/
|
|
337
411
|
function buildDetectPromptOptions(cliToolId) {
|
|
338
412
|
if (cliToolId === 'claude') {
|
|
339
413
|
return { requireDefaultIndicator: false };
|
|
340
414
|
}
|
|
415
|
+
// [D2-006] OpenCode prompt "Ask anything..." does not use standard indicators (> / ❯),
|
|
416
|
+
// so requireDefaultIndicator must be false to avoid missing prompt detection.
|
|
417
|
+
if (cliToolId === 'opencode') {
|
|
418
|
+
return { requireDefaultIndicator: false };
|
|
419
|
+
}
|
|
341
420
|
return undefined; // Default behavior (requireDefaultIndicator = true)
|
|
342
421
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
3
|
* CLI Tool Manager
|
|
4
|
-
* Singleton class to manage multiple CLI tools (Claude, Codex, Gemini, Vibe Local)
|
|
4
|
+
* Singleton class to manage multiple CLI tools (Claude, Codex, Gemini, Vibe Local, OpenCode)
|
|
5
5
|
*/
|
|
6
6
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
7
|
exports.CLIToolManager = void 0;
|
|
@@ -9,10 +9,11 @@ const claude_1 = require("./claude");
|
|
|
9
9
|
const codex_1 = require("./codex");
|
|
10
10
|
const gemini_1 = require("./gemini");
|
|
11
11
|
const vibe_local_1 = require("./vibe-local");
|
|
12
|
+
const opencode_1 = require("./opencode");
|
|
12
13
|
const response_poller_1 = require("../response-poller");
|
|
13
14
|
/**
|
|
14
15
|
* CLI Tool Manager (Singleton)
|
|
15
|
-
* Provides centralized access to all CLI tools (Issue #368: includes Vibe Local)
|
|
16
|
+
* Provides centralized access to all CLI tools (Issue #368: includes Vibe Local, Issue #379: includes OpenCode)
|
|
16
17
|
*/
|
|
17
18
|
class CLIToolManager {
|
|
18
19
|
static instance;
|
|
@@ -27,6 +28,7 @@ class CLIToolManager {
|
|
|
27
28
|
this.tools.set('codex', new codex_1.CodexTool());
|
|
28
29
|
this.tools.set('gemini', new gemini_1.GeminiTool());
|
|
29
30
|
this.tools.set('vibe-local', new vibe_local_1.VibeLocalTool());
|
|
31
|
+
this.tools.set('opencode', new opencode_1.OpenCodeTool());
|
|
30
32
|
}
|
|
31
33
|
/**
|
|
32
34
|
* Get singleton instance
|
|
@@ -68,7 +70,7 @@ class CLIToolManager {
|
|
|
68
70
|
* ```typescript
|
|
69
71
|
* const manager = CLIToolManager.getInstance();
|
|
70
72
|
* const allTools = manager.getAllTools();
|
|
71
|
-
* console.log(allTools.map(t => t.name)); // ['Claude Code', 'Codex CLI', 'Gemini CLI']
|
|
73
|
+
* console.log(allTools.map(t => t.name)); // ['Claude Code', 'Codex CLI', 'Gemini CLI', 'Vibe Local', 'OpenCode']
|
|
72
74
|
* ```
|
|
73
75
|
*/
|
|
74
76
|
getAllTools() {
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* OpenCode configuration file generator
|
|
4
|
+
* Issue #379: Generates opencode.json with Ollama provider configuration
|
|
5
|
+
*
|
|
6
|
+
* @remarks [D1-001 SRP] Separated from opencode.ts to maintain single responsibility.
|
|
7
|
+
* This module handles Ollama HTTP API calls and config file I/O,
|
|
8
|
+
* while opencode.ts handles tmux session management.
|
|
9
|
+
*/
|
|
10
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
11
|
+
if (k2 === undefined) k2 = k;
|
|
12
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
13
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
14
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
15
|
+
}
|
|
16
|
+
Object.defineProperty(o, k2, desc);
|
|
17
|
+
}) : (function(o, m, k, k2) {
|
|
18
|
+
if (k2 === undefined) k2 = k;
|
|
19
|
+
o[k2] = m[k];
|
|
20
|
+
}));
|
|
21
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
22
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
23
|
+
}) : function(o, v) {
|
|
24
|
+
o["default"] = v;
|
|
25
|
+
});
|
|
26
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
27
|
+
var ownKeys = function(o) {
|
|
28
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
29
|
+
var ar = [];
|
|
30
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
31
|
+
return ar;
|
|
32
|
+
};
|
|
33
|
+
return ownKeys(o);
|
|
34
|
+
};
|
|
35
|
+
return function (mod) {
|
|
36
|
+
if (mod && mod.__esModule) return mod;
|
|
37
|
+
var result = {};
|
|
38
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
39
|
+
__setModuleDefault(result, mod);
|
|
40
|
+
return result;
|
|
41
|
+
};
|
|
42
|
+
})();
|
|
43
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
44
|
+
exports.OLLAMA_MODEL_PATTERN = exports.MAX_OLLAMA_MODELS = exports.OLLAMA_BASE_URL = exports.OLLAMA_API_URL = void 0;
|
|
45
|
+
exports.ensureOpencodeConfig = ensureOpencodeConfig;
|
|
46
|
+
const fs = __importStar(require("fs"));
|
|
47
|
+
const path = __importStar(require("path"));
|
|
48
|
+
// =============================================================================
|
|
49
|
+
// Constants
|
|
50
|
+
// =============================================================================
|
|
51
|
+
/**
|
|
52
|
+
* [SEC-001] SSRF Prevention: Ollama API URL is hardcoded.
|
|
53
|
+
* This value MUST NOT be derived from environment variables, config files,
|
|
54
|
+
* or user input. OWASP A10:2021
|
|
55
|
+
*/
|
|
56
|
+
exports.OLLAMA_API_URL = 'http://localhost:11434/api/tags';
|
|
57
|
+
/**
|
|
58
|
+
* [SEC-001] SSRF Prevention: Ollama base URL for opencode.json config.
|
|
59
|
+
* Same policy as OLLAMA_API_URL.
|
|
60
|
+
*/
|
|
61
|
+
exports.OLLAMA_BASE_URL = 'http://localhost:11434/v1';
|
|
62
|
+
/** Maximum number of Ollama models to include in config (DoS prevention) */
|
|
63
|
+
exports.MAX_OLLAMA_MODELS = 100;
|
|
64
|
+
/**
|
|
65
|
+
* Ollama model name validation pattern (with length limit).
|
|
66
|
+
* Allows: alphanumeric, dots, underscores, colons, slashes, hyphens.
|
|
67
|
+
* Max 100 characters (length encoded in regex). [D4-003]
|
|
68
|
+
*
|
|
69
|
+
* [SEC-001] Defense-in-depth validation at point of use.
|
|
70
|
+
*
|
|
71
|
+
* Note: This pattern differs from OLLAMA_MODEL_PATTERN in types.ts.
|
|
72
|
+
* - types.ts: `^[a-zA-Z0-9][a-zA-Z0-9._:/-]*$` (no length limit, requires alphanumeric start)
|
|
73
|
+
* Used for API/DB validation where the first character constraint matters.
|
|
74
|
+
* - This file: `^[a-zA-Z0-9._:/-]{1,100}$` (length-limited, used for Ollama API response validation)
|
|
75
|
+
* Length limit provides DoS protection against excessively long model names from Ollama API.
|
|
76
|
+
*/
|
|
77
|
+
exports.OLLAMA_MODEL_PATTERN = /^[a-zA-Z0-9._:/-]{1,100}$/;
|
|
78
|
+
/** Ollama API request timeout in milliseconds */
|
|
79
|
+
const OLLAMA_API_TIMEOUT_MS = 3000;
|
|
80
|
+
/** Maximum Ollama API response size (1MB) [D4-007] */
|
|
81
|
+
const MAX_OLLAMA_RESPONSE_SIZE = 1 * 1024 * 1024;
|
|
82
|
+
/** Config file name */
|
|
83
|
+
const CONFIG_FILE_NAME = 'opencode.json';
|
|
84
|
+
// =============================================================================
|
|
85
|
+
// Helpers
|
|
86
|
+
// =============================================================================
|
|
87
|
+
/**
|
|
88
|
+
* Format model display name with size and quantization info
|
|
89
|
+
*/
|
|
90
|
+
function formatModelDisplayName(model) {
|
|
91
|
+
const name = String(model.name);
|
|
92
|
+
const details = model.details;
|
|
93
|
+
if (!details)
|
|
94
|
+
return name;
|
|
95
|
+
const parts = [name];
|
|
96
|
+
// Sanitize and extract parameter_size (e.g., "7.6B", "27.8B")
|
|
97
|
+
if (typeof details.parameter_size === 'string' && /^[\d.]+[BKMGT]?B?$/i.test(details.parameter_size)) {
|
|
98
|
+
parts.push(details.parameter_size);
|
|
99
|
+
}
|
|
100
|
+
// Sanitize and extract quantization_level (e.g., "Q4_K_M", "Q8_0")
|
|
101
|
+
if (typeof details.quantization_level === 'string' && /^[A-Z0-9_]{1,20}$/i.test(details.quantization_level)) {
|
|
102
|
+
parts.push(details.quantization_level);
|
|
103
|
+
}
|
|
104
|
+
return parts.length > 1 ? `${name} (${parts.slice(1).join(', ')})` : name;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Validate worktree path for path traversal prevention [D4-004].
|
|
108
|
+
*
|
|
109
|
+
* Trust chain: API layer -> DB (worktrees.path) -> startSession -> ensureOpencodeConfig.
|
|
110
|
+
* Although the DB stores validated paths, this function provides defense-in-depth
|
|
111
|
+
* by re-validating at the point of filesystem access.
|
|
112
|
+
*
|
|
113
|
+
* Steps:
|
|
114
|
+
* 1. path.resolve() - Normalize path (remove .., ., etc.)
|
|
115
|
+
* 2. fs.lstatSync() - Verify path exists and is a directory (symlink-aware)
|
|
116
|
+
* 3. fs.realpathSync() - Resolve symlinks to get the canonical path
|
|
117
|
+
*
|
|
118
|
+
* @param worktreePath - Path to validate
|
|
119
|
+
* @returns Resolved real path (after symlink resolution)
|
|
120
|
+
* @throws Error if path does not exist or is not a directory
|
|
121
|
+
* @internal
|
|
122
|
+
*/
|
|
123
|
+
function validateWorktreePath(worktreePath) {
|
|
124
|
+
// 1. path.resolve() for normalization
|
|
125
|
+
const resolvedPath = path.resolve(worktreePath);
|
|
126
|
+
// 2. Verify the path exists and is a directory (lstatSync for symlink detection)
|
|
127
|
+
try {
|
|
128
|
+
const stat = fs.lstatSync(resolvedPath);
|
|
129
|
+
if (!stat.isDirectory()) {
|
|
130
|
+
throw new Error(`Path is not a directory: ${resolvedPath}`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
catch (error) {
|
|
134
|
+
if (error.code === 'ENOENT') {
|
|
135
|
+
throw new Error(`Path does not exist: ${resolvedPath}`);
|
|
136
|
+
}
|
|
137
|
+
throw error;
|
|
138
|
+
}
|
|
139
|
+
// 3. Resolve symlinks to get real path
|
|
140
|
+
const realPath = fs.realpathSync(resolvedPath);
|
|
141
|
+
return realPath;
|
|
142
|
+
}
|
|
143
|
+
// =============================================================================
|
|
144
|
+
// Main function
|
|
145
|
+
// =============================================================================
|
|
146
|
+
/**
|
|
147
|
+
* Ensure opencode.json exists in the worktree directory.
|
|
148
|
+
* If the file already exists, it is NOT overwritten (respects user configuration).
|
|
149
|
+
* If Ollama is not running, the function logs a warning and returns without error.
|
|
150
|
+
*
|
|
151
|
+
* @param worktreePath - Worktree directory path (from DB)
|
|
152
|
+
* @internal
|
|
153
|
+
*/
|
|
154
|
+
async function ensureOpencodeConfig(worktreePath) {
|
|
155
|
+
// Validate path [D4-004]
|
|
156
|
+
const validatedPath = validateWorktreePath(worktreePath);
|
|
157
|
+
const configPath = path.join(validatedPath, CONFIG_FILE_NAME);
|
|
158
|
+
// Skip if config already exists (respect user configuration)
|
|
159
|
+
if (fs.existsSync(configPath)) {
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
// Fetch models from Ollama API
|
|
163
|
+
const models = {};
|
|
164
|
+
try {
|
|
165
|
+
const controller = new AbortController();
|
|
166
|
+
const timeoutId = setTimeout(() => controller.abort(), OLLAMA_API_TIMEOUT_MS);
|
|
167
|
+
const response = await fetch(exports.OLLAMA_API_URL, {
|
|
168
|
+
signal: controller.signal,
|
|
169
|
+
}).finally(() => {
|
|
170
|
+
clearTimeout(timeoutId);
|
|
171
|
+
});
|
|
172
|
+
if (!response.ok) {
|
|
173
|
+
console.warn(`Ollama API returned status ${response.status}, skipping opencode.json generation`);
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
// [D4-007] Response size check
|
|
177
|
+
const text = await response.text();
|
|
178
|
+
if (text.length > MAX_OLLAMA_RESPONSE_SIZE) {
|
|
179
|
+
console.warn('Ollama API response too large, skipping opencode.json generation');
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
// Parse and validate response structure [D4-007]
|
|
183
|
+
const data = JSON.parse(text);
|
|
184
|
+
if (!data || !Array.isArray(data.models)) {
|
|
185
|
+
console.warn('Invalid Ollama API response structure, skipping opencode.json generation');
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
// Limit model count (DoS prevention)
|
|
189
|
+
const modelList = data.models.slice(0, exports.MAX_OLLAMA_MODELS);
|
|
190
|
+
// Validate each model (whitelist approach) [D4-007]
|
|
191
|
+
for (const model of modelList) {
|
|
192
|
+
if (typeof model?.name !== 'string')
|
|
193
|
+
continue;
|
|
194
|
+
if (!exports.OLLAMA_MODEL_PATTERN.test(model.name))
|
|
195
|
+
continue;
|
|
196
|
+
models[model.name] = { name: formatModelDisplayName(model) };
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
catch (error) {
|
|
200
|
+
// Non-fatal: Ollama may not be running [D4-002]
|
|
201
|
+
if (error instanceof Error && error.name === 'AbortError') {
|
|
202
|
+
console.warn('Ollama API timeout, skipping opencode.json generation');
|
|
203
|
+
}
|
|
204
|
+
else {
|
|
205
|
+
console.warn('Failed to fetch Ollama models, skipping opencode.json generation');
|
|
206
|
+
}
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
// [D4-005] Generate config using JSON.stringify (not template literals).
|
|
210
|
+
// JSON.stringify ensures proper escaping of model names and other values,
|
|
211
|
+
// preventing JSON injection via maliciously crafted Ollama model metadata.
|
|
212
|
+
const config = {
|
|
213
|
+
$schema: 'https://opencode.ai/config.json',
|
|
214
|
+
provider: {
|
|
215
|
+
ollama: {
|
|
216
|
+
npm: '@ai-sdk/openai-compatible',
|
|
217
|
+
name: 'Ollama (local)',
|
|
218
|
+
options: { baseURL: exports.OLLAMA_BASE_URL },
|
|
219
|
+
models,
|
|
220
|
+
},
|
|
221
|
+
},
|
|
222
|
+
};
|
|
223
|
+
try {
|
|
224
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), {
|
|
225
|
+
encoding: 'utf-8',
|
|
226
|
+
flag: 'wx',
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
catch (error) {
|
|
230
|
+
if (error.code === 'EEXIST') {
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
// Non-fatal: write failure should not prevent session start
|
|
234
|
+
console.warn(`Failed to write opencode.json: ${error instanceof Error ? error.message : String(error)}`);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* OpenCode CLI tool implementation
|
|
4
|
+
* Issue #379: Provides integration with OpenCode TUI in interactive mode
|
|
5
|
+
*
|
|
6
|
+
* @remarks Follows the same tmux-based pattern as Claude/Codex/Gemini/VibeLocal tools.
|
|
7
|
+
* - startSession: launches `opencode` TUI in tmux
|
|
8
|
+
* - sendMessage: sends text via tmux send-keys + Enter
|
|
9
|
+
* - killSession: sends `/exit` command then falls back to tmux kill-session
|
|
10
|
+
* - interrupt(): inherits BaseCLITool default (Escape key) [D2-008]
|
|
11
|
+
*/
|
|
12
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
|
+
exports.OpenCodeTool = exports.OPENCODE_INIT_WAIT_MS = exports.OPENCODE_PANE_HEIGHT = exports.OPENCODE_EXIT_COMMAND = void 0;
|
|
14
|
+
const base_1 = require("./base");
|
|
15
|
+
const tmux_1 = require("../tmux");
|
|
16
|
+
const pasted_text_helper_1 = require("../pasted-text-helper");
|
|
17
|
+
const opencode_config_1 = require("./opencode-config");
|
|
18
|
+
const child_process_1 = require("child_process");
|
|
19
|
+
const util_1 = require("util");
|
|
20
|
+
const execFileAsync = (0, util_1.promisify)(child_process_1.execFile);
|
|
21
|
+
/**
|
|
22
|
+
* Extract error message from unknown error type (DRY)
|
|
23
|
+
* Same pattern as claude-session.ts / codex.ts / gemini.ts / vibe-local.ts.
|
|
24
|
+
* A shared version exists in src/lib/errors.ts (getErrorMessage), but CLI tool
|
|
25
|
+
* modules use local copies to avoid importing the server-side error module.
|
|
26
|
+
* [D1-002] Future refactoring candidate: extract to BaseCLITool or a shared util.
|
|
27
|
+
*/
|
|
28
|
+
function getErrorMessage(error) {
|
|
29
|
+
return error instanceof Error ? error.message : String(error);
|
|
30
|
+
}
|
|
31
|
+
/** OpenCode TUI graceful exit command [D1-006] */
|
|
32
|
+
exports.OPENCODE_EXIT_COMMAND = '/exit';
|
|
33
|
+
/**
|
|
34
|
+
* OpenCode tmux pane height (rows).
|
|
35
|
+
* Set to 200 to expand the TUI content area (~190 visible lines),
|
|
36
|
+
* allowing most responses to be captured in a single tmux capture-pane.
|
|
37
|
+
* OpenCode runs in alternate screen mode with no scrollback buffer,
|
|
38
|
+
* so only visible rows are capturable.
|
|
39
|
+
*/
|
|
40
|
+
exports.OPENCODE_PANE_HEIGHT = 200;
|
|
41
|
+
/**
|
|
42
|
+
* Wait for OpenCode TUI to initialize after launch.
|
|
43
|
+
* Set to 15000ms to accommodate GPU model loading via Ollama.
|
|
44
|
+
*/
|
|
45
|
+
exports.OPENCODE_INIT_WAIT_MS = 15000;
|
|
46
|
+
/**
|
|
47
|
+
* OpenCode CLI tool implementation
|
|
48
|
+
* Manages OpenCode interactive sessions using tmux
|
|
49
|
+
*/
|
|
50
|
+
class OpenCodeTool extends base_1.BaseCLITool {
|
|
51
|
+
id = 'opencode';
|
|
52
|
+
name = 'OpenCode';
|
|
53
|
+
command = 'opencode';
|
|
54
|
+
// interrupt() is inherited from BaseCLITool (Escape key) [D2-008]
|
|
55
|
+
// OpenCode TUI supports Escape for interruption ("esc interrupt" display)
|
|
56
|
+
/**
|
|
57
|
+
* Check if OpenCode session is running for a worktree
|
|
58
|
+
*/
|
|
59
|
+
async isRunning(worktreeId) {
|
|
60
|
+
const sessionName = this.getSessionName(worktreeId);
|
|
61
|
+
return await (0, tmux_1.hasSession)(sessionName);
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Start a new OpenCode session for a worktree
|
|
65
|
+
* Launches `opencode` TUI in interactive mode within tmux
|
|
66
|
+
*
|
|
67
|
+
* @param worktreeId - Worktree ID
|
|
68
|
+
* @param worktreePath - Worktree path
|
|
69
|
+
*/
|
|
70
|
+
async startSession(worktreeId, worktreePath) {
|
|
71
|
+
const opencodeAvailable = await this.isInstalled();
|
|
72
|
+
if (!opencodeAvailable) {
|
|
73
|
+
throw new Error('OpenCode is not installed or not in PATH');
|
|
74
|
+
}
|
|
75
|
+
const sessionName = this.getSessionName(worktreeId);
|
|
76
|
+
const exists = await (0, tmux_1.hasSession)(sessionName);
|
|
77
|
+
if (exists) {
|
|
78
|
+
console.log(`OpenCode session ${sessionName} already exists`);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
try {
|
|
82
|
+
// Generate opencode.json if not present (non-fatal on failure)
|
|
83
|
+
await (0, opencode_config_1.ensureOpencodeConfig)(worktreePath);
|
|
84
|
+
// Create tmux session with large history buffer
|
|
85
|
+
await (0, tmux_1.createSession)({
|
|
86
|
+
sessionName,
|
|
87
|
+
workingDirectory: worktreePath,
|
|
88
|
+
historyLimit: 50000,
|
|
89
|
+
});
|
|
90
|
+
// Wait a moment for the session to be created
|
|
91
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
92
|
+
// Resize tmux window to 80 columns (hide sidebar for clean capture-pane output)
|
|
93
|
+
// [SEC-001] Uses execFile (not exec) to prevent shell meta-character injection via sessionName
|
|
94
|
+
try {
|
|
95
|
+
await execFileAsync('tmux', [
|
|
96
|
+
'resize-window', '-t', sessionName,
|
|
97
|
+
'-x', '80', '-y', String(exports.OPENCODE_PANE_HEIGHT),
|
|
98
|
+
]);
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
// Non-fatal: resize may fail in some environments
|
|
102
|
+
}
|
|
103
|
+
// Start OpenCode TUI
|
|
104
|
+
await (0, tmux_1.sendKeys)(sessionName, 'opencode', true);
|
|
105
|
+
// Wait for OpenCode to initialize (GPU model loading via Ollama)
|
|
106
|
+
await new Promise((resolve) => setTimeout(resolve, exports.OPENCODE_INIT_WAIT_MS));
|
|
107
|
+
console.log(`Started OpenCode session: ${sessionName}`);
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
const errorMessage = getErrorMessage(error);
|
|
111
|
+
throw new Error(`Failed to start OpenCode session: ${errorMessage}`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Send a message to OpenCode interactive session
|
|
116
|
+
* [D1-004] Same pattern as Codex/Gemini/VibeLocal (future Template Method candidate)
|
|
117
|
+
*
|
|
118
|
+
* @param worktreeId - Worktree ID
|
|
119
|
+
* @param message - Message to send
|
|
120
|
+
*/
|
|
121
|
+
async sendMessage(worktreeId, message) {
|
|
122
|
+
const sessionName = this.getSessionName(worktreeId);
|
|
123
|
+
const exists = await (0, tmux_1.hasSession)(sessionName);
|
|
124
|
+
if (!exists) {
|
|
125
|
+
throw new Error(`OpenCode session ${sessionName} does not exist. Start the session first.`);
|
|
126
|
+
}
|
|
127
|
+
try {
|
|
128
|
+
// Send message to OpenCode (without Enter)
|
|
129
|
+
await (0, tmux_1.sendKeys)(sessionName, message, false);
|
|
130
|
+
// Wait a moment for the text to be typed
|
|
131
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
132
|
+
// Send Enter key separately
|
|
133
|
+
await (0, tmux_1.sendSpecialKey)(sessionName, 'C-m');
|
|
134
|
+
// Wait a moment for the message to be processed
|
|
135
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
136
|
+
// Detect [Pasted text] and resend Enter for multi-line messages
|
|
137
|
+
if (message.includes('\n')) {
|
|
138
|
+
await (0, pasted_text_helper_1.detectAndResendIfPastedText)(sessionName);
|
|
139
|
+
}
|
|
140
|
+
console.log(`Sent message to OpenCode session: ${sessionName}`);
|
|
141
|
+
}
|
|
142
|
+
catch (error) {
|
|
143
|
+
const errorMessage = getErrorMessage(error);
|
|
144
|
+
throw new Error(`Failed to send message to OpenCode: ${errorMessage}`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Kill OpenCode session with graceful shutdown.
|
|
149
|
+
*
|
|
150
|
+
* Shutdown sequence [D1-006, D1-007]:
|
|
151
|
+
* 1. Check if session exists
|
|
152
|
+
* 2. If exists: send `/exit` TUI command for graceful shutdown
|
|
153
|
+
* 3. Wait 2s for OpenCode to process the exit command
|
|
154
|
+
* 4. Re-check session: if still running, force-kill via tmux kill-session
|
|
155
|
+
* 5. If session did not exist: attempt kill anyway (cleanup stale sessions)
|
|
156
|
+
*
|
|
157
|
+
* @param worktreeId - Worktree ID
|
|
158
|
+
*/
|
|
159
|
+
async killSession(worktreeId) {
|
|
160
|
+
const sessionName = this.getSessionName(worktreeId);
|
|
161
|
+
try {
|
|
162
|
+
// Step 1: Check if the tmux session currently exists
|
|
163
|
+
const exists = await (0, tmux_1.hasSession)(sessionName);
|
|
164
|
+
if (exists) {
|
|
165
|
+
// Step 2: Send /exit command for graceful TUI shutdown [D1-006]
|
|
166
|
+
await (0, tmux_1.sendKeys)(sessionName, exports.OPENCODE_EXIT_COMMAND, true);
|
|
167
|
+
// Step 3: Wait for OpenCode to process the exit command
|
|
168
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
169
|
+
// Step 4: Check if session still exists; force-kill if needed [D1-007]
|
|
170
|
+
const stillExists = await (0, tmux_1.hasSession)(sessionName);
|
|
171
|
+
if (stillExists) {
|
|
172
|
+
await (0, tmux_1.killSession)(sessionName);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
else {
|
|
176
|
+
// Step 5: Session does not exist, attempt kill anyway (cleanup stale tmux sessions)
|
|
177
|
+
await (0, tmux_1.killSession)(sessionName);
|
|
178
|
+
}
|
|
179
|
+
console.log(`Stopped OpenCode session: ${sessionName}`);
|
|
180
|
+
}
|
|
181
|
+
catch (error) {
|
|
182
|
+
const errorMessage = getErrorMessage(error);
|
|
183
|
+
console.error(`Error stopping OpenCode session: ${errorMessage}`);
|
|
184
|
+
throw error;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
exports.OpenCodeTool = OpenCodeTool;
|