commandmate 0.3.5 → 0.4.0
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 +19 -23
- 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 +69 -55
- package/.next/required-server-files.json +1 -1
- package/.next/server/app/_not-found/page.js +1 -1
- package/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/server/app/api/app/update-check/route.js +1 -1
- package/.next/server/app/api/repositories/clone/route.js +1 -1
- package/.next/server/app/api/repositories/route.js +8 -8
- package/.next/server/app/api/repositories/scan/route.js +1 -1
- package/.next/server/app/api/worktrees/[id]/capture/route.js +1 -2
- package/.next/server/app/api/worktrees/[id]/capture/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]/files/[...path]/route.js +1 -1
- package/.next/server/app/api/worktrees/[id]/prompt-response/route.js +1 -1
- package/.next/server/app/api/worktrees/[id]/respond/route.js +1 -1
- package/.next/server/app/api/worktrees/[id]/route.js +1 -1
- package/.next/server/app/api/worktrees/[id]/schedules/[scheduleId]/route.js +1 -1
- package/.next/server/app/api/worktrees/[id]/schedules/route.js +2 -2
- package/.next/server/app/api/worktrees/[id]/search/route.js +1 -1
- package/.next/server/app/api/worktrees/[id]/terminal/route.js +1 -1
- package/.next/server/app/api/worktrees/[id]/terminal/route.js.nft.json +1 -1
- package/.next/server/app/api/worktrees/[id]/tree/[...path]/route.js +1 -1
- package/.next/server/app/api/worktrees/[id]/upload/[...path]/route.js +1 -1
- package/.next/server/app/api/worktrees/route.js +1 -1
- package/.next/server/app/login/page.js +1 -1
- package/.next/server/app/login/page.js.nft.json +1 -1
- package/.next/server/app/login/page_client-reference-manifest.js +1 -1
- package/.next/server/app/page.js +3 -3
- package/.next/server/app/page.js.nft.json +1 -1
- package/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/server/app/proxy/[...path]/route.js +4 -4
- package/.next/server/app/worktrees/[id]/files/[...path]/page.js +1 -1
- package/.next/server/app/worktrees/[id]/files/[...path]/page.js.nft.json +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 +6 -6
- package/.next/server/app/worktrees/[id]/page.js.nft.json +1 -1
- package/.next/server/app/worktrees/[id]/page_client-reference-manifest.js +1 -1
- package/.next/server/app/worktrees/[id]/terminal/page.js +2 -4
- package/.next/server/app/worktrees/[id]/terminal/page.js.nft.json +1 -1
- package/.next/server/app/worktrees/[id]/terminal/page_client-reference-manifest.js +1 -1
- package/.next/server/app-paths-manifest.json +8 -8
- package/.next/server/chunks/{3294.js → 1628.js} +3 -3
- package/.next/server/chunks/185.js +36 -0
- package/.next/server/chunks/3860.js +1 -1
- package/.next/server/chunks/4893.js +2 -2
- package/.next/server/chunks/4952.js +1 -1
- package/.next/server/chunks/5488.js +6 -6
- package/.next/server/chunks/7425.js +34 -31
- package/.next/server/chunks/7566.js +2 -2
- package/.next/server/chunks/8199.js +1 -0
- package/.next/server/chunks/8585.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/server-reference-manifest.json +1 -1
- package/.next/static/chunks/12-00c528d46a0a0a1d.js +1 -0
- package/.next/static/chunks/{13.feeafc7cc620f8c4.js → 13.b9521543496f4468.js} +1 -1
- package/.next/static/chunks/1334.bfedf44ee9fe2761.js +1 -0
- package/.next/static/chunks/143.eb6b4671490cd223.js +1 -0
- package/.next/static/chunks/{3574.7a94c27e6a496a56.js → 1442.74b5f4de9a4b4e1b.js} +1 -1
- package/.next/static/chunks/2083-b5bed0c77cc53281.js +1 -0
- package/.next/static/chunks/2725.eb2d236c8030711c.js +1 -0
- package/.next/static/chunks/3398-3d40a17387bd554b.js +1 -0
- package/.next/static/chunks/3516.3c576047408cae6b.js +1 -0
- package/.next/static/chunks/3559.422c6ca760b85750.js +1 -0
- package/.next/static/chunks/3956.52c5b9a0071a641d.js +1 -0
- package/.next/static/chunks/4012.32b576a4fa621774.js +1 -0
- package/.next/static/chunks/4212.e7ba1009bc1da62d.js +131 -0
- package/.next/static/chunks/4303.caf91e86105d5e70.js +1 -0
- package/.next/static/chunks/4327.4dcda9b6fab6a385.js +82 -0
- package/.next/static/chunks/4671.d86d21d0dfdace41.js +1 -0
- package/.next/static/chunks/5518.ec88dcb5a27b17fe.js +1 -0
- package/.next/static/chunks/6434.08d262283371d333.js +1 -0
- package/.next/static/chunks/{656.5e2de0173f5a06bd.js → 656.dc26b973d07d9627.js} +5 -5
- package/.next/static/chunks/7119.01777af21b55740c.js +1 -0
- package/.next/static/chunks/7293.fb88bb102af4aa04.js +1 -0
- package/.next/static/chunks/8913-40625650292eb3d0.js +1 -0
- package/.next/static/chunks/8977.fc18b8260cd8bc1f.js +1 -0
- package/.next/static/chunks/9552.d959149efd41e84b.js +1 -0
- package/.next/static/chunks/app/layout-7198a7a49aa21a97.js +1 -0
- package/.next/static/chunks/app/page-7498cf75e69d9227.js +1 -0
- package/.next/static/chunks/app/worktrees/[id]/files/[...path]/page-0599f64a8e80d255.js +1 -0
- package/.next/static/chunks/app/worktrees/[id]/page-94ad7a1ce1f0c440.js +1 -0
- package/.next/static/chunks/app/worktrees/[id]/terminal/page-175b618c047bc992.js +1 -0
- package/.next/static/chunks/d3ac728e.daf595a898e9b720.js +1 -0
- package/.next/static/chunks/webpack-f7111aab807d73b9.js +1 -0
- package/.next/static/css/f7dc01350168df01.css +3 -0
- package/.next/trace +5 -5
- package/README.md +66 -56
- package/dist/server/server.js +5 -0
- package/dist/server/src/lib/auto-yes-manager.js +58 -18
- package/dist/server/src/lib/claude-session.js +9 -3
- package/dist/server/src/lib/cli-session.js +60 -10
- package/dist/server/src/lib/cli-tools/codex.js +7 -7
- package/dist/server/src/lib/cli-tools/gemini.js +3 -0
- package/dist/server/src/lib/cli-tools/opencode-config.js +179 -33
- package/dist/server/src/lib/cli-tools/opencode.js +5 -0
- package/dist/server/src/lib/cli-tools/vibe-local.js +3 -0
- package/dist/server/src/lib/cmate-parser.js +7 -7
- package/dist/server/src/lib/db-migrations.js +18 -1
- package/dist/server/src/lib/errors.js +153 -0
- package/dist/server/src/lib/prompt-answer-sender.js +3 -0
- package/dist/server/src/lib/prompt-detector.js +49 -7
- package/dist/server/src/lib/resource-cleanup.js +257 -0
- package/dist/server/src/lib/schedule-manager.js +269 -83
- package/dist/server/src/lib/tmux-capture-cache.js +221 -0
- package/dist/server/src/lib/tmux.js +41 -20
- package/dist/server/src/types/markdown-editor.js +9 -1
- package/package.json +11 -8
- package/.next/server/chunks/539.js +0 -35
- package/.next/server/chunks/7458.js +0 -1
- package/.next/server/chunks/7808.js +0 -1
- package/.next/static/chunks/1038-3509435b68c0967e.js +0 -1
- package/.next/static/chunks/1098.49268c9fe1b028fa.js +0 -1
- package/.next/static/chunks/2335-98a211e00b94c7ac.js +0 -1
- package/.next/static/chunks/3559.f073f72c4466ce0e.js +0 -1
- package/.next/static/chunks/3843.3fdda732987f7bb8.js +0 -1
- package/.next/static/chunks/4212.52c1bb34fc97d0d0.js +0 -131
- package/.next/static/chunks/4327.157a4c226d919531.js +0 -60
- package/.next/static/chunks/4362.7bd6f0282e49d79b.js +0 -1
- package/.next/static/chunks/4721.40615a5f4f32b5fb.js +0 -1
- package/.next/static/chunks/5112.17318d1c6b28044b.js +0 -1
- package/.next/static/chunks/6406.9653f0d41ab85059.js +0 -1
- package/.next/static/chunks/6792.3c01ac4dda4b5c6d.js +0 -1
- package/.next/static/chunks/8091-d65d2ab6daed23c6.js +0 -1
- package/.next/static/chunks/8125.245a9df052d274fb.js +0 -1
- package/.next/static/chunks/8522.1607e96011c66877.js +0 -1
- package/.next/static/chunks/8841.dadeb1ece8e46004.js +0 -1
- package/.next/static/chunks/8885.f8d9912b40d74811.js +0 -1
- package/.next/static/chunks/9178-88850a7c48deea07.js +0 -1
- package/.next/static/chunks/9552.b7dfb7903ead934b.js +0 -1
- package/.next/static/chunks/app/layout-9110f9a5e41c6bf4.js +0 -1
- package/.next/static/chunks/app/page-9e523a8f415bc707.js +0 -1
- package/.next/static/chunks/app/worktrees/[id]/files/[...path]/page-4a3c0861367e0391.js +0 -1
- package/.next/static/chunks/app/worktrees/[id]/page-8fb4dc30b58a5681.js +0 -1
- package/.next/static/chunks/app/worktrees/[id]/terminal/page-5d85a7e508ce36d3.js +0 -1
- package/.next/static/chunks/d3ac728e.6c9c508274d4d2d5.js +0 -1
- package/.next/static/chunks/webpack-81c97591dd5567ac.js +0 -1
- package/.next/static/css/45b3a41370668314.css +0 -3
- /package/.next/static/chunks/{30d07d85-393352a92199f695.js → 30d07d85.1dc99a921fc18e34.js} +0 -0
- /package/.next/static/{p3hosTZoJ22r35fWwUoLr → dwGMLEU53HOvFOWqiZOT0}/_buildManifest.js +0 -0
- /package/.next/static/{p3hosTZoJ22r35fWwUoLr → dwGMLEU53HOvFOWqiZOT0}/_ssgManifest.js +0 -0
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* OpenCode configuration file generator
|
|
4
4
|
* Issue #379: Generates opencode.json with Ollama provider configuration
|
|
5
|
+
* Issue #398: Added LM Studio provider support
|
|
5
6
|
*
|
|
6
7
|
* @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
|
+
* This module handles Ollama/LM Studio HTTP API calls and config file I/O,
|
|
8
9
|
* while opencode.ts handles tmux session management.
|
|
9
10
|
*/
|
|
10
11
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
@@ -41,7 +42,9 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
41
42
|
};
|
|
42
43
|
})();
|
|
43
44
|
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.LM_STUDIO_MODEL_PATTERN = exports.MAX_LM_STUDIO_MODELS = exports.LM_STUDIO_BASE_URL = exports.LM_STUDIO_API_URL = exports.OLLAMA_MODEL_PATTERN = exports.MAX_OLLAMA_MODELS = exports.OLLAMA_BASE_URL = exports.OLLAMA_API_URL = void 0;
|
|
46
|
+
exports.fetchOllamaModels = fetchOllamaModels;
|
|
47
|
+
exports.fetchLmStudioModels = fetchLmStudioModels;
|
|
45
48
|
exports.ensureOpencodeConfig = ensureOpencodeConfig;
|
|
46
49
|
const fs = __importStar(require("fs"));
|
|
47
50
|
const path = __importStar(require("path"));
|
|
@@ -75,10 +78,49 @@ exports.MAX_OLLAMA_MODELS = 100;
|
|
|
75
78
|
* Length limit provides DoS protection against excessively long model names from Ollama API.
|
|
76
79
|
*/
|
|
77
80
|
exports.OLLAMA_MODEL_PATTERN = /^[a-zA-Z0-9._:/-]{1,100}$/;
|
|
81
|
+
/**
|
|
82
|
+
* [SEC-001] SSRF Prevention: LM Studio API URL is hardcoded.
|
|
83
|
+
* This value MUST NOT be derived from environment variables, config files,
|
|
84
|
+
* or user input. OWASP A10:2021
|
|
85
|
+
*/
|
|
86
|
+
exports.LM_STUDIO_API_URL = 'http://localhost:1234/v1/models';
|
|
87
|
+
/**
|
|
88
|
+
* [SEC-001] SSRF Prevention: LM Studio base URL for opencode.json config.
|
|
89
|
+
* Same policy as LM_STUDIO_API_URL.
|
|
90
|
+
*/
|
|
91
|
+
exports.LM_STUDIO_BASE_URL = 'http://localhost:1234/v1';
|
|
92
|
+
/** Maximum number of LM Studio models to include in config (DoS prevention) */
|
|
93
|
+
exports.MAX_LM_STUDIO_MODELS = 100;
|
|
94
|
+
/**
|
|
95
|
+
* LM Studio model ID validation pattern (with length limit).
|
|
96
|
+
* Allows: alphanumeric, dots, underscores, colons, slashes, @, hyphens.
|
|
97
|
+
* Max 200 characters (length encoded in regex).
|
|
98
|
+
*
|
|
99
|
+
* [SEC-001] Defense-in-depth validation at point of use.
|
|
100
|
+
*
|
|
101
|
+
* Character set rationale:
|
|
102
|
+
* - a-zA-Z0-9._:/- : Common character set shared with Ollama (model names, org/model format)
|
|
103
|
+
* - @ : HuggingFace revision format support (e.g., org/model@revision)
|
|
104
|
+
*
|
|
105
|
+
* Actual model ID examples:
|
|
106
|
+
* - lmstudio-community/Meta-Llama-3.1-8B-Instruct-GGUF (54 chars)
|
|
107
|
+
* - TheBloke/Mistral-7B-Instruct-v0.2-GGUF (41 chars)
|
|
108
|
+
*
|
|
109
|
+
* Length limit rationale: Actual model IDs max ~60 chars; 200 provides
|
|
110
|
+
* sufficient safety margin for org+model+quantization+revision.
|
|
111
|
+
*
|
|
112
|
+
* Note: Hyphen `-` is placed at the end of the character class to avoid
|
|
113
|
+
* the need for escaping in the regex.
|
|
114
|
+
*/
|
|
115
|
+
exports.LM_STUDIO_MODEL_PATTERN = /^[a-zA-Z0-9._:/@-]{1,200}$/;
|
|
78
116
|
/** Ollama API request timeout in milliseconds */
|
|
79
117
|
const OLLAMA_API_TIMEOUT_MS = 3000;
|
|
80
118
|
/** Maximum Ollama API response size (1MB) [D4-007] */
|
|
81
119
|
const MAX_OLLAMA_RESPONSE_SIZE = 1 * 1024 * 1024;
|
|
120
|
+
/** LM Studio API request timeout in milliseconds */
|
|
121
|
+
const LM_STUDIO_API_TIMEOUT_MS = 3000;
|
|
122
|
+
/** Maximum LM Studio API response size (1MB) */
|
|
123
|
+
const MAX_LM_STUDIO_RESPONSE_SIZE = 1 * 1024 * 1024;
|
|
82
124
|
/** Config file name */
|
|
83
125
|
const CONFIG_FILE_NAME = 'opencode.json';
|
|
84
126
|
// =============================================================================
|
|
@@ -141,25 +183,22 @@ function validateWorktreePath(worktreePath) {
|
|
|
141
183
|
return realPath;
|
|
142
184
|
}
|
|
143
185
|
// =============================================================================
|
|
144
|
-
//
|
|
186
|
+
// Provider Functions
|
|
145
187
|
// =============================================================================
|
|
146
188
|
/**
|
|
147
|
-
*
|
|
148
|
-
*
|
|
149
|
-
* If Ollama is not running, the function logs a warning and returns without error.
|
|
189
|
+
* Fetch model list from Ollama API.
|
|
190
|
+
* Returns empty object on any failure (non-fatal).
|
|
150
191
|
*
|
|
151
|
-
*
|
|
192
|
+
* Extracted from ensureOpencodeConfig() for SRP compliance.
|
|
193
|
+
* All error paths (non-200 response, size exceeded, invalid structure, exceptions)
|
|
194
|
+
* return empty object {} instead of throwing.
|
|
195
|
+
*
|
|
196
|
+
* @returns Model map (key: model name, value: { name: display name })
|
|
152
197
|
* @internal
|
|
153
198
|
*/
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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
|
|
199
|
+
// TODO: If a 3rd provider is added, extract common HTTP fetch logic
|
|
200
|
+
// to fetchWithTimeout(url, timeoutMs, maxResponseSize): Promise<string | null>
|
|
201
|
+
async function fetchOllamaModels() {
|
|
163
202
|
const models = {};
|
|
164
203
|
try {
|
|
165
204
|
const controller = new AbortController();
|
|
@@ -170,20 +209,20 @@ async function ensureOpencodeConfig(worktreePath) {
|
|
|
170
209
|
clearTimeout(timeoutId);
|
|
171
210
|
});
|
|
172
211
|
if (!response.ok) {
|
|
173
|
-
console.warn(`Ollama API returned status ${response.status}, skipping
|
|
174
|
-
return;
|
|
212
|
+
console.warn(`Ollama API returned status ${response.status}, skipping model fetch`);
|
|
213
|
+
return {};
|
|
175
214
|
}
|
|
176
215
|
// [D4-007] Response size check
|
|
177
216
|
const text = await response.text();
|
|
178
217
|
if (text.length > MAX_OLLAMA_RESPONSE_SIZE) {
|
|
179
|
-
console.warn('Ollama API response too large, skipping
|
|
180
|
-
return;
|
|
218
|
+
console.warn('Ollama API response too large, skipping model fetch');
|
|
219
|
+
return {};
|
|
181
220
|
}
|
|
182
221
|
// Parse and validate response structure [D4-007]
|
|
183
222
|
const data = JSON.parse(text);
|
|
184
223
|
if (!data || !Array.isArray(data.models)) {
|
|
185
|
-
console.warn('Invalid Ollama API response structure, skipping
|
|
186
|
-
return;
|
|
224
|
+
console.warn('Invalid Ollama API response structure, skipping model fetch');
|
|
225
|
+
return {};
|
|
187
226
|
}
|
|
188
227
|
// Limit model count (DoS prevention)
|
|
189
228
|
const modelList = data.models.slice(0, exports.MAX_OLLAMA_MODELS);
|
|
@@ -199,26 +238,133 @@ async function ensureOpencodeConfig(worktreePath) {
|
|
|
199
238
|
catch (error) {
|
|
200
239
|
// Non-fatal: Ollama may not be running [D4-002]
|
|
201
240
|
if (error instanceof Error && error.name === 'AbortError') {
|
|
202
|
-
console.warn('Ollama API timeout, skipping
|
|
241
|
+
console.warn('Ollama API timeout, skipping model fetch');
|
|
242
|
+
}
|
|
243
|
+
else {
|
|
244
|
+
console.warn('Failed to fetch Ollama models, skipping model fetch');
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
return models;
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Fetch model list from LM Studio OpenAI-compatible API.
|
|
251
|
+
* Returns empty object on any failure (non-fatal).
|
|
252
|
+
*
|
|
253
|
+
* LM Studio provides an OpenAI-compatible API at /v1/models.
|
|
254
|
+
* Response format: { data: [{ id: string, object: string, ... }] }
|
|
255
|
+
* Model IDs are used as-is for display names (no details available).
|
|
256
|
+
*
|
|
257
|
+
* Model IDs are validated with LM_STUDIO_MODEL_PATTERN and used as
|
|
258
|
+
* opencode.json model keys. JSON.stringify() ensures proper escaping
|
|
259
|
+
* to prevent JSON structure corruption via malicious model IDs. [SEC-005]
|
|
260
|
+
*
|
|
261
|
+
* @returns Model map (key: model id, value: { name: model id })
|
|
262
|
+
* @internal
|
|
263
|
+
*/
|
|
264
|
+
// TODO: If a 3rd provider is added, extract common HTTP fetch logic
|
|
265
|
+
// to fetchWithTimeout(url, timeoutMs, maxResponseSize): Promise<string | null>
|
|
266
|
+
async function fetchLmStudioModels() {
|
|
267
|
+
const models = {};
|
|
268
|
+
try {
|
|
269
|
+
const controller = new AbortController();
|
|
270
|
+
const timeoutId = setTimeout(() => controller.abort(), LM_STUDIO_API_TIMEOUT_MS);
|
|
271
|
+
const response = await fetch(exports.LM_STUDIO_API_URL, {
|
|
272
|
+
signal: controller.signal,
|
|
273
|
+
}).finally(() => {
|
|
274
|
+
clearTimeout(timeoutId);
|
|
275
|
+
});
|
|
276
|
+
if (!response.ok) {
|
|
277
|
+
console.warn(`LM Studio API returned status ${response.status}, skipping model fetch`);
|
|
278
|
+
return {};
|
|
279
|
+
}
|
|
280
|
+
const text = await response.text();
|
|
281
|
+
if (text.length > MAX_LM_STUDIO_RESPONSE_SIZE) {
|
|
282
|
+
console.warn('LM Studio API response too large, skipping model fetch');
|
|
283
|
+
return {};
|
|
284
|
+
}
|
|
285
|
+
const data = JSON.parse(text);
|
|
286
|
+
if (!data || !Array.isArray(data.data)) {
|
|
287
|
+
console.warn('Invalid LM Studio API response structure, skipping model fetch');
|
|
288
|
+
return {};
|
|
289
|
+
}
|
|
290
|
+
const modelList = data.data.slice(0, exports.MAX_LM_STUDIO_MODELS);
|
|
291
|
+
for (const model of modelList) {
|
|
292
|
+
if (typeof model?.id !== 'string')
|
|
293
|
+
continue;
|
|
294
|
+
if (!exports.LM_STUDIO_MODEL_PATTERN.test(model.id))
|
|
295
|
+
continue;
|
|
296
|
+
models[model.id] = { name: model.id };
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
catch (error) {
|
|
300
|
+
if (error instanceof Error && error.name === 'AbortError') {
|
|
301
|
+
console.warn('LM Studio API timeout, skipping model fetch');
|
|
203
302
|
}
|
|
204
303
|
else {
|
|
205
|
-
console.warn('Failed to fetch
|
|
304
|
+
console.warn('Failed to fetch LM Studio models, skipping model fetch');
|
|
206
305
|
}
|
|
306
|
+
}
|
|
307
|
+
return models;
|
|
308
|
+
}
|
|
309
|
+
// =============================================================================
|
|
310
|
+
// Main function
|
|
311
|
+
// =============================================================================
|
|
312
|
+
/**
|
|
313
|
+
* Ensure opencode.json exists in the worktree directory.
|
|
314
|
+
* If the file already exists, it is NOT overwritten (respects user configuration).
|
|
315
|
+
* If both Ollama and LM Studio are not running, the function returns without error
|
|
316
|
+
* and does not generate opencode.json.
|
|
317
|
+
*
|
|
318
|
+
* Provider configuration is built dynamically: only providers with models are included.
|
|
319
|
+
* If a 3rd provider is added, consider refactoring to a data-driven design
|
|
320
|
+
* (providerDefinitions array + loop) instead of inline if-branches. [KISS]
|
|
321
|
+
*
|
|
322
|
+
* @param worktreePath - Worktree directory path (from DB)
|
|
323
|
+
* @internal
|
|
324
|
+
*/
|
|
325
|
+
async function ensureOpencodeConfig(worktreePath) {
|
|
326
|
+
// Validate path [D4-004]
|
|
327
|
+
const validatedPath = validateWorktreePath(worktreePath);
|
|
328
|
+
const configPath = path.join(validatedPath, CONFIG_FILE_NAME);
|
|
329
|
+
// Skip if config already exists (respect user configuration)
|
|
330
|
+
if (fs.existsSync(configPath)) {
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
// Fetch models from both providers in parallel.
|
|
334
|
+
// Each function catches all exceptions internally and returns {} on failure,
|
|
335
|
+
// so Promise.all will never reject.
|
|
336
|
+
const [ollamaModels, lmStudioModels] = await Promise.all([
|
|
337
|
+
fetchOllamaModels(),
|
|
338
|
+
fetchLmStudioModels(),
|
|
339
|
+
]);
|
|
340
|
+
// Dynamic provider configuration: only include providers with models
|
|
341
|
+
const provider = {};
|
|
342
|
+
if (Object.keys(ollamaModels).length > 0) {
|
|
343
|
+
provider.ollama = {
|
|
344
|
+
npm: '@ai-sdk/openai-compatible',
|
|
345
|
+
name: 'Ollama (local)',
|
|
346
|
+
options: { baseURL: exports.OLLAMA_BASE_URL },
|
|
347
|
+
models: ollamaModels,
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
if (Object.keys(lmStudioModels).length > 0) {
|
|
351
|
+
provider.lmstudio = {
|
|
352
|
+
npm: '@ai-sdk/openai-compatible',
|
|
353
|
+
name: 'LM Studio (local)',
|
|
354
|
+
options: { baseURL: exports.LM_STUDIO_BASE_URL },
|
|
355
|
+
models: lmStudioModels,
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
// Both providers have 0 models: skip opencode.json generation
|
|
359
|
+
if (Object.keys(provider).length === 0) {
|
|
207
360
|
return;
|
|
208
361
|
}
|
|
209
362
|
// [D4-005] Generate config using JSON.stringify (not template literals).
|
|
210
363
|
// JSON.stringify ensures proper escaping of model names and other values,
|
|
211
|
-
// preventing JSON injection via maliciously crafted
|
|
364
|
+
// preventing JSON injection via maliciously crafted model metadata.
|
|
212
365
|
const config = {
|
|
213
366
|
$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
|
-
},
|
|
367
|
+
provider,
|
|
222
368
|
};
|
|
223
369
|
try {
|
|
224
370
|
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), {
|
|
@@ -14,6 +14,7 @@ exports.OpenCodeTool = exports.OPENCODE_INIT_WAIT_MS = exports.OPENCODE_PANE_HEI
|
|
|
14
14
|
const base_1 = require("./base");
|
|
15
15
|
const tmux_1 = require("../tmux");
|
|
16
16
|
const pasted_text_helper_1 = require("../pasted-text-helper");
|
|
17
|
+
const tmux_capture_cache_1 = require("../tmux-capture-cache");
|
|
17
18
|
const opencode_config_1 = require("./opencode-config");
|
|
18
19
|
const child_process_1 = require("child_process");
|
|
19
20
|
const util_1 = require("util");
|
|
@@ -137,6 +138,8 @@ class OpenCodeTool extends base_1.BaseCLITool {
|
|
|
137
138
|
if (message.includes('\n')) {
|
|
138
139
|
await (0, pasted_text_helper_1.detectAndResendIfPastedText)(sessionName);
|
|
139
140
|
}
|
|
141
|
+
// Issue #405: Invalidate cache after sending message
|
|
142
|
+
(0, tmux_capture_cache_1.invalidateCache)(sessionName);
|
|
140
143
|
console.log(`Sent message to OpenCode session: ${sessionName}`);
|
|
141
144
|
}
|
|
142
145
|
catch (error) {
|
|
@@ -176,6 +179,8 @@ class OpenCodeTool extends base_1.BaseCLITool {
|
|
|
176
179
|
// Step 5: Session does not exist, attempt kill anyway (cleanup stale tmux sessions)
|
|
177
180
|
await (0, tmux_1.killSession)(sessionName);
|
|
178
181
|
}
|
|
182
|
+
// Issue #405: Invalidate cache after session kill
|
|
183
|
+
(0, tmux_capture_cache_1.invalidateCache)(sessionName);
|
|
179
184
|
console.log(`Stopped OpenCode session: ${sessionName}`);
|
|
180
185
|
}
|
|
181
186
|
catch (error) {
|
|
@@ -14,6 +14,7 @@ const base_1 = require("./base");
|
|
|
14
14
|
const types_1 = require("./types");
|
|
15
15
|
const tmux_1 = require("../tmux");
|
|
16
16
|
const pasted_text_helper_1 = require("../pasted-text-helper");
|
|
17
|
+
const tmux_capture_cache_1 = require("../tmux-capture-cache");
|
|
17
18
|
const db_instance_1 = require("../db-instance");
|
|
18
19
|
const db_1 = require("../db");
|
|
19
20
|
/**
|
|
@@ -132,6 +133,8 @@ class VibeLocalTool extends base_1.BaseCLITool {
|
|
|
132
133
|
if (message.includes('\n')) {
|
|
133
134
|
await (0, pasted_text_helper_1.detectAndResendIfPastedText)(sessionName);
|
|
134
135
|
}
|
|
136
|
+
// Issue #405: Invalidate cache after sending message
|
|
137
|
+
(0, tmux_capture_cache_1.invalidateCache)(sessionName);
|
|
135
138
|
console.log(`✓ Sent message to Vibe Local session: ${sessionName}`);
|
|
136
139
|
}
|
|
137
140
|
catch (error) {
|
|
@@ -22,7 +22,7 @@ exports.validateCmatePath = validateCmatePath;
|
|
|
22
22
|
exports.parseCmateFile = parseCmateFile;
|
|
23
23
|
exports.parseSchedulesSection = parseSchedulesSection;
|
|
24
24
|
exports.readCmateFile = readCmateFile;
|
|
25
|
-
const
|
|
25
|
+
const promises_1 = require("fs/promises");
|
|
26
26
|
const path_1 = __importDefault(require("path"));
|
|
27
27
|
const types_1 = require("../lib/cli-tools/types");
|
|
28
28
|
const schedule_config_1 = require("../config/schedule-config");
|
|
@@ -64,9 +64,9 @@ function sanitizeMessageContent(content) {
|
|
|
64
64
|
* @returns true if path is valid and within worktree directory
|
|
65
65
|
* @throws Error if path traversal is detected
|
|
66
66
|
*/
|
|
67
|
-
function validateCmatePath(filePath, worktreeDir) {
|
|
68
|
-
const realFilePath = (0,
|
|
69
|
-
const realWorktreeDir = (0,
|
|
67
|
+
async function validateCmatePath(filePath, worktreeDir) {
|
|
68
|
+
const realFilePath = await (0, promises_1.realpath)(filePath);
|
|
69
|
+
const realWorktreeDir = await (0, promises_1.realpath)(worktreeDir);
|
|
70
70
|
// Ensure the file is within the worktree directory
|
|
71
71
|
if (!realFilePath.startsWith(realWorktreeDir + path_1.default.sep) &&
|
|
72
72
|
realFilePath !== path_1.default.join(realWorktreeDir, cmate_constants_1.CMATE_FILENAME)) {
|
|
@@ -243,12 +243,12 @@ function parseSchedulesSection(rows) {
|
|
|
243
243
|
* @returns Parsed CmateConfig, or null if the file doesn't exist
|
|
244
244
|
* @throws Error if path traversal is detected
|
|
245
245
|
*/
|
|
246
|
-
function readCmateFile(worktreeDir) {
|
|
246
|
+
async function readCmateFile(worktreeDir) {
|
|
247
247
|
const filePath = path_1.default.join(worktreeDir, cmate_constants_1.CMATE_FILENAME);
|
|
248
248
|
try {
|
|
249
249
|
// Validate path before reading
|
|
250
|
-
validateCmatePath(filePath, worktreeDir);
|
|
251
|
-
const content = (0,
|
|
250
|
+
await validateCmatePath(filePath, worktreeDir);
|
|
251
|
+
const content = await (0, promises_1.readFile)(filePath, 'utf-8');
|
|
252
252
|
return parseCmateFile(content);
|
|
253
253
|
}
|
|
254
254
|
catch (error) {
|
|
@@ -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 =
|
|
22
|
+
exports.CURRENT_SCHEMA_VERSION = 21;
|
|
23
23
|
/**
|
|
24
24
|
* Migration registry
|
|
25
25
|
* All migrations should be added to this array in order
|
|
@@ -876,6 +876,23 @@ const migrations = [
|
|
|
876
876
|
// vibe_local_context_window is a nullable INTEGER column; harmless if unused
|
|
877
877
|
console.log('No rollback for vibe_local_context_window column (SQLite limitation)');
|
|
878
878
|
}
|
|
879
|
+
},
|
|
880
|
+
{
|
|
881
|
+
version: 21,
|
|
882
|
+
name: 'add-scheduled-executions-worktree-enabled-index',
|
|
883
|
+
up: (db) => {
|
|
884
|
+
// Issue #409: Add composite index for schedule sync performance
|
|
885
|
+
// Used by syncSchedules() to batch-query schedules by worktree_id + enabled
|
|
886
|
+
db.exec(`
|
|
887
|
+
CREATE INDEX IF NOT EXISTS idx_scheduled_executions_worktree_enabled
|
|
888
|
+
ON scheduled_executions(worktree_id, enabled);
|
|
889
|
+
`);
|
|
890
|
+
console.log('✓ Created composite index idx_scheduled_executions_worktree_enabled');
|
|
891
|
+
},
|
|
892
|
+
down: (db) => {
|
|
893
|
+
db.exec('DROP INDEX IF EXISTS idx_scheduled_executions_worktree_enabled');
|
|
894
|
+
console.log('✓ Dropped idx_scheduled_executions_worktree_enabled index');
|
|
895
|
+
}
|
|
879
896
|
}
|
|
880
897
|
];
|
|
881
898
|
/**
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Error Definitions
|
|
4
|
+
* Issue #136: Phase 1 - Foundation
|
|
5
|
+
*
|
|
6
|
+
* Centralized error handling for the application.
|
|
7
|
+
* SF-SEC-003: Separates client-facing and internal error messages.
|
|
8
|
+
*
|
|
9
|
+
* @module errors
|
|
10
|
+
*/
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.AppError = exports.ErrorCode = void 0;
|
|
13
|
+
exports.createAppError = createAppError;
|
|
14
|
+
exports.isAppError = isAppError;
|
|
15
|
+
exports.wrapError = wrapError;
|
|
16
|
+
exports.getErrorMessage = getErrorMessage;
|
|
17
|
+
/**
|
|
18
|
+
* Standard error codes used throughout the application
|
|
19
|
+
* These codes are safe to expose to clients.
|
|
20
|
+
*/
|
|
21
|
+
exports.ErrorCode = {
|
|
22
|
+
// Input validation errors
|
|
23
|
+
INVALID_ISSUE_NO: 'INVALID_ISSUE_NO',
|
|
24
|
+
ISSUE_NO_OUT_OF_RANGE: 'ISSUE_NO_OUT_OF_RANGE',
|
|
25
|
+
INVALID_BRANCH_NAME: 'INVALID_BRANCH_NAME',
|
|
26
|
+
BRANCH_NAME_TOO_LONG: 'BRANCH_NAME_TOO_LONG',
|
|
27
|
+
INVALID_PORT: 'INVALID_PORT',
|
|
28
|
+
// Resource errors
|
|
29
|
+
PORT_EXHAUSTED: 'PORT_EXHAUSTED',
|
|
30
|
+
PORT_IN_USE: 'PORT_IN_USE',
|
|
31
|
+
WORKTREE_NOT_FOUND: 'WORKTREE_NOT_FOUND',
|
|
32
|
+
WORKTREE_ALREADY_EXISTS: 'WORKTREE_ALREADY_EXISTS',
|
|
33
|
+
PID_FILE_EXISTS: 'PID_FILE_EXISTS',
|
|
34
|
+
PROCESS_NOT_RUNNING: 'PROCESS_NOT_RUNNING',
|
|
35
|
+
// Security errors
|
|
36
|
+
PATH_TRAVERSAL: 'PATH_TRAVERSAL',
|
|
37
|
+
UNAUTHORIZED: 'UNAUTHORIZED',
|
|
38
|
+
FORBIDDEN: 'FORBIDDEN',
|
|
39
|
+
// System errors
|
|
40
|
+
DATABASE_ERROR: 'DATABASE_ERROR',
|
|
41
|
+
FILESYSTEM_ERROR: 'FILESYSTEM_ERROR',
|
|
42
|
+
GIT_ERROR: 'GIT_ERROR',
|
|
43
|
+
TIMEOUT: 'TIMEOUT',
|
|
44
|
+
// Generic errors
|
|
45
|
+
UNKNOWN_ERROR: 'UNKNOWN_ERROR',
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* Application-specific error class
|
|
49
|
+
*
|
|
50
|
+
* @example
|
|
51
|
+
* ```typescript
|
|
52
|
+
* throw new AppError('INVALID_ISSUE_NO', 'Issue number must be a positive integer', { received: -1 });
|
|
53
|
+
* ```
|
|
54
|
+
*/
|
|
55
|
+
class AppError extends Error {
|
|
56
|
+
/**
|
|
57
|
+
* Error code - safe to expose to clients
|
|
58
|
+
*/
|
|
59
|
+
code;
|
|
60
|
+
/**
|
|
61
|
+
* Additional details - may contain sensitive info, log only
|
|
62
|
+
*/
|
|
63
|
+
details;
|
|
64
|
+
/**
|
|
65
|
+
* Timestamp when error occurred
|
|
66
|
+
*/
|
|
67
|
+
timestamp;
|
|
68
|
+
constructor(code, message, details) {
|
|
69
|
+
super(message);
|
|
70
|
+
this.name = 'AppError';
|
|
71
|
+
this.code = code;
|
|
72
|
+
this.details = details;
|
|
73
|
+
this.timestamp = new Date().toISOString();
|
|
74
|
+
// Maintains proper stack trace for where our error was thrown (only available on V8)
|
|
75
|
+
if (Error.captureStackTrace) {
|
|
76
|
+
Error.captureStackTrace(this, AppError);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Create a client-safe representation (excludes sensitive details)
|
|
81
|
+
*/
|
|
82
|
+
toClientError() {
|
|
83
|
+
return {
|
|
84
|
+
code: this.code,
|
|
85
|
+
message: this.message,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Create a log-safe representation (includes details for debugging)
|
|
90
|
+
*/
|
|
91
|
+
toLogError() {
|
|
92
|
+
return {
|
|
93
|
+
code: this.code,
|
|
94
|
+
message: this.message,
|
|
95
|
+
details: this.details,
|
|
96
|
+
timestamp: this.timestamp,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
exports.AppError = AppError;
|
|
101
|
+
/**
|
|
102
|
+
* Factory function to create AppError
|
|
103
|
+
*
|
|
104
|
+
* @param code - Error code from ErrorCode enum
|
|
105
|
+
* @param message - Human-readable error message
|
|
106
|
+
* @param details - Optional additional details (logged, not sent to client)
|
|
107
|
+
* @returns AppError instance
|
|
108
|
+
*
|
|
109
|
+
* @example
|
|
110
|
+
* ```typescript
|
|
111
|
+
* throw createAppError(ErrorCode.PORT_EXHAUSTED, 'No available ports in range', { range: [3001, 3100] });
|
|
112
|
+
* ```
|
|
113
|
+
*/
|
|
114
|
+
function createAppError(code, message, details) {
|
|
115
|
+
return new AppError(code, message, details);
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Type guard to check if an error is an AppError
|
|
119
|
+
*
|
|
120
|
+
* @param error - Error to check
|
|
121
|
+
* @returns true if error is an AppError
|
|
122
|
+
*/
|
|
123
|
+
function isAppError(error) {
|
|
124
|
+
return error instanceof AppError;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Wrap unknown error into AppError
|
|
128
|
+
*
|
|
129
|
+
* @param error - Unknown error
|
|
130
|
+
* @param defaultCode - Default error code if error is not AppError
|
|
131
|
+
* @returns AppError instance
|
|
132
|
+
*/
|
|
133
|
+
function wrapError(error, defaultCode = exports.ErrorCode.UNKNOWN_ERROR) {
|
|
134
|
+
if (isAppError(error)) {
|
|
135
|
+
return error;
|
|
136
|
+
}
|
|
137
|
+
if (error instanceof Error) {
|
|
138
|
+
return new AppError(defaultCode, error.message, { originalError: error.name });
|
|
139
|
+
}
|
|
140
|
+
return new AppError(defaultCode, String(error));
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Get error message from unknown error
|
|
144
|
+
*
|
|
145
|
+
* @param error - Unknown error
|
|
146
|
+
* @returns Error message string
|
|
147
|
+
*/
|
|
148
|
+
function getErrorMessage(error) {
|
|
149
|
+
if (error instanceof Error) {
|
|
150
|
+
return error.message;
|
|
151
|
+
}
|
|
152
|
+
return String(error);
|
|
153
|
+
}
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
10
|
exports.sendPromptAnswer = sendPromptAnswer;
|
|
11
11
|
const tmux_1 = require("./tmux");
|
|
12
|
+
const tmux_capture_cache_1 = require("./tmux-capture-cache");
|
|
12
13
|
/** Regex pattern to detect checkbox-style multi-select options */
|
|
13
14
|
const CHECKBOX_OPTION_PATTERN = /^\[[ x]\] /;
|
|
14
15
|
/**
|
|
@@ -86,4 +87,6 @@ async function sendPromptAnswer(params) {
|
|
|
86
87
|
// Send Enter
|
|
87
88
|
await (0, tmux_1.sendKeys)(sessionName, '', true);
|
|
88
89
|
}
|
|
90
|
+
// Issue #405: Invalidate cache after sending prompt answer
|
|
91
|
+
(0, tmux_capture_cache_1.invalidateCache)(sessionName);
|
|
89
92
|
}
|
|
@@ -6,8 +6,22 @@
|
|
|
6
6
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
7
|
exports.detectPrompt = detectPrompt;
|
|
8
8
|
exports.getAnswerInput = getAnswerInput;
|
|
9
|
+
exports.resetDetectPromptCache = resetDetectPromptCache;
|
|
9
10
|
const logger_1 = require("./logger");
|
|
10
11
|
const logger = (0, logger_1.createLogger)('prompt-detector');
|
|
12
|
+
/**
|
|
13
|
+
* Last output tail used for duplicate log suppression.
|
|
14
|
+
* Only the last 50 lines of the output are compared.
|
|
15
|
+
* This is a performance optimization to reduce log I/O -- it does NOT
|
|
16
|
+
* affect detectPrompt()'s return value in any way.
|
|
17
|
+
*
|
|
18
|
+
* Uses pure module-scope (not globalThis) since Hot Reload reset is
|
|
19
|
+
* acceptable for log-only caching. Same pattern as ip-restriction.ts
|
|
20
|
+
* module-scope cache. [S2-004]
|
|
21
|
+
*
|
|
22
|
+
* @internal
|
|
23
|
+
*/
|
|
24
|
+
let lastOutputTail = null;
|
|
11
25
|
/**
|
|
12
26
|
* Maximum number of lines to retain in rawContent.
|
|
13
27
|
* Tail lines are preserved (instruction text typically appears just before the prompt).
|
|
@@ -109,8 +123,22 @@ function yesNoPromptResult(question, cleanContent, rawContent, defaultOption) {
|
|
|
109
123
|
* ```
|
|
110
124
|
*/
|
|
111
125
|
function detectPrompt(output, options) {
|
|
112
|
-
|
|
126
|
+
// D2-001: Extract tail 50 lines for duplicate log suppression.
|
|
127
|
+
// Reuse `lines` for both dedup check and yes/no pattern matching below.
|
|
128
|
+
// detectMultipleChoicePrompt() has its own independent split() in its
|
|
129
|
+
// own scope -- this is intentional function encapsulation [S1-004][S2-001].
|
|
113
130
|
const lines = output.split('\n');
|
|
131
|
+
const tailForDedup = lines.slice(-50).join('\n');
|
|
132
|
+
const isDuplicate = tailForDedup === lastOutputTail;
|
|
133
|
+
// D2-002: Only log on new (non-duplicate) output [S1-001 SRP tradeoff:
|
|
134
|
+
// dedup logic is inlined here because it needs direct access to output.
|
|
135
|
+
// If log suppression grows complex (e.g., per-worktree cache), extract
|
|
136
|
+
// to shouldSuppressLog(output): boolean helper.]
|
|
137
|
+
if (!isDuplicate) {
|
|
138
|
+
logger.debug('detectPrompt:start', { outputLength: output.length });
|
|
139
|
+
}
|
|
140
|
+
// D2-003: Update cache (affects logging only, never return values)
|
|
141
|
+
lastOutputTail = tailForDedup;
|
|
114
142
|
// [SF-003] [MF-S2-001] Expanded from 10 to 20 lines for rawContent coverage
|
|
115
143
|
const lastLines = lines.slice(-20).join('\n');
|
|
116
144
|
// Pattern 0: Multiple choice (numbered options with ❯ indicator)
|
|
@@ -121,11 +149,14 @@ function detectPrompt(output, options) {
|
|
|
121
149
|
// 3. Cancel
|
|
122
150
|
const multipleChoiceResult = detectMultipleChoicePrompt(output, options);
|
|
123
151
|
if (multipleChoiceResult.isPrompt) {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
152
|
+
// D2-004: Suppress duplicate multipleChoice info log
|
|
153
|
+
if (!isDuplicate) {
|
|
154
|
+
logger.info('detectPrompt:multipleChoice', {
|
|
155
|
+
isPrompt: true,
|
|
156
|
+
question: multipleChoiceResult.promptData?.question,
|
|
157
|
+
optionsCount: multipleChoiceResult.promptData?.options?.length,
|
|
158
|
+
});
|
|
159
|
+
}
|
|
129
160
|
return multipleChoiceResult;
|
|
130
161
|
}
|
|
131
162
|
// Patterns 1-4: Yes/no patterns (data-driven matching)
|
|
@@ -148,7 +179,10 @@ function detectPrompt(output, options) {
|
|
|
148
179
|
return yesNoPromptResult(question, content || 'Approve?', trimmedLastLines);
|
|
149
180
|
}
|
|
150
181
|
// No prompt detected
|
|
151
|
-
|
|
182
|
+
// D2-005: Suppress duplicate complete log
|
|
183
|
+
if (!isDuplicate) {
|
|
184
|
+
logger.debug('detectPrompt:complete', { isPrompt: false });
|
|
185
|
+
}
|
|
152
186
|
return {
|
|
153
187
|
isPrompt: false,
|
|
154
188
|
cleanContent: output.trim(),
|
|
@@ -697,3 +731,11 @@ function getAnswerInput(answer, promptType = 'yes_no') {
|
|
|
697
731
|
// SEC-003: Fixed error message without user input to prevent log injection
|
|
698
732
|
throw new Error("Invalid answer for yes/no prompt. Expected 'yes', 'no', 'y', or 'n'.");
|
|
699
733
|
}
|
|
734
|
+
/**
|
|
735
|
+
* Reset the duplicate log suppression cache.
|
|
736
|
+
* Intended for test isolation only.
|
|
737
|
+
* @internal
|
|
738
|
+
*/
|
|
739
|
+
function resetDetectPromptCache() {
|
|
740
|
+
lastOutputTail = null;
|
|
741
|
+
}
|