commandmate 0.2.3 → 0.2.4
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 +25 -25
- package/.next/app-path-routes-manifest.json +1 -1
- package/.next/build-manifest.json +7 -7
- package/.next/cache/.tsbuildinfo +1 -1
- package/.next/cache/config.json +3 -3
- package/.next/cache/fetch-cache/799a63cbfa61e2ab38626c05fe43500464c7bbd38341bdde69f5ec4b25acff68 +1 -0
- 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/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-minimal-server.js.nft.json +1 -1
- package/.next/next-server.js.nft.json +1 -1
- package/.next/prerender-manifest.json +1 -1
- 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 -0
- package/.next/server/app/api/app/update-check/route.js.nft.json +1 -0
- package/.next/server/app/api/app/update-check.body +1 -0
- package/.next/server/app/api/app/update-check.meta +1 -0
- package/.next/server/app/api/repositories/restore/route.js +1 -1
- package/.next/server/app/api/repositories/scan/route.js +1 -1
- package/.next/server/app/api/repositories/sync/route.js +1 -1
- package/.next/server/app/api/worktrees/[id]/logs/[filename]/route.js +1 -1
- package/.next/server/app/api/worktrees/[id]/logs/route.js +2 -2
- package/.next/server/app/api/worktrees/[id]/search/route.js +1 -1
- package/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/server/app/proxy/[...path]/route.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 +3 -3
- 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 +10 -9
- package/.next/server/chunks/2683.js +1 -1
- package/.next/server/chunks/5488.js +1 -1
- package/.next/server/chunks/5823.js +1 -1
- package/.next/server/chunks/7425.js +3 -3
- package/.next/server/chunks/7536.js +1 -1
- package/.next/server/chunks/9367.js +2 -2
- package/.next/server/functions-config-manifest.json +1 -1
- package/.next/server/middleware-build-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/{816-bb41b20a51ae924a.js → 816-af44cb865b0c980e.js} +1 -1
- package/.next/static/chunks/app/page-43b5de1a0a788b1f.js +1 -0
- package/.next/static/chunks/app/worktrees/[id]/{page-9d77c6f755d08086.js → page-9632761937a4d1ad.js} +1 -1
- package/.next/static/chunks/{main-b6d727aa9248d4f2.js → main-f00f82f1cf18dd99.js} +1 -1
- package/.next/static/chunks/{webpack-e6531fcf859d9451.js → webpack-af8567a485ade35a.js} +1 -1
- package/.next/static/css/{4eca30cb81bc52b4.css → 6a92c8ad3c94d15a.css} +1 -1
- package/.next/trace +5 -5
- package/.next/types/app/api/app/update-check/route.ts +343 -0
- package/dist/server/src/lib/prompt-detector.js +207 -67
- package/package.json +1 -1
- package/.next/static/chunks/app/page-792c0577dc44e5e5.js +0 -1
- /package/.next/static/{rppRTm2sRWa4sZE7ili8A → b3UR0y5mw3Ubf_vI5JjIN}/_buildManifest.js +0 -0
- /package/.next/static/{rppRTm2sRWa4sZE7ili8A → b3UR0y5mw3Ubf_vI5JjIN}/_ssgManifest.js +0 -0
- /package/.next/static/chunks/{4733-db0112b08802aaa7.js → 4733-50bdfc169adb4881.js} +0 -0
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
// File: /home/runner/work/CommandMate/CommandMate/src/app/api/app/update-check/route.ts
|
|
2
|
+
import * as entry from '../../../../../../src/app/api/app/update-check/route.js'
|
|
3
|
+
import type { NextRequest } from 'next/server.js'
|
|
4
|
+
|
|
5
|
+
type TEntry = typeof import('../../../../../../src/app/api/app/update-check/route.js')
|
|
6
|
+
|
|
7
|
+
// Check that the entry is a valid entry
|
|
8
|
+
checkFields<Diff<{
|
|
9
|
+
GET?: Function
|
|
10
|
+
HEAD?: Function
|
|
11
|
+
OPTIONS?: Function
|
|
12
|
+
POST?: Function
|
|
13
|
+
PUT?: Function
|
|
14
|
+
DELETE?: Function
|
|
15
|
+
PATCH?: Function
|
|
16
|
+
config?: {}
|
|
17
|
+
generateStaticParams?: Function
|
|
18
|
+
revalidate?: RevalidateRange<TEntry> | false
|
|
19
|
+
dynamic?: 'auto' | 'force-dynamic' | 'error' | 'force-static'
|
|
20
|
+
dynamicParams?: boolean
|
|
21
|
+
fetchCache?: 'auto' | 'force-no-store' | 'only-no-store' | 'default-no-store' | 'default-cache' | 'only-cache' | 'force-cache'
|
|
22
|
+
preferredRegion?: 'auto' | 'global' | 'home' | string | string[]
|
|
23
|
+
runtime?: 'nodejs' | 'experimental-edge' | 'edge'
|
|
24
|
+
maxDuration?: number
|
|
25
|
+
|
|
26
|
+
}, TEntry, ''>>()
|
|
27
|
+
|
|
28
|
+
// Check the prop type of the entry function
|
|
29
|
+
if ('GET' in entry) {
|
|
30
|
+
checkFields<
|
|
31
|
+
Diff<
|
|
32
|
+
ParamCheck<Request | NextRequest>,
|
|
33
|
+
{
|
|
34
|
+
__tag__: 'GET'
|
|
35
|
+
__param_position__: 'first'
|
|
36
|
+
__param_type__: FirstArg<MaybeField<TEntry, 'GET'>>
|
|
37
|
+
},
|
|
38
|
+
'GET'
|
|
39
|
+
>
|
|
40
|
+
>()
|
|
41
|
+
checkFields<
|
|
42
|
+
Diff<
|
|
43
|
+
ParamCheck<PageParams>,
|
|
44
|
+
{
|
|
45
|
+
__tag__: 'GET'
|
|
46
|
+
__param_position__: 'second'
|
|
47
|
+
__param_type__: SecondArg<MaybeField<TEntry, 'GET'>>
|
|
48
|
+
},
|
|
49
|
+
'GET'
|
|
50
|
+
>
|
|
51
|
+
>()
|
|
52
|
+
|
|
53
|
+
checkFields<
|
|
54
|
+
Diff<
|
|
55
|
+
{
|
|
56
|
+
__tag__: 'GET',
|
|
57
|
+
__return_type__: Response | void | never | Promise<Response | void | never>
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
__tag__: 'GET',
|
|
61
|
+
__return_type__: ReturnType<MaybeField<TEntry, 'GET'>>
|
|
62
|
+
},
|
|
63
|
+
'GET'
|
|
64
|
+
>
|
|
65
|
+
>()
|
|
66
|
+
}
|
|
67
|
+
// Check the prop type of the entry function
|
|
68
|
+
if ('HEAD' in entry) {
|
|
69
|
+
checkFields<
|
|
70
|
+
Diff<
|
|
71
|
+
ParamCheck<Request | NextRequest>,
|
|
72
|
+
{
|
|
73
|
+
__tag__: 'HEAD'
|
|
74
|
+
__param_position__: 'first'
|
|
75
|
+
__param_type__: FirstArg<MaybeField<TEntry, 'HEAD'>>
|
|
76
|
+
},
|
|
77
|
+
'HEAD'
|
|
78
|
+
>
|
|
79
|
+
>()
|
|
80
|
+
checkFields<
|
|
81
|
+
Diff<
|
|
82
|
+
ParamCheck<PageParams>,
|
|
83
|
+
{
|
|
84
|
+
__tag__: 'HEAD'
|
|
85
|
+
__param_position__: 'second'
|
|
86
|
+
__param_type__: SecondArg<MaybeField<TEntry, 'HEAD'>>
|
|
87
|
+
},
|
|
88
|
+
'HEAD'
|
|
89
|
+
>
|
|
90
|
+
>()
|
|
91
|
+
|
|
92
|
+
checkFields<
|
|
93
|
+
Diff<
|
|
94
|
+
{
|
|
95
|
+
__tag__: 'HEAD',
|
|
96
|
+
__return_type__: Response | void | never | Promise<Response | void | never>
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
__tag__: 'HEAD',
|
|
100
|
+
__return_type__: ReturnType<MaybeField<TEntry, 'HEAD'>>
|
|
101
|
+
},
|
|
102
|
+
'HEAD'
|
|
103
|
+
>
|
|
104
|
+
>()
|
|
105
|
+
}
|
|
106
|
+
// Check the prop type of the entry function
|
|
107
|
+
if ('OPTIONS' in entry) {
|
|
108
|
+
checkFields<
|
|
109
|
+
Diff<
|
|
110
|
+
ParamCheck<Request | NextRequest>,
|
|
111
|
+
{
|
|
112
|
+
__tag__: 'OPTIONS'
|
|
113
|
+
__param_position__: 'first'
|
|
114
|
+
__param_type__: FirstArg<MaybeField<TEntry, 'OPTIONS'>>
|
|
115
|
+
},
|
|
116
|
+
'OPTIONS'
|
|
117
|
+
>
|
|
118
|
+
>()
|
|
119
|
+
checkFields<
|
|
120
|
+
Diff<
|
|
121
|
+
ParamCheck<PageParams>,
|
|
122
|
+
{
|
|
123
|
+
__tag__: 'OPTIONS'
|
|
124
|
+
__param_position__: 'second'
|
|
125
|
+
__param_type__: SecondArg<MaybeField<TEntry, 'OPTIONS'>>
|
|
126
|
+
},
|
|
127
|
+
'OPTIONS'
|
|
128
|
+
>
|
|
129
|
+
>()
|
|
130
|
+
|
|
131
|
+
checkFields<
|
|
132
|
+
Diff<
|
|
133
|
+
{
|
|
134
|
+
__tag__: 'OPTIONS',
|
|
135
|
+
__return_type__: Response | void | never | Promise<Response | void | never>
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
__tag__: 'OPTIONS',
|
|
139
|
+
__return_type__: ReturnType<MaybeField<TEntry, 'OPTIONS'>>
|
|
140
|
+
},
|
|
141
|
+
'OPTIONS'
|
|
142
|
+
>
|
|
143
|
+
>()
|
|
144
|
+
}
|
|
145
|
+
// Check the prop type of the entry function
|
|
146
|
+
if ('POST' in entry) {
|
|
147
|
+
checkFields<
|
|
148
|
+
Diff<
|
|
149
|
+
ParamCheck<Request | NextRequest>,
|
|
150
|
+
{
|
|
151
|
+
__tag__: 'POST'
|
|
152
|
+
__param_position__: 'first'
|
|
153
|
+
__param_type__: FirstArg<MaybeField<TEntry, 'POST'>>
|
|
154
|
+
},
|
|
155
|
+
'POST'
|
|
156
|
+
>
|
|
157
|
+
>()
|
|
158
|
+
checkFields<
|
|
159
|
+
Diff<
|
|
160
|
+
ParamCheck<PageParams>,
|
|
161
|
+
{
|
|
162
|
+
__tag__: 'POST'
|
|
163
|
+
__param_position__: 'second'
|
|
164
|
+
__param_type__: SecondArg<MaybeField<TEntry, 'POST'>>
|
|
165
|
+
},
|
|
166
|
+
'POST'
|
|
167
|
+
>
|
|
168
|
+
>()
|
|
169
|
+
|
|
170
|
+
checkFields<
|
|
171
|
+
Diff<
|
|
172
|
+
{
|
|
173
|
+
__tag__: 'POST',
|
|
174
|
+
__return_type__: Response | void | never | Promise<Response | void | never>
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
__tag__: 'POST',
|
|
178
|
+
__return_type__: ReturnType<MaybeField<TEntry, 'POST'>>
|
|
179
|
+
},
|
|
180
|
+
'POST'
|
|
181
|
+
>
|
|
182
|
+
>()
|
|
183
|
+
}
|
|
184
|
+
// Check the prop type of the entry function
|
|
185
|
+
if ('PUT' in entry) {
|
|
186
|
+
checkFields<
|
|
187
|
+
Diff<
|
|
188
|
+
ParamCheck<Request | NextRequest>,
|
|
189
|
+
{
|
|
190
|
+
__tag__: 'PUT'
|
|
191
|
+
__param_position__: 'first'
|
|
192
|
+
__param_type__: FirstArg<MaybeField<TEntry, 'PUT'>>
|
|
193
|
+
},
|
|
194
|
+
'PUT'
|
|
195
|
+
>
|
|
196
|
+
>()
|
|
197
|
+
checkFields<
|
|
198
|
+
Diff<
|
|
199
|
+
ParamCheck<PageParams>,
|
|
200
|
+
{
|
|
201
|
+
__tag__: 'PUT'
|
|
202
|
+
__param_position__: 'second'
|
|
203
|
+
__param_type__: SecondArg<MaybeField<TEntry, 'PUT'>>
|
|
204
|
+
},
|
|
205
|
+
'PUT'
|
|
206
|
+
>
|
|
207
|
+
>()
|
|
208
|
+
|
|
209
|
+
checkFields<
|
|
210
|
+
Diff<
|
|
211
|
+
{
|
|
212
|
+
__tag__: 'PUT',
|
|
213
|
+
__return_type__: Response | void | never | Promise<Response | void | never>
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
__tag__: 'PUT',
|
|
217
|
+
__return_type__: ReturnType<MaybeField<TEntry, 'PUT'>>
|
|
218
|
+
},
|
|
219
|
+
'PUT'
|
|
220
|
+
>
|
|
221
|
+
>()
|
|
222
|
+
}
|
|
223
|
+
// Check the prop type of the entry function
|
|
224
|
+
if ('DELETE' in entry) {
|
|
225
|
+
checkFields<
|
|
226
|
+
Diff<
|
|
227
|
+
ParamCheck<Request | NextRequest>,
|
|
228
|
+
{
|
|
229
|
+
__tag__: 'DELETE'
|
|
230
|
+
__param_position__: 'first'
|
|
231
|
+
__param_type__: FirstArg<MaybeField<TEntry, 'DELETE'>>
|
|
232
|
+
},
|
|
233
|
+
'DELETE'
|
|
234
|
+
>
|
|
235
|
+
>()
|
|
236
|
+
checkFields<
|
|
237
|
+
Diff<
|
|
238
|
+
ParamCheck<PageParams>,
|
|
239
|
+
{
|
|
240
|
+
__tag__: 'DELETE'
|
|
241
|
+
__param_position__: 'second'
|
|
242
|
+
__param_type__: SecondArg<MaybeField<TEntry, 'DELETE'>>
|
|
243
|
+
},
|
|
244
|
+
'DELETE'
|
|
245
|
+
>
|
|
246
|
+
>()
|
|
247
|
+
|
|
248
|
+
checkFields<
|
|
249
|
+
Diff<
|
|
250
|
+
{
|
|
251
|
+
__tag__: 'DELETE',
|
|
252
|
+
__return_type__: Response | void | never | Promise<Response | void | never>
|
|
253
|
+
},
|
|
254
|
+
{
|
|
255
|
+
__tag__: 'DELETE',
|
|
256
|
+
__return_type__: ReturnType<MaybeField<TEntry, 'DELETE'>>
|
|
257
|
+
},
|
|
258
|
+
'DELETE'
|
|
259
|
+
>
|
|
260
|
+
>()
|
|
261
|
+
}
|
|
262
|
+
// Check the prop type of the entry function
|
|
263
|
+
if ('PATCH' in entry) {
|
|
264
|
+
checkFields<
|
|
265
|
+
Diff<
|
|
266
|
+
ParamCheck<Request | NextRequest>,
|
|
267
|
+
{
|
|
268
|
+
__tag__: 'PATCH'
|
|
269
|
+
__param_position__: 'first'
|
|
270
|
+
__param_type__: FirstArg<MaybeField<TEntry, 'PATCH'>>
|
|
271
|
+
},
|
|
272
|
+
'PATCH'
|
|
273
|
+
>
|
|
274
|
+
>()
|
|
275
|
+
checkFields<
|
|
276
|
+
Diff<
|
|
277
|
+
ParamCheck<PageParams>,
|
|
278
|
+
{
|
|
279
|
+
__tag__: 'PATCH'
|
|
280
|
+
__param_position__: 'second'
|
|
281
|
+
__param_type__: SecondArg<MaybeField<TEntry, 'PATCH'>>
|
|
282
|
+
},
|
|
283
|
+
'PATCH'
|
|
284
|
+
>
|
|
285
|
+
>()
|
|
286
|
+
|
|
287
|
+
checkFields<
|
|
288
|
+
Diff<
|
|
289
|
+
{
|
|
290
|
+
__tag__: 'PATCH',
|
|
291
|
+
__return_type__: Response | void | never | Promise<Response | void | never>
|
|
292
|
+
},
|
|
293
|
+
{
|
|
294
|
+
__tag__: 'PATCH',
|
|
295
|
+
__return_type__: ReturnType<MaybeField<TEntry, 'PATCH'>>
|
|
296
|
+
},
|
|
297
|
+
'PATCH'
|
|
298
|
+
>
|
|
299
|
+
>()
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Check the arguments and return type of the generateStaticParams function
|
|
303
|
+
if ('generateStaticParams' in entry) {
|
|
304
|
+
checkFields<Diff<{ params: PageParams }, FirstArg<MaybeField<TEntry, 'generateStaticParams'>>, 'generateStaticParams'>>()
|
|
305
|
+
checkFields<Diff<{ __tag__: 'generateStaticParams', __return_type__: any[] | Promise<any[]> }, { __tag__: 'generateStaticParams', __return_type__: ReturnType<MaybeField<TEntry, 'generateStaticParams'>> }>>()
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
type PageParams = any
|
|
309
|
+
export interface PageProps {
|
|
310
|
+
params?: any
|
|
311
|
+
searchParams?: any
|
|
312
|
+
}
|
|
313
|
+
export interface LayoutProps {
|
|
314
|
+
children?: React.ReactNode
|
|
315
|
+
|
|
316
|
+
params?: any
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// =============
|
|
320
|
+
// Utility types
|
|
321
|
+
type RevalidateRange<T> = T extends { revalidate: any } ? NonNegative<T['revalidate']> : never
|
|
322
|
+
|
|
323
|
+
// If T is unknown or any, it will be an empty {} type. Otherwise, it will be the same as Omit<T, keyof Base>.
|
|
324
|
+
type OmitWithTag<T, K extends keyof any, _M> = Omit<T, K>
|
|
325
|
+
type Diff<Base, T extends Base, Message extends string = ''> = 0 extends (1 & T) ? {} : OmitWithTag<T, keyof Base, Message>
|
|
326
|
+
|
|
327
|
+
type FirstArg<T extends Function> = T extends (...args: [infer T, any]) => any ? unknown extends T ? any : T : never
|
|
328
|
+
type SecondArg<T extends Function> = T extends (...args: [any, infer T]) => any ? unknown extends T ? any : T : never
|
|
329
|
+
type MaybeField<T, K extends string> = T extends { [k in K]: infer G } ? G extends Function ? G : never : never
|
|
330
|
+
|
|
331
|
+
type ParamCheck<T> = {
|
|
332
|
+
__tag__: string
|
|
333
|
+
__param_position__: string
|
|
334
|
+
__param_type__: T
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function checkFields<_ extends { [k in keyof any]: never }>() {}
|
|
338
|
+
|
|
339
|
+
// https://github.com/sindresorhus/type-fest
|
|
340
|
+
type Numeric = number | bigint
|
|
341
|
+
type Zero = 0 | 0n
|
|
342
|
+
type Negative<T extends Numeric> = T extends Zero ? never : `${T}` extends `-${string}` ? T : never
|
|
343
|
+
type NonNegative<T extends Numeric> = T extends Zero ? T : Negative<T> extends never ? T : '__invalid_negative_number__'
|
|
@@ -184,6 +184,28 @@ const NORMAL_OPTION_PATTERN = /^\s*(\d+)\.\s*(.+)$/;
|
|
|
184
184
|
* Anchored at both ends -- ReDoS safe (S4-001).
|
|
185
185
|
*/
|
|
186
186
|
const SEPARATOR_LINE_PATTERN = /^[-─]+$/;
|
|
187
|
+
/**
|
|
188
|
+
* Maximum number of lines to scan upward from questionEndIndex
|
|
189
|
+
* when the questionEndIndex line itself is not a question-like line.
|
|
190
|
+
*
|
|
191
|
+
* Design rationale (IC-256-001):
|
|
192
|
+
* - model selection prompts have 1-2 lines between "Select model" and first option
|
|
193
|
+
* - multi-line question wrapping typically produces 2-3 continuation lines
|
|
194
|
+
* - value of 3 covers these cases while minimizing False Positive surface
|
|
195
|
+
*
|
|
196
|
+
* [SF-002] Change guidelines:
|
|
197
|
+
* - Increase this value ONLY if real-world prompts are discovered where
|
|
198
|
+
* the question line is more than 3 lines above questionEndIndex
|
|
199
|
+
* - Before increasing, verify that the new value does not cause
|
|
200
|
+
* T11h-T11m False Positive tests to fail
|
|
201
|
+
* - Consider that larger values increase the False Positive surface area
|
|
202
|
+
* - If increasing beyond 5, consider whether the detection approach
|
|
203
|
+
* itself needs to be redesigned (e.g., pattern-based instead of scan-based)
|
|
204
|
+
* - Document the specific prompt pattern that necessitated the change
|
|
205
|
+
*
|
|
206
|
+
* @see Issue #256: multiple_choice prompt detection improvement
|
|
207
|
+
*/
|
|
208
|
+
const QUESTION_SCAN_RANGE = 3;
|
|
187
209
|
/**
|
|
188
210
|
* Creates a "no prompt detected" result.
|
|
189
211
|
* Centralizes the repeated pattern of returning isPrompt: false with trimmed content.
|
|
@@ -244,17 +266,75 @@ function isQuestionLikeLine(line) {
|
|
|
244
266
|
// Empty lines are not questions
|
|
245
267
|
if (line.length === 0)
|
|
246
268
|
return false;
|
|
247
|
-
// Pattern 1: Lines
|
|
269
|
+
// Pattern 1: Lines containing question mark anywhere (English '?' or full-width U+FF1F).
|
|
270
|
+
// This covers both:
|
|
271
|
+
// - Lines ending with '?' (standard question format)
|
|
272
|
+
// - Lines with '?' mid-line (Issue #256: multi-line question wrapping where '?'
|
|
273
|
+
// appears mid-line due to terminal width causing the question text to wrap)
|
|
274
|
+
//
|
|
248
275
|
// Full-width question mark (U+FF1F) support is a defensive measure: Claude Code/CLI
|
|
249
276
|
// displays questions in English, but this covers future multi-language support
|
|
250
277
|
// and third-party tool integration.
|
|
251
|
-
|
|
278
|
+
//
|
|
279
|
+
// [SF-001] Scope constraints:
|
|
280
|
+
// - The mid-line '?' detection is effective without False Positive risk only within
|
|
281
|
+
// SEC-001b guard context (questionEndIndex vicinity and upward scan range).
|
|
282
|
+
// - isQuestionLikeLine() is currently module-private (no export).
|
|
283
|
+
// - If this function is exported for external use in the future, consider:
|
|
284
|
+
// (a) Providing a stricter variant (e.g., isStrictQuestionLikeLine()) without mid-line match
|
|
285
|
+
// (b) Separating mid-line match into a SEC-001b-specific helper function
|
|
286
|
+
// (c) Adding URL exclusion logic (/[?&]\w+=/.test(line) to exclude)
|
|
287
|
+
if (line.includes('?') || line.includes('\uff1f'))
|
|
288
|
+
return true;
|
|
289
|
+
// Pattern 2: Lines containing a selection/input keyword.
|
|
290
|
+
// Detects both colon-terminated (e.g., "Select an option:", "Choose a mode:") and
|
|
291
|
+
// non-colon forms (e.g., "Select model") used by CLI prompts (Issue #256).
|
|
292
|
+
//
|
|
293
|
+
// [SF-001] Scope constraints apply:
|
|
294
|
+
// - Effective without False Positive risk only within SEC-001b guard context.
|
|
295
|
+
// - T11h-T11m False Positive lines do not contain QUESTION_KEYWORD_PATTERN keywords.
|
|
296
|
+
// - If this function is exported, consider restricting this pattern to SEC-001b context.
|
|
297
|
+
if (QUESTION_KEYWORD_PATTERN.test(line))
|
|
252
298
|
return true;
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
299
|
+
return false;
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* Search upward from a given line index to find a question-like line.
|
|
303
|
+
* Skips empty lines and separator lines (horizontal rules).
|
|
304
|
+
*
|
|
305
|
+
* This function is used by SEC-001b guard to find a question line above
|
|
306
|
+
* questionEndIndex when the questionEndIndex line itself is not a question-like line.
|
|
307
|
+
* This handles cases where the question text wraps across multiple lines or
|
|
308
|
+
* where description lines appear between the question and the numbered options.
|
|
309
|
+
*
|
|
310
|
+
* @param lines - Array of output lines
|
|
311
|
+
* @param startIndex - Starting line index (exclusive, searches startIndex-1 and above)
|
|
312
|
+
* @param scanRange - Maximum number of lines to scan upward (must be >= 0, clamped to MAX_SCAN_RANGE=10)
|
|
313
|
+
* @param lowerBound - Minimum line index (inclusive, scan will not go below this)
|
|
314
|
+
* @returns true if a question-like line is found within the scan range
|
|
315
|
+
*
|
|
316
|
+
* @see IC-256-002: SEC-001b upward scan implementation
|
|
317
|
+
* @see SF-003: Function extraction for readability
|
|
318
|
+
* @see SF-S4-001: scanRange input validation (defensive clamping)
|
|
319
|
+
*
|
|
320
|
+
* ReDoS safe: Uses SEPARATOR_LINE_PATTERN (existing ReDoS safe pattern) and
|
|
321
|
+
* isQuestionLikeLine() (literal character checks + simple alternation pattern).
|
|
322
|
+
* No new regex patterns introduced. (C-S4-001)
|
|
323
|
+
*/
|
|
324
|
+
function findQuestionLineInRange(lines, startIndex, scanRange, lowerBound) {
|
|
325
|
+
// [SF-S4-001] Defensive input validation: clamp scanRange to safe bounds.
|
|
326
|
+
// Currently only called with QUESTION_SCAN_RANGE=3, but guards against
|
|
327
|
+
// future misuse if the function is refactored or exported.
|
|
328
|
+
const safeScanRange = Math.min(Math.max(scanRange, 0), 10);
|
|
329
|
+
const scanLimit = Math.max(lowerBound, startIndex - safeScanRange);
|
|
330
|
+
for (let i = startIndex - 1; i >= scanLimit; i--) {
|
|
331
|
+
const candidateLine = lines[i]?.trim() ?? '';
|
|
332
|
+
// Skip empty lines and separator lines (horizontal rules)
|
|
333
|
+
if (!candidateLine || SEPARATOR_LINE_PATTERN.test(candidateLine))
|
|
334
|
+
continue;
|
|
335
|
+
if (isQuestionLikeLine(candidateLine)) {
|
|
257
336
|
return true;
|
|
337
|
+
}
|
|
258
338
|
}
|
|
259
339
|
return false;
|
|
260
340
|
}
|
|
@@ -308,19 +388,103 @@ function isConsecutiveFromOne(numbers) {
|
|
|
308
388
|
* @returns true if the line should be treated as a continuation of a previous option
|
|
309
389
|
*/
|
|
310
390
|
function isContinuationLine(rawLine, line) {
|
|
311
|
-
//
|
|
312
|
-
// Excludes lines ending with '?' or '?' (U+FF1F) because those are typically question lines
|
|
391
|
+
// Lines ending with '?' or full-width '?' (U+FF1F) are typically question lines
|
|
313
392
|
// (e.g., " Do you want to proceed?", " コピーしたい対象はどれですか?") from CLI tool output
|
|
314
|
-
// where both the question and options are 2-space indented.
|
|
315
|
-
//
|
|
316
|
-
//
|
|
393
|
+
// where both the question and options are 2-space indented. These must NOT be
|
|
394
|
+
// treated as continuation lines, otherwise questionEndIndex remains -1 and
|
|
395
|
+
// Layer 5 SEC-001 blocks detection.
|
|
317
396
|
const endsWithQuestion = line.endsWith('?') || line.endsWith('\uff1f');
|
|
318
|
-
|
|
319
|
-
//
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
397
|
+
// Check 1: Indented non-option line (label text wrapping with indentation).
|
|
398
|
+
// Must have 2+ leading spaces, not start with a number (option line), and not end with '?'.
|
|
399
|
+
if (!endsWithQuestion && /^\s{2,}[^\d]/.test(rawLine) && !/^\s*\d+\./.test(rawLine)) {
|
|
400
|
+
return true;
|
|
401
|
+
}
|
|
402
|
+
// Check 2: Short fragment (< 5 chars, e.g., filename tail).
|
|
403
|
+
// Excludes question-ending lines to prevent misclassifying short questions.
|
|
404
|
+
if (line.length < 5 && !endsWithQuestion) {
|
|
405
|
+
return true;
|
|
406
|
+
}
|
|
407
|
+
// Check 3: Path string continuation (Issue #181).
|
|
408
|
+
// Lines starting with / or ~, or alphanumeric-only fragments (2+ chars).
|
|
409
|
+
if (/^[\/~]/.test(line) || (line.length >= 2 && /^[a-zA-Z0-9_-]+$/.test(line))) {
|
|
410
|
+
return true;
|
|
411
|
+
}
|
|
412
|
+
return false;
|
|
413
|
+
}
|
|
414
|
+
/**
|
|
415
|
+
* Extract question text from the lines around questionEndIndex.
|
|
416
|
+
* Collects non-empty, non-separator lines from up to 5 lines before questionEndIndex
|
|
417
|
+
* through questionEndIndex itself, joining them with spaces.
|
|
418
|
+
*
|
|
419
|
+
* @param lines - Array of output lines
|
|
420
|
+
* @param questionEndIndex - Index of the last line before options, or -1 if not found
|
|
421
|
+
* @returns Extracted question text, or generic fallback if questionEndIndex is -1
|
|
422
|
+
*/
|
|
423
|
+
function extractQuestionText(lines, questionEndIndex) {
|
|
424
|
+
if (questionEndIndex < 0) {
|
|
425
|
+
return 'Please select an option:';
|
|
426
|
+
}
|
|
427
|
+
const questionLines = [];
|
|
428
|
+
for (let i = Math.max(0, questionEndIndex - 5); i <= questionEndIndex; i++) {
|
|
429
|
+
const line = lines[i].trim();
|
|
430
|
+
if (line && !SEPARATOR_LINE_PATTERN.test(line)) {
|
|
431
|
+
questionLines.push(line);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
return questionLines.join(' ');
|
|
435
|
+
}
|
|
436
|
+
/**
|
|
437
|
+
* Extract instruction text for the prompt block.
|
|
438
|
+
* Captures the complete AskUserQuestion block including context before the question,
|
|
439
|
+
* option descriptions, and navigation hints.
|
|
440
|
+
*
|
|
441
|
+
* @param lines - Array of output lines
|
|
442
|
+
* @param questionEndIndex - Index of the last line before options, or -1 if not found
|
|
443
|
+
* @param effectiveEnd - End index of non-trailing-empty lines
|
|
444
|
+
* @returns Instruction text string, or undefined if no question line found
|
|
445
|
+
*/
|
|
446
|
+
function extractInstructionText(lines, questionEndIndex, effectiveEnd) {
|
|
447
|
+
if (questionEndIndex < 0) {
|
|
448
|
+
return undefined;
|
|
449
|
+
}
|
|
450
|
+
const contextStart = Math.max(0, questionEndIndex - 19);
|
|
451
|
+
const blockLines = lines.slice(contextStart, effectiveEnd)
|
|
452
|
+
.map(l => l.trimEnd());
|
|
453
|
+
const joined = blockLines.join('\n').trim();
|
|
454
|
+
return joined.length > 0 ? joined : undefined;
|
|
455
|
+
}
|
|
456
|
+
/**
|
|
457
|
+
* Build the final PromptDetectionResult for a multiple choice prompt.
|
|
458
|
+
* Maps collected options to the output format, checking each option for
|
|
459
|
+
* text input requirements using TEXT_INPUT_PATTERNS.
|
|
460
|
+
*
|
|
461
|
+
* @param question - Extracted question text
|
|
462
|
+
* @param collectedOptions - Options collected during Pass 2 scanning
|
|
463
|
+
* @param instructionText - Instruction text for the prompt block
|
|
464
|
+
* @param output - Original output text (used for rawContent truncation)
|
|
465
|
+
* @returns PromptDetectionResult with isPrompt: true and multiple_choice data
|
|
466
|
+
*/
|
|
467
|
+
function buildMultipleChoiceResult(question, collectedOptions, instructionText, output) {
|
|
468
|
+
return {
|
|
469
|
+
isPrompt: true,
|
|
470
|
+
promptData: {
|
|
471
|
+
type: 'multiple_choice',
|
|
472
|
+
question: question.trim(),
|
|
473
|
+
options: collectedOptions.map(opt => {
|
|
474
|
+
const requiresTextInput = TEXT_INPUT_PATTERNS.some(pattern => pattern.test(opt.label));
|
|
475
|
+
return {
|
|
476
|
+
number: opt.number,
|
|
477
|
+
label: opt.label,
|
|
478
|
+
isDefault: opt.isDefault,
|
|
479
|
+
requiresTextInput,
|
|
480
|
+
};
|
|
481
|
+
}),
|
|
482
|
+
status: 'pending',
|
|
483
|
+
instructionText,
|
|
484
|
+
},
|
|
485
|
+
cleanContent: question.trim(),
|
|
486
|
+
rawContent: truncateRawContent(output.trim()), // Issue #235: complete prompt output (truncated) [MF-001]
|
|
487
|
+
};
|
|
324
488
|
}
|
|
325
489
|
/**
|
|
326
490
|
* Detect multiple choice prompts (numbered list with ❯ indicator)
|
|
@@ -399,6 +563,20 @@ function detectMultipleChoicePrompt(output, options) {
|
|
|
399
563
|
}
|
|
400
564
|
// Non-option line handling
|
|
401
565
|
if (collectedOptions.length > 0 && line && !SEPARATOR_LINE_PATTERN.test(line)) {
|
|
566
|
+
// [MF-001 / Issue #256] Check if line is a question-like line BEFORE
|
|
567
|
+
// continuation check. This preserves isContinuationLine()'s SRP by not
|
|
568
|
+
// mixing question detection into it. Without this pre-check, indented
|
|
569
|
+
// question lines (e.g., " Select model") could be misclassified as
|
|
570
|
+
// continuation lines by isContinuationLine()'s hasLeadingSpaces check.
|
|
571
|
+
//
|
|
572
|
+
// [SF-S4-003] Both this pre-check and SEC-001b upward scan use the same
|
|
573
|
+
// isQuestionLikeLine() function intentionally (DRY). If a question line is
|
|
574
|
+
// caught here, SEC-001b upward scan is not needed (questionEndIndex line
|
|
575
|
+
// itself passes isQuestionLikeLine()).
|
|
576
|
+
if (isQuestionLikeLine(line)) {
|
|
577
|
+
questionEndIndex = i;
|
|
578
|
+
break;
|
|
579
|
+
}
|
|
402
580
|
// Check if this is a continuation line (indented line between options,
|
|
403
581
|
// or path/filename fragments from terminal width wrapping - Issue #181)
|
|
404
582
|
const rawLine = lines[i]; // Original line with indentation preserved
|
|
@@ -435,61 +613,23 @@ function detectMultipleChoicePrompt(output, options) {
|
|
|
435
613
|
// SEC-001b: Question line exists but is not actually a question/selection request.
|
|
436
614
|
// Validates that the question line contains a question mark or a selection keyword
|
|
437
615
|
// with colon, distinguishing "Select an option:" from "Recommendations:".
|
|
616
|
+
//
|
|
617
|
+
// [Issue #256] Enhanced with upward scan via findQuestionLineInRange() (SF-003).
|
|
618
|
+
// When questionEndIndex line itself is not a question-like line, scan upward
|
|
619
|
+
// within QUESTION_SCAN_RANGE to find a question line above it. This handles:
|
|
620
|
+
// - Multi-line question wrapping where ? is on a line above questionEndIndex
|
|
621
|
+
// - Model selection prompts where "Select model" is above description lines
|
|
438
622
|
const questionLine = lines[questionEndIndex]?.trim() ?? '';
|
|
439
623
|
if (!isQuestionLikeLine(questionLine)) {
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
// Extract question text
|
|
444
|
-
let question = '';
|
|
445
|
-
if (questionEndIndex >= 0) {
|
|
446
|
-
// Get all non-empty lines from questionEndIndex up to (but not including) first option
|
|
447
|
-
const questionLines = [];
|
|
448
|
-
for (let i = Math.max(0, questionEndIndex - 5); i <= questionEndIndex; i++) {
|
|
449
|
-
const line = lines[i].trim();
|
|
450
|
-
if (line && !SEPARATOR_LINE_PATTERN.test(line)) {
|
|
451
|
-
questionLines.push(line);
|
|
624
|
+
// Upward scan: look for a question-like line above questionEndIndex
|
|
625
|
+
if (!findQuestionLineInRange(lines, questionEndIndex, QUESTION_SCAN_RANGE, scanStart)) {
|
|
626
|
+
return noPromptResult(output);
|
|
452
627
|
}
|
|
453
628
|
}
|
|
454
|
-
question = questionLines.join(' ');
|
|
455
|
-
}
|
|
456
|
-
else {
|
|
457
|
-
// No clear question found - use a generic one
|
|
458
|
-
question = 'Please select an option:';
|
|
459
|
-
}
|
|
460
|
-
// Extract instruction text: full prompt block (context before question through all options/descriptions)
|
|
461
|
-
// Captures the complete AskUserQuestion block including option descriptions and navigation hints.
|
|
462
|
-
let instructionText;
|
|
463
|
-
if (questionEndIndex >= 0) {
|
|
464
|
-
const contextStart = Math.max(0, questionEndIndex - 19);
|
|
465
|
-
const blockLines = lines.slice(contextStart, effectiveEnd)
|
|
466
|
-
.map(l => l.trimEnd());
|
|
467
|
-
const joined = blockLines.join('\n').trim();
|
|
468
|
-
if (joined.length > 0) {
|
|
469
|
-
instructionText = joined;
|
|
470
|
-
}
|
|
471
629
|
}
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
type: 'multiple_choice',
|
|
476
|
-
question: question.trim(),
|
|
477
|
-
options: collectedOptions.map(opt => {
|
|
478
|
-
// Check if this option requires text input using module-level patterns
|
|
479
|
-
const requiresTextInput = TEXT_INPUT_PATTERNS.some(pattern => pattern.test(opt.label));
|
|
480
|
-
return {
|
|
481
|
-
number: opt.number,
|
|
482
|
-
label: opt.label,
|
|
483
|
-
isDefault: opt.isDefault,
|
|
484
|
-
requiresTextInput,
|
|
485
|
-
};
|
|
486
|
-
}),
|
|
487
|
-
status: 'pending',
|
|
488
|
-
instructionText,
|
|
489
|
-
},
|
|
490
|
-
cleanContent: question.trim(),
|
|
491
|
-
rawContent: truncateRawContent(output.trim()), // Issue #235: complete prompt output (truncated) [MF-001]
|
|
492
|
-
};
|
|
630
|
+
const question = extractQuestionText(lines, questionEndIndex);
|
|
631
|
+
const instructionText = extractInstructionText(lines, questionEndIndex, effectiveEnd);
|
|
632
|
+
return buildMultipleChoiceResult(question, collectedOptions, instructionText, output);
|
|
493
633
|
}
|
|
494
634
|
/**
|
|
495
635
|
* Get tmux input string for an answer
|