@tarquinen/opencode-dcp 3.2.1-beta0 → 3.2.2-beta0
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/lib/analysis/tokens.ts +225 -0
- package/lib/config.ts +1071 -0
- package/lib/logger.ts +235 -0
- package/lib/messages/query.ts +56 -0
- package/lib/state/index.ts +4 -0
- package/lib/state/persistence.ts +260 -0
- package/lib/state/state.ts +180 -0
- package/lib/state/tool-cache.ts +98 -0
- package/lib/state/types.ts +108 -0
- package/lib/state/utils.ts +310 -0
- package/lib/token-utils.ts +162 -0
- package/package.json +13 -2
- package/tui/data/context.ts +177 -0
- package/tui/index.tsx +34 -0
- package/tui/routes/summary.tsx +175 -0
- package/tui/shared/names.ts +9 -0
- package/tui/shared/theme.ts +58 -0
- package/tui/shared/types.ts +38 -0
- package/tui/slots/sidebar-content.tsx +502 -0
package/lib/config.ts
ADDED
|
@@ -0,0 +1,1071 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, statSync } from "fs"
|
|
2
|
+
import { join, dirname } from "path"
|
|
3
|
+
import { homedir } from "os"
|
|
4
|
+
import { parse } from "jsonc-parser"
|
|
5
|
+
import type { PluginInput } from "@opencode-ai/plugin"
|
|
6
|
+
|
|
7
|
+
type Permission = "ask" | "allow" | "deny"
|
|
8
|
+
type CompressMode = "range" | "message"
|
|
9
|
+
|
|
10
|
+
export interface Deduplication {
|
|
11
|
+
enabled: boolean
|
|
12
|
+
protectedTools: string[]
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface CompressConfig {
|
|
16
|
+
mode: CompressMode
|
|
17
|
+
permission: Permission
|
|
18
|
+
showCompression: boolean
|
|
19
|
+
summaryBuffer: boolean
|
|
20
|
+
maxContextLimit: number | `${number}%`
|
|
21
|
+
minContextLimit: number | `${number}%`
|
|
22
|
+
modelMaxLimits?: Record<string, number | `${number}%`>
|
|
23
|
+
modelMinLimits?: Record<string, number | `${number}%`>
|
|
24
|
+
nudgeFrequency: number
|
|
25
|
+
iterationNudgeThreshold: number
|
|
26
|
+
nudgeForce: "strong" | "soft"
|
|
27
|
+
protectedTools: string[]
|
|
28
|
+
protectUserMessages: boolean
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface Commands {
|
|
32
|
+
enabled: boolean
|
|
33
|
+
protectedTools: string[]
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface ManualModeConfig {
|
|
37
|
+
enabled: boolean
|
|
38
|
+
automaticStrategies: boolean
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface PurgeErrors {
|
|
42
|
+
enabled: boolean
|
|
43
|
+
turns: number
|
|
44
|
+
protectedTools: string[]
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface TurnProtection {
|
|
48
|
+
enabled: boolean
|
|
49
|
+
turns: number
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface TuiConfig {
|
|
53
|
+
sidebar: boolean
|
|
54
|
+
debug: boolean
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface ExperimentalConfig {
|
|
58
|
+
allowSubAgents: boolean
|
|
59
|
+
customPrompts: boolean
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface PluginConfig {
|
|
63
|
+
enabled: boolean
|
|
64
|
+
debug: boolean
|
|
65
|
+
pruneNotification: "off" | "minimal" | "detailed"
|
|
66
|
+
pruneNotificationType: "chat" | "toast"
|
|
67
|
+
commands: Commands
|
|
68
|
+
manualMode: ManualModeConfig
|
|
69
|
+
turnProtection: TurnProtection
|
|
70
|
+
tui: TuiConfig
|
|
71
|
+
experimental: ExperimentalConfig
|
|
72
|
+
protectedFilePatterns: string[]
|
|
73
|
+
compress: CompressConfig
|
|
74
|
+
strategies: {
|
|
75
|
+
deduplication: Deduplication
|
|
76
|
+
purgeErrors: PurgeErrors
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
type CompressOverride = Partial<CompressConfig>
|
|
81
|
+
|
|
82
|
+
const DEFAULT_PROTECTED_TOOLS = [
|
|
83
|
+
"task",
|
|
84
|
+
"skill",
|
|
85
|
+
"todowrite",
|
|
86
|
+
"todoread",
|
|
87
|
+
"compress",
|
|
88
|
+
"batch",
|
|
89
|
+
"plan_enter",
|
|
90
|
+
"plan_exit",
|
|
91
|
+
"write",
|
|
92
|
+
"edit",
|
|
93
|
+
]
|
|
94
|
+
|
|
95
|
+
const COMPRESS_DEFAULT_PROTECTED_TOOLS = ["task", "skill", "todowrite", "todoread"]
|
|
96
|
+
|
|
97
|
+
export const VALID_CONFIG_KEYS = new Set([
|
|
98
|
+
"$schema",
|
|
99
|
+
"enabled",
|
|
100
|
+
"debug",
|
|
101
|
+
"pruneNotification",
|
|
102
|
+
"pruneNotificationType",
|
|
103
|
+
"turnProtection",
|
|
104
|
+
"turnProtection.enabled",
|
|
105
|
+
"turnProtection.turns",
|
|
106
|
+
"tui",
|
|
107
|
+
"tui.sidebar",
|
|
108
|
+
"tui.debug",
|
|
109
|
+
"experimental",
|
|
110
|
+
"experimental.allowSubAgents",
|
|
111
|
+
"experimental.customPrompts",
|
|
112
|
+
"protectedFilePatterns",
|
|
113
|
+
"commands",
|
|
114
|
+
"commands.enabled",
|
|
115
|
+
"commands.protectedTools",
|
|
116
|
+
"manualMode",
|
|
117
|
+
"manualMode.enabled",
|
|
118
|
+
"manualMode.automaticStrategies",
|
|
119
|
+
"compress",
|
|
120
|
+
"compress.mode",
|
|
121
|
+
"compress.permission",
|
|
122
|
+
"compress.showCompression",
|
|
123
|
+
"compress.summaryBuffer",
|
|
124
|
+
"compress.maxContextLimit",
|
|
125
|
+
"compress.minContextLimit",
|
|
126
|
+
"compress.modelMaxLimits",
|
|
127
|
+
"compress.modelMinLimits",
|
|
128
|
+
"compress.nudgeFrequency",
|
|
129
|
+
"compress.iterationNudgeThreshold",
|
|
130
|
+
"compress.nudgeForce",
|
|
131
|
+
"compress.protectedTools",
|
|
132
|
+
"compress.protectUserMessages",
|
|
133
|
+
"strategies",
|
|
134
|
+
"strategies.deduplication",
|
|
135
|
+
"strategies.deduplication.enabled",
|
|
136
|
+
"strategies.deduplication.protectedTools",
|
|
137
|
+
"strategies.purgeErrors",
|
|
138
|
+
"strategies.purgeErrors.enabled",
|
|
139
|
+
"strategies.purgeErrors.turns",
|
|
140
|
+
"strategies.purgeErrors.protectedTools",
|
|
141
|
+
])
|
|
142
|
+
|
|
143
|
+
function getConfigKeyPaths(obj: Record<string, any>, prefix = ""): string[] {
|
|
144
|
+
const keys: string[] = []
|
|
145
|
+
for (const key of Object.keys(obj)) {
|
|
146
|
+
const fullKey = prefix ? `${prefix}.${key}` : key
|
|
147
|
+
keys.push(fullKey)
|
|
148
|
+
|
|
149
|
+
// model*Limits are dynamic maps keyed by providerID/modelID; do not recurse into arbitrary IDs.
|
|
150
|
+
if (fullKey === "compress.modelMaxLimits" || fullKey === "compress.modelMinLimits") {
|
|
151
|
+
continue
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (obj[key] && typeof obj[key] === "object" && !Array.isArray(obj[key])) {
|
|
155
|
+
keys.push(...getConfigKeyPaths(obj[key], fullKey))
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return keys
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function getInvalidConfigKeys(userConfig: Record<string, any>): string[] {
|
|
162
|
+
const userKeys = getConfigKeyPaths(userConfig)
|
|
163
|
+
return userKeys.filter((key) => !VALID_CONFIG_KEYS.has(key))
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
interface ValidationError {
|
|
167
|
+
key: string
|
|
168
|
+
expected: string
|
|
169
|
+
actual: string
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
type ConfigWarningNotifier = (title: string, message: string) => void
|
|
173
|
+
|
|
174
|
+
interface ConfigWarningCallbacks {
|
|
175
|
+
onParseWarning?: (title: string, message: string) => void
|
|
176
|
+
onConfigWarning?: (configPath: string, data: Record<string, any>, isProject: boolean) => void
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export function validateConfigTypes(config: Record<string, any>): ValidationError[] {
|
|
180
|
+
const errors: ValidationError[] = []
|
|
181
|
+
|
|
182
|
+
if (config.enabled !== undefined && typeof config.enabled !== "boolean") {
|
|
183
|
+
errors.push({ key: "enabled", expected: "boolean", actual: typeof config.enabled })
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (config.debug !== undefined && typeof config.debug !== "boolean") {
|
|
187
|
+
errors.push({ key: "debug", expected: "boolean", actual: typeof config.debug })
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (config.pruneNotification !== undefined) {
|
|
191
|
+
const validValues = ["off", "minimal", "detailed"]
|
|
192
|
+
if (!validValues.includes(config.pruneNotification)) {
|
|
193
|
+
errors.push({
|
|
194
|
+
key: "pruneNotification",
|
|
195
|
+
expected: '"off" | "minimal" | "detailed"',
|
|
196
|
+
actual: JSON.stringify(config.pruneNotification),
|
|
197
|
+
})
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (config.pruneNotificationType !== undefined) {
|
|
202
|
+
const validValues = ["chat", "toast"]
|
|
203
|
+
if (!validValues.includes(config.pruneNotificationType)) {
|
|
204
|
+
errors.push({
|
|
205
|
+
key: "pruneNotificationType",
|
|
206
|
+
expected: '"chat" | "toast"',
|
|
207
|
+
actual: JSON.stringify(config.pruneNotificationType),
|
|
208
|
+
})
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (config.protectedFilePatterns !== undefined) {
|
|
213
|
+
if (!Array.isArray(config.protectedFilePatterns)) {
|
|
214
|
+
errors.push({
|
|
215
|
+
key: "protectedFilePatterns",
|
|
216
|
+
expected: "string[]",
|
|
217
|
+
actual: typeof config.protectedFilePatterns,
|
|
218
|
+
})
|
|
219
|
+
} else if (!config.protectedFilePatterns.every((v: unknown) => typeof v === "string")) {
|
|
220
|
+
errors.push({
|
|
221
|
+
key: "protectedFilePatterns",
|
|
222
|
+
expected: "string[]",
|
|
223
|
+
actual: "non-string entries",
|
|
224
|
+
})
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (config.turnProtection) {
|
|
229
|
+
if (
|
|
230
|
+
config.turnProtection.enabled !== undefined &&
|
|
231
|
+
typeof config.turnProtection.enabled !== "boolean"
|
|
232
|
+
) {
|
|
233
|
+
errors.push({
|
|
234
|
+
key: "turnProtection.enabled",
|
|
235
|
+
expected: "boolean",
|
|
236
|
+
actual: typeof config.turnProtection.enabled,
|
|
237
|
+
})
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (
|
|
241
|
+
config.turnProtection.turns !== undefined &&
|
|
242
|
+
typeof config.turnProtection.turns !== "number"
|
|
243
|
+
) {
|
|
244
|
+
errors.push({
|
|
245
|
+
key: "turnProtection.turns",
|
|
246
|
+
expected: "number",
|
|
247
|
+
actual: typeof config.turnProtection.turns,
|
|
248
|
+
})
|
|
249
|
+
}
|
|
250
|
+
if (typeof config.turnProtection.turns === "number" && config.turnProtection.turns < 1) {
|
|
251
|
+
errors.push({
|
|
252
|
+
key: "turnProtection.turns",
|
|
253
|
+
expected: "positive number (>= 1)",
|
|
254
|
+
actual: `${config.turnProtection.turns}`,
|
|
255
|
+
})
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const experimental = config.experimental
|
|
260
|
+
if (experimental !== undefined) {
|
|
261
|
+
if (
|
|
262
|
+
typeof experimental !== "object" ||
|
|
263
|
+
experimental === null ||
|
|
264
|
+
Array.isArray(experimental)
|
|
265
|
+
) {
|
|
266
|
+
errors.push({
|
|
267
|
+
key: "experimental",
|
|
268
|
+
expected: "object",
|
|
269
|
+
actual: typeof experimental,
|
|
270
|
+
})
|
|
271
|
+
} else {
|
|
272
|
+
if (
|
|
273
|
+
experimental.allowSubAgents !== undefined &&
|
|
274
|
+
typeof experimental.allowSubAgents !== "boolean"
|
|
275
|
+
) {
|
|
276
|
+
errors.push({
|
|
277
|
+
key: "experimental.allowSubAgents",
|
|
278
|
+
expected: "boolean",
|
|
279
|
+
actual: typeof experimental.allowSubAgents,
|
|
280
|
+
})
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (
|
|
284
|
+
experimental.customPrompts !== undefined &&
|
|
285
|
+
typeof experimental.customPrompts !== "boolean"
|
|
286
|
+
) {
|
|
287
|
+
errors.push({
|
|
288
|
+
key: "experimental.customPrompts",
|
|
289
|
+
expected: "boolean",
|
|
290
|
+
actual: typeof experimental.customPrompts,
|
|
291
|
+
})
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const tui = config.tui
|
|
297
|
+
if (tui !== undefined) {
|
|
298
|
+
if (typeof tui !== "object" || tui === null || Array.isArray(tui)) {
|
|
299
|
+
errors.push({
|
|
300
|
+
key: "tui",
|
|
301
|
+
expected: "object",
|
|
302
|
+
actual: typeof tui,
|
|
303
|
+
})
|
|
304
|
+
} else {
|
|
305
|
+
if (tui.sidebar !== undefined && typeof tui.sidebar !== "boolean") {
|
|
306
|
+
errors.push({
|
|
307
|
+
key: "tui.sidebar",
|
|
308
|
+
expected: "boolean",
|
|
309
|
+
actual: typeof tui.sidebar,
|
|
310
|
+
})
|
|
311
|
+
}
|
|
312
|
+
if (tui.debug !== undefined && typeof tui.debug !== "boolean") {
|
|
313
|
+
errors.push({
|
|
314
|
+
key: "tui.debug",
|
|
315
|
+
expected: "boolean",
|
|
316
|
+
actual: typeof tui.debug,
|
|
317
|
+
})
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const commands = config.commands
|
|
323
|
+
if (commands !== undefined) {
|
|
324
|
+
if (typeof commands !== "object" || commands === null || Array.isArray(commands)) {
|
|
325
|
+
errors.push({
|
|
326
|
+
key: "commands",
|
|
327
|
+
expected: "object",
|
|
328
|
+
actual: typeof commands,
|
|
329
|
+
})
|
|
330
|
+
} else {
|
|
331
|
+
if (commands.enabled !== undefined && typeof commands.enabled !== "boolean") {
|
|
332
|
+
errors.push({
|
|
333
|
+
key: "commands.enabled",
|
|
334
|
+
expected: "boolean",
|
|
335
|
+
actual: typeof commands.enabled,
|
|
336
|
+
})
|
|
337
|
+
}
|
|
338
|
+
if (commands.protectedTools !== undefined && !Array.isArray(commands.protectedTools)) {
|
|
339
|
+
errors.push({
|
|
340
|
+
key: "commands.protectedTools",
|
|
341
|
+
expected: "string[]",
|
|
342
|
+
actual: typeof commands.protectedTools,
|
|
343
|
+
})
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const manualMode = config.manualMode
|
|
349
|
+
if (manualMode !== undefined) {
|
|
350
|
+
if (typeof manualMode !== "object" || manualMode === null || Array.isArray(manualMode)) {
|
|
351
|
+
errors.push({
|
|
352
|
+
key: "manualMode",
|
|
353
|
+
expected: "object",
|
|
354
|
+
actual: typeof manualMode,
|
|
355
|
+
})
|
|
356
|
+
} else {
|
|
357
|
+
if (manualMode.enabled !== undefined && typeof manualMode.enabled !== "boolean") {
|
|
358
|
+
errors.push({
|
|
359
|
+
key: "manualMode.enabled",
|
|
360
|
+
expected: "boolean",
|
|
361
|
+
actual: typeof manualMode.enabled,
|
|
362
|
+
})
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
if (
|
|
366
|
+
manualMode.automaticStrategies !== undefined &&
|
|
367
|
+
typeof manualMode.automaticStrategies !== "boolean"
|
|
368
|
+
) {
|
|
369
|
+
errors.push({
|
|
370
|
+
key: "manualMode.automaticStrategies",
|
|
371
|
+
expected: "boolean",
|
|
372
|
+
actual: typeof manualMode.automaticStrategies,
|
|
373
|
+
})
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const compress = config.compress
|
|
379
|
+
if (compress !== undefined) {
|
|
380
|
+
if (typeof compress !== "object" || compress === null || Array.isArray(compress)) {
|
|
381
|
+
errors.push({
|
|
382
|
+
key: "compress",
|
|
383
|
+
expected: "object",
|
|
384
|
+
actual: typeof compress,
|
|
385
|
+
})
|
|
386
|
+
} else {
|
|
387
|
+
if (
|
|
388
|
+
compress.mode !== undefined &&
|
|
389
|
+
compress.mode !== "range" &&
|
|
390
|
+
compress.mode !== "message"
|
|
391
|
+
) {
|
|
392
|
+
errors.push({
|
|
393
|
+
key: "compress.mode",
|
|
394
|
+
expected: '"range" | "message"',
|
|
395
|
+
actual: JSON.stringify(compress.mode),
|
|
396
|
+
})
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
if (
|
|
400
|
+
compress.summaryBuffer !== undefined &&
|
|
401
|
+
typeof compress.summaryBuffer !== "boolean"
|
|
402
|
+
) {
|
|
403
|
+
errors.push({
|
|
404
|
+
key: "compress.summaryBuffer",
|
|
405
|
+
expected: "boolean",
|
|
406
|
+
actual: typeof compress.summaryBuffer,
|
|
407
|
+
})
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
if (
|
|
411
|
+
compress.nudgeFrequency !== undefined &&
|
|
412
|
+
typeof compress.nudgeFrequency !== "number"
|
|
413
|
+
) {
|
|
414
|
+
errors.push({
|
|
415
|
+
key: "compress.nudgeFrequency",
|
|
416
|
+
expected: "number",
|
|
417
|
+
actual: typeof compress.nudgeFrequency,
|
|
418
|
+
})
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
if (typeof compress.nudgeFrequency === "number" && compress.nudgeFrequency < 1) {
|
|
422
|
+
errors.push({
|
|
423
|
+
key: "compress.nudgeFrequency",
|
|
424
|
+
expected: "positive number (>= 1)",
|
|
425
|
+
actual: `${compress.nudgeFrequency} (will be clamped to 1)`,
|
|
426
|
+
})
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
if (
|
|
430
|
+
compress.iterationNudgeThreshold !== undefined &&
|
|
431
|
+
typeof compress.iterationNudgeThreshold !== "number"
|
|
432
|
+
) {
|
|
433
|
+
errors.push({
|
|
434
|
+
key: "compress.iterationNudgeThreshold",
|
|
435
|
+
expected: "number",
|
|
436
|
+
actual: typeof compress.iterationNudgeThreshold,
|
|
437
|
+
})
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
if (
|
|
441
|
+
compress.nudgeForce !== undefined &&
|
|
442
|
+
compress.nudgeForce !== "strong" &&
|
|
443
|
+
compress.nudgeForce !== "soft"
|
|
444
|
+
) {
|
|
445
|
+
errors.push({
|
|
446
|
+
key: "compress.nudgeForce",
|
|
447
|
+
expected: '"strong" | "soft"',
|
|
448
|
+
actual: JSON.stringify(compress.nudgeForce),
|
|
449
|
+
})
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
if (compress.protectedTools !== undefined && !Array.isArray(compress.protectedTools)) {
|
|
453
|
+
errors.push({
|
|
454
|
+
key: "compress.protectedTools",
|
|
455
|
+
expected: "string[]",
|
|
456
|
+
actual: typeof compress.protectedTools,
|
|
457
|
+
})
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
if (
|
|
461
|
+
compress.protectUserMessages !== undefined &&
|
|
462
|
+
typeof compress.protectUserMessages !== "boolean"
|
|
463
|
+
) {
|
|
464
|
+
errors.push({
|
|
465
|
+
key: "compress.protectUserMessages",
|
|
466
|
+
expected: "boolean",
|
|
467
|
+
actual: typeof compress.protectUserMessages,
|
|
468
|
+
})
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
if (
|
|
472
|
+
typeof compress.iterationNudgeThreshold === "number" &&
|
|
473
|
+
compress.iterationNudgeThreshold < 1
|
|
474
|
+
) {
|
|
475
|
+
errors.push({
|
|
476
|
+
key: "compress.iterationNudgeThreshold",
|
|
477
|
+
expected: "positive number (>= 1)",
|
|
478
|
+
actual: `${compress.iterationNudgeThreshold} (will be clamped to 1)`,
|
|
479
|
+
})
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
const validateLimitValue = (
|
|
483
|
+
key: string,
|
|
484
|
+
value: unknown,
|
|
485
|
+
actualValue: unknown = value,
|
|
486
|
+
): void => {
|
|
487
|
+
const isValidNumber = typeof value === "number"
|
|
488
|
+
const isPercentString = typeof value === "string" && value.endsWith("%")
|
|
489
|
+
|
|
490
|
+
if (!isValidNumber && !isPercentString) {
|
|
491
|
+
errors.push({
|
|
492
|
+
key,
|
|
493
|
+
expected: 'number | "${number}%"',
|
|
494
|
+
actual: JSON.stringify(actualValue),
|
|
495
|
+
})
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
const validateModelLimits = (
|
|
500
|
+
key: "compress.modelMaxLimits" | "compress.modelMinLimits",
|
|
501
|
+
limits: unknown,
|
|
502
|
+
): void => {
|
|
503
|
+
if (limits === undefined) {
|
|
504
|
+
return
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
if (typeof limits !== "object" || limits === null || Array.isArray(limits)) {
|
|
508
|
+
errors.push({
|
|
509
|
+
key,
|
|
510
|
+
expected: "Record<string, number | ${number}%>",
|
|
511
|
+
actual: typeof limits,
|
|
512
|
+
})
|
|
513
|
+
return
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
for (const [providerModelKey, limit] of Object.entries(limits)) {
|
|
517
|
+
const isValidNumber = typeof limit === "number"
|
|
518
|
+
const isPercentString =
|
|
519
|
+
typeof limit === "string" && /^\d+(?:\.\d+)?%$/.test(limit)
|
|
520
|
+
if (!isValidNumber && !isPercentString) {
|
|
521
|
+
errors.push({
|
|
522
|
+
key: `${key}.${providerModelKey}`,
|
|
523
|
+
expected: 'number | "${number}%"',
|
|
524
|
+
actual: JSON.stringify(limit),
|
|
525
|
+
})
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
if (compress.maxContextLimit !== undefined) {
|
|
531
|
+
validateLimitValue("compress.maxContextLimit", compress.maxContextLimit)
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
if (compress.minContextLimit !== undefined) {
|
|
535
|
+
validateLimitValue("compress.minContextLimit", compress.minContextLimit)
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
validateModelLimits("compress.modelMaxLimits", compress.modelMaxLimits)
|
|
539
|
+
validateModelLimits("compress.modelMinLimits", compress.modelMinLimits)
|
|
540
|
+
|
|
541
|
+
const validValues = ["ask", "allow", "deny"]
|
|
542
|
+
if (compress.permission !== undefined && !validValues.includes(compress.permission)) {
|
|
543
|
+
errors.push({
|
|
544
|
+
key: "compress.permission",
|
|
545
|
+
expected: '"ask" | "allow" | "deny"',
|
|
546
|
+
actual: JSON.stringify(compress.permission),
|
|
547
|
+
})
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
if (
|
|
551
|
+
compress.showCompression !== undefined &&
|
|
552
|
+
typeof compress.showCompression !== "boolean"
|
|
553
|
+
) {
|
|
554
|
+
errors.push({
|
|
555
|
+
key: "compress.showCompression",
|
|
556
|
+
expected: "boolean",
|
|
557
|
+
actual: typeof compress.showCompression,
|
|
558
|
+
})
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
const strategies = config.strategies
|
|
564
|
+
if (strategies) {
|
|
565
|
+
if (
|
|
566
|
+
strategies.deduplication?.enabled !== undefined &&
|
|
567
|
+
typeof strategies.deduplication.enabled !== "boolean"
|
|
568
|
+
) {
|
|
569
|
+
errors.push({
|
|
570
|
+
key: "strategies.deduplication.enabled",
|
|
571
|
+
expected: "boolean",
|
|
572
|
+
actual: typeof strategies.deduplication.enabled,
|
|
573
|
+
})
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
if (
|
|
577
|
+
strategies.deduplication?.protectedTools !== undefined &&
|
|
578
|
+
!Array.isArray(strategies.deduplication.protectedTools)
|
|
579
|
+
) {
|
|
580
|
+
errors.push({
|
|
581
|
+
key: "strategies.deduplication.protectedTools",
|
|
582
|
+
expected: "string[]",
|
|
583
|
+
actual: typeof strategies.deduplication.protectedTools,
|
|
584
|
+
})
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
if (strategies.purgeErrors) {
|
|
588
|
+
if (
|
|
589
|
+
strategies.purgeErrors.enabled !== undefined &&
|
|
590
|
+
typeof strategies.purgeErrors.enabled !== "boolean"
|
|
591
|
+
) {
|
|
592
|
+
errors.push({
|
|
593
|
+
key: "strategies.purgeErrors.enabled",
|
|
594
|
+
expected: "boolean",
|
|
595
|
+
actual: typeof strategies.purgeErrors.enabled,
|
|
596
|
+
})
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
if (
|
|
600
|
+
strategies.purgeErrors.turns !== undefined &&
|
|
601
|
+
typeof strategies.purgeErrors.turns !== "number"
|
|
602
|
+
) {
|
|
603
|
+
errors.push({
|
|
604
|
+
key: "strategies.purgeErrors.turns",
|
|
605
|
+
expected: "number",
|
|
606
|
+
actual: typeof strategies.purgeErrors.turns,
|
|
607
|
+
})
|
|
608
|
+
}
|
|
609
|
+
// Warn if turns is 0 or negative - will be clamped to 1
|
|
610
|
+
if (
|
|
611
|
+
typeof strategies.purgeErrors.turns === "number" &&
|
|
612
|
+
strategies.purgeErrors.turns < 1
|
|
613
|
+
) {
|
|
614
|
+
errors.push({
|
|
615
|
+
key: "strategies.purgeErrors.turns",
|
|
616
|
+
expected: "positive number (>= 1)",
|
|
617
|
+
actual: `${strategies.purgeErrors.turns} (will be clamped to 1)`,
|
|
618
|
+
})
|
|
619
|
+
}
|
|
620
|
+
if (
|
|
621
|
+
strategies.purgeErrors.protectedTools !== undefined &&
|
|
622
|
+
!Array.isArray(strategies.purgeErrors.protectedTools)
|
|
623
|
+
) {
|
|
624
|
+
errors.push({
|
|
625
|
+
key: "strategies.purgeErrors.protectedTools",
|
|
626
|
+
expected: "string[]",
|
|
627
|
+
actual: typeof strategies.purgeErrors.protectedTools,
|
|
628
|
+
})
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
return errors
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
function scheduleConfigWarning(
|
|
637
|
+
notify: ConfigWarningNotifier | undefined,
|
|
638
|
+
title: string,
|
|
639
|
+
message: string,
|
|
640
|
+
): void {
|
|
641
|
+
setTimeout(() => {
|
|
642
|
+
if (!notify) return
|
|
643
|
+
try {
|
|
644
|
+
notify(title, message)
|
|
645
|
+
} catch {}
|
|
646
|
+
}, 7000)
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
function showConfigWarnings(
|
|
650
|
+
notify: ConfigWarningNotifier | undefined,
|
|
651
|
+
configPath: string,
|
|
652
|
+
configData: Record<string, any>,
|
|
653
|
+
isProject: boolean,
|
|
654
|
+
): void {
|
|
655
|
+
const invalidKeys = getInvalidConfigKeys(configData)
|
|
656
|
+
const typeErrors = validateConfigTypes(configData)
|
|
657
|
+
|
|
658
|
+
if (invalidKeys.length === 0 && typeErrors.length === 0) {
|
|
659
|
+
return
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
const configType = isProject ? "project config" : "config"
|
|
663
|
+
const messages: string[] = []
|
|
664
|
+
|
|
665
|
+
if (invalidKeys.length > 0) {
|
|
666
|
+
const keyList = invalidKeys.slice(0, 3).join(", ")
|
|
667
|
+
const suffix = invalidKeys.length > 3 ? ` (+${invalidKeys.length - 3} more)` : ""
|
|
668
|
+
messages.push(`Unknown keys: ${keyList}${suffix}`)
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
if (typeErrors.length > 0) {
|
|
672
|
+
for (const err of typeErrors.slice(0, 2)) {
|
|
673
|
+
messages.push(`${err.key}: expected ${err.expected}, got ${err.actual}`)
|
|
674
|
+
}
|
|
675
|
+
if (typeErrors.length > 2) {
|
|
676
|
+
messages.push(`(+${typeErrors.length - 2} more type errors)`)
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
scheduleConfigWarning(
|
|
681
|
+
notify,
|
|
682
|
+
`DCP: ${configType} warning`,
|
|
683
|
+
`${configPath}\n${messages.join("\n")}`,
|
|
684
|
+
)
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
const defaultConfig: PluginConfig = {
|
|
688
|
+
enabled: true,
|
|
689
|
+
debug: false,
|
|
690
|
+
pruneNotification: "detailed",
|
|
691
|
+
pruneNotificationType: "chat",
|
|
692
|
+
commands: {
|
|
693
|
+
enabled: true,
|
|
694
|
+
protectedTools: [...DEFAULT_PROTECTED_TOOLS],
|
|
695
|
+
},
|
|
696
|
+
manualMode: {
|
|
697
|
+
enabled: false,
|
|
698
|
+
automaticStrategies: true,
|
|
699
|
+
},
|
|
700
|
+
tui: {
|
|
701
|
+
sidebar: true,
|
|
702
|
+
debug: false,
|
|
703
|
+
},
|
|
704
|
+
turnProtection: {
|
|
705
|
+
enabled: false,
|
|
706
|
+
turns: 4,
|
|
707
|
+
},
|
|
708
|
+
experimental: {
|
|
709
|
+
allowSubAgents: false,
|
|
710
|
+
customPrompts: false,
|
|
711
|
+
},
|
|
712
|
+
protectedFilePatterns: [],
|
|
713
|
+
compress: {
|
|
714
|
+
mode: "range",
|
|
715
|
+
permission: "allow",
|
|
716
|
+
showCompression: false,
|
|
717
|
+
summaryBuffer: true,
|
|
718
|
+
maxContextLimit: 100000,
|
|
719
|
+
minContextLimit: 50000,
|
|
720
|
+
nudgeFrequency: 5,
|
|
721
|
+
iterationNudgeThreshold: 15,
|
|
722
|
+
nudgeForce: "soft",
|
|
723
|
+
protectedTools: [...COMPRESS_DEFAULT_PROTECTED_TOOLS],
|
|
724
|
+
protectUserMessages: false,
|
|
725
|
+
},
|
|
726
|
+
strategies: {
|
|
727
|
+
deduplication: {
|
|
728
|
+
enabled: true,
|
|
729
|
+
protectedTools: [],
|
|
730
|
+
},
|
|
731
|
+
purgeErrors: {
|
|
732
|
+
enabled: true,
|
|
733
|
+
turns: 4,
|
|
734
|
+
protectedTools: [],
|
|
735
|
+
},
|
|
736
|
+
},
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
const GLOBAL_CONFIG_DIR = process.env.XDG_CONFIG_HOME
|
|
740
|
+
? join(process.env.XDG_CONFIG_HOME, "opencode")
|
|
741
|
+
: join(homedir(), ".config", "opencode")
|
|
742
|
+
const GLOBAL_CONFIG_PATH_JSONC = join(GLOBAL_CONFIG_DIR, "dcp.jsonc")
|
|
743
|
+
const GLOBAL_CONFIG_PATH_JSON = join(GLOBAL_CONFIG_DIR, "dcp.json")
|
|
744
|
+
|
|
745
|
+
function findOpencodeDir(startDir: string): string | null {
|
|
746
|
+
let current = startDir
|
|
747
|
+
while (current !== "/") {
|
|
748
|
+
const candidate = join(current, ".opencode")
|
|
749
|
+
if (existsSync(candidate) && statSync(candidate).isDirectory()) {
|
|
750
|
+
return candidate
|
|
751
|
+
}
|
|
752
|
+
const parent = dirname(current)
|
|
753
|
+
if (parent === current) {
|
|
754
|
+
break
|
|
755
|
+
}
|
|
756
|
+
current = parent
|
|
757
|
+
}
|
|
758
|
+
return null
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
function getConfigPaths(directory?: string): {
|
|
762
|
+
global: string | null
|
|
763
|
+
configDir: string | null
|
|
764
|
+
project: string | null
|
|
765
|
+
} {
|
|
766
|
+
const global = existsSync(GLOBAL_CONFIG_PATH_JSONC)
|
|
767
|
+
? GLOBAL_CONFIG_PATH_JSONC
|
|
768
|
+
: existsSync(GLOBAL_CONFIG_PATH_JSON)
|
|
769
|
+
? GLOBAL_CONFIG_PATH_JSON
|
|
770
|
+
: null
|
|
771
|
+
|
|
772
|
+
let configDir: string | null = null
|
|
773
|
+
const opencodeConfigDir = process.env.OPENCODE_CONFIG_DIR
|
|
774
|
+
if (opencodeConfigDir) {
|
|
775
|
+
const configJsonc = join(opencodeConfigDir, "dcp.jsonc")
|
|
776
|
+
const configJson = join(opencodeConfigDir, "dcp.json")
|
|
777
|
+
configDir = existsSync(configJsonc)
|
|
778
|
+
? configJsonc
|
|
779
|
+
: existsSync(configJson)
|
|
780
|
+
? configJson
|
|
781
|
+
: null
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
let project: string | null = null
|
|
785
|
+
if (directory) {
|
|
786
|
+
const opencodeDir = findOpencodeDir(directory)
|
|
787
|
+
if (opencodeDir) {
|
|
788
|
+
const projectJsonc = join(opencodeDir, "dcp.jsonc")
|
|
789
|
+
const projectJson = join(opencodeDir, "dcp.json")
|
|
790
|
+
project = existsSync(projectJsonc)
|
|
791
|
+
? projectJsonc
|
|
792
|
+
: existsSync(projectJson)
|
|
793
|
+
? projectJson
|
|
794
|
+
: null
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
return { global, configDir, project }
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
function createDefaultConfig(): void {
|
|
802
|
+
if (!existsSync(GLOBAL_CONFIG_DIR)) {
|
|
803
|
+
mkdirSync(GLOBAL_CONFIG_DIR, { recursive: true })
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
const configContent = `{
|
|
807
|
+
"$schema": "https://raw.githubusercontent.com/Opencode-DCP/opencode-dynamic-context-pruning/master/dcp.schema.json"
|
|
808
|
+
}
|
|
809
|
+
`
|
|
810
|
+
writeFileSync(GLOBAL_CONFIG_PATH_JSONC, configContent, "utf-8")
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
interface ConfigLoadResult {
|
|
814
|
+
data: Record<string, any> | null
|
|
815
|
+
parseError?: string
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
function loadConfigFile(configPath: string): ConfigLoadResult {
|
|
819
|
+
let fileContent = ""
|
|
820
|
+
try {
|
|
821
|
+
fileContent = readFileSync(configPath, "utf-8")
|
|
822
|
+
} catch {
|
|
823
|
+
return { data: null }
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
try {
|
|
827
|
+
const parsed = parse(fileContent, undefined, { allowTrailingComma: true })
|
|
828
|
+
if (parsed === undefined || parsed === null) {
|
|
829
|
+
return { data: null, parseError: "Config file is empty or invalid" }
|
|
830
|
+
}
|
|
831
|
+
return { data: parsed }
|
|
832
|
+
} catch (error: any) {
|
|
833
|
+
return { data: null, parseError: error.message || "Failed to parse config" }
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
function mergeStrategies(
|
|
838
|
+
base: PluginConfig["strategies"],
|
|
839
|
+
override?: Partial<PluginConfig["strategies"]>,
|
|
840
|
+
): PluginConfig["strategies"] {
|
|
841
|
+
if (!override) {
|
|
842
|
+
return base
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
return {
|
|
846
|
+
deduplication: {
|
|
847
|
+
enabled: override.deduplication?.enabled ?? base.deduplication.enabled,
|
|
848
|
+
protectedTools: [
|
|
849
|
+
...new Set([
|
|
850
|
+
...base.deduplication.protectedTools,
|
|
851
|
+
...(override.deduplication?.protectedTools ?? []),
|
|
852
|
+
]),
|
|
853
|
+
],
|
|
854
|
+
},
|
|
855
|
+
purgeErrors: {
|
|
856
|
+
enabled: override.purgeErrors?.enabled ?? base.purgeErrors.enabled,
|
|
857
|
+
turns: override.purgeErrors?.turns ?? base.purgeErrors.turns,
|
|
858
|
+
protectedTools: [
|
|
859
|
+
...new Set([
|
|
860
|
+
...base.purgeErrors.protectedTools,
|
|
861
|
+
...(override.purgeErrors?.protectedTools ?? []),
|
|
862
|
+
]),
|
|
863
|
+
],
|
|
864
|
+
},
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
function mergeCompress(
|
|
869
|
+
base: PluginConfig["compress"],
|
|
870
|
+
override?: CompressOverride,
|
|
871
|
+
): PluginConfig["compress"] {
|
|
872
|
+
if (!override) {
|
|
873
|
+
return base
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
return {
|
|
877
|
+
mode: override.mode ?? base.mode,
|
|
878
|
+
permission: override.permission ?? base.permission,
|
|
879
|
+
showCompression: override.showCompression ?? base.showCompression,
|
|
880
|
+
summaryBuffer: override.summaryBuffer ?? base.summaryBuffer,
|
|
881
|
+
maxContextLimit: override.maxContextLimit ?? base.maxContextLimit,
|
|
882
|
+
minContextLimit: override.minContextLimit ?? base.minContextLimit,
|
|
883
|
+
modelMaxLimits: override.modelMaxLimits ?? base.modelMaxLimits,
|
|
884
|
+
modelMinLimits: override.modelMinLimits ?? base.modelMinLimits,
|
|
885
|
+
nudgeFrequency: override.nudgeFrequency ?? base.nudgeFrequency,
|
|
886
|
+
iterationNudgeThreshold: override.iterationNudgeThreshold ?? base.iterationNudgeThreshold,
|
|
887
|
+
nudgeForce: override.nudgeForce ?? base.nudgeForce,
|
|
888
|
+
protectedTools: [...new Set([...base.protectedTools, ...(override.protectedTools ?? [])])],
|
|
889
|
+
protectUserMessages: override.protectUserMessages ?? base.protectUserMessages,
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
function mergeCommands(
|
|
894
|
+
base: PluginConfig["commands"],
|
|
895
|
+
override?: Partial<PluginConfig["commands"]>,
|
|
896
|
+
): PluginConfig["commands"] {
|
|
897
|
+
if (!override) {
|
|
898
|
+
return base
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
return {
|
|
902
|
+
enabled: override.enabled ?? base.enabled,
|
|
903
|
+
protectedTools: [...new Set([...base.protectedTools, ...(override.protectedTools ?? [])])],
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
function mergeManualMode(
|
|
908
|
+
base: PluginConfig["manualMode"],
|
|
909
|
+
override?: Partial<PluginConfig["manualMode"]>,
|
|
910
|
+
): PluginConfig["manualMode"] {
|
|
911
|
+
if (override === undefined) return base
|
|
912
|
+
|
|
913
|
+
return {
|
|
914
|
+
enabled: override.enabled ?? base.enabled,
|
|
915
|
+
automaticStrategies: override.automaticStrategies ?? base.automaticStrategies,
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
function mergeTui(
|
|
920
|
+
base: PluginConfig["tui"],
|
|
921
|
+
override?: Partial<PluginConfig["tui"]>,
|
|
922
|
+
): PluginConfig["tui"] {
|
|
923
|
+
if (override === undefined) return base
|
|
924
|
+
|
|
925
|
+
return {
|
|
926
|
+
sidebar: override.sidebar ?? base.sidebar,
|
|
927
|
+
debug: override.debug ?? base.debug,
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
function mergeExperimental(
|
|
932
|
+
base: PluginConfig["experimental"],
|
|
933
|
+
override?: Partial<PluginConfig["experimental"]>,
|
|
934
|
+
): PluginConfig["experimental"] {
|
|
935
|
+
if (override === undefined) return base
|
|
936
|
+
|
|
937
|
+
return {
|
|
938
|
+
allowSubAgents: override.allowSubAgents ?? base.allowSubAgents,
|
|
939
|
+
customPrompts: override.customPrompts ?? base.customPrompts,
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
function deepCloneConfig(config: PluginConfig): PluginConfig {
|
|
944
|
+
return {
|
|
945
|
+
...config,
|
|
946
|
+
commands: {
|
|
947
|
+
enabled: config.commands.enabled,
|
|
948
|
+
protectedTools: [...config.commands.protectedTools],
|
|
949
|
+
},
|
|
950
|
+
manualMode: {
|
|
951
|
+
enabled: config.manualMode.enabled,
|
|
952
|
+
automaticStrategies: config.manualMode.automaticStrategies,
|
|
953
|
+
},
|
|
954
|
+
tui: { ...config.tui },
|
|
955
|
+
turnProtection: { ...config.turnProtection },
|
|
956
|
+
experimental: { ...config.experimental },
|
|
957
|
+
protectedFilePatterns: [...config.protectedFilePatterns],
|
|
958
|
+
compress: {
|
|
959
|
+
...config.compress,
|
|
960
|
+
modelMaxLimits: { ...config.compress.modelMaxLimits },
|
|
961
|
+
modelMinLimits: { ...config.compress.modelMinLimits },
|
|
962
|
+
protectedTools: [...config.compress.protectedTools],
|
|
963
|
+
},
|
|
964
|
+
strategies: {
|
|
965
|
+
deduplication: {
|
|
966
|
+
...config.strategies.deduplication,
|
|
967
|
+
protectedTools: [...config.strategies.deduplication.protectedTools],
|
|
968
|
+
},
|
|
969
|
+
purgeErrors: {
|
|
970
|
+
...config.strategies.purgeErrors,
|
|
971
|
+
protectedTools: [...config.strategies.purgeErrors.protectedTools],
|
|
972
|
+
},
|
|
973
|
+
},
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
function mergeLayer(config: PluginConfig, data: Record<string, any>): PluginConfig {
|
|
978
|
+
return {
|
|
979
|
+
enabled: data.enabled ?? config.enabled,
|
|
980
|
+
debug: data.debug ?? config.debug,
|
|
981
|
+
pruneNotification: data.pruneNotification ?? config.pruneNotification,
|
|
982
|
+
pruneNotificationType: data.pruneNotificationType ?? config.pruneNotificationType,
|
|
983
|
+
tui: mergeTui(config.tui, data.tui as any),
|
|
984
|
+
commands: mergeCommands(config.commands, data.commands as any),
|
|
985
|
+
manualMode: mergeManualMode(config.manualMode, data.manualMode as any),
|
|
986
|
+
turnProtection: {
|
|
987
|
+
enabled: data.turnProtection?.enabled ?? config.turnProtection.enabled,
|
|
988
|
+
turns: data.turnProtection?.turns ?? config.turnProtection.turns,
|
|
989
|
+
},
|
|
990
|
+
experimental: mergeExperimental(config.experimental, data.experimental as any),
|
|
991
|
+
protectedFilePatterns: [
|
|
992
|
+
...new Set([...config.protectedFilePatterns, ...(data.protectedFilePatterns ?? [])]),
|
|
993
|
+
],
|
|
994
|
+
compress: mergeCompress(config.compress, data.compress as CompressOverride),
|
|
995
|
+
strategies: mergeStrategies(config.strategies, data.strategies as any),
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
function createConfigWarningCallbacks(
|
|
1000
|
+
notify?: ConfigWarningNotifier,
|
|
1001
|
+
): ConfigWarningCallbacks | undefined {
|
|
1002
|
+
if (!notify) return undefined
|
|
1003
|
+
|
|
1004
|
+
return {
|
|
1005
|
+
onParseWarning: (title, message) => scheduleConfigWarning(notify, title, message),
|
|
1006
|
+
onConfigWarning: (configPath, data, isProject) =>
|
|
1007
|
+
showConfigWarnings(notify, configPath, data, isProject),
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
function loadMergedConfig(directory?: string, callbacks?: ConfigWarningCallbacks): PluginConfig {
|
|
1012
|
+
let config = deepCloneConfig(defaultConfig)
|
|
1013
|
+
const configPaths = getConfigPaths(directory)
|
|
1014
|
+
|
|
1015
|
+
if (!configPaths.global) {
|
|
1016
|
+
createDefaultConfig()
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
const layers: Array<{ path: string | null; name: string; isProject: boolean }> = [
|
|
1020
|
+
{ path: configPaths.global, name: "config", isProject: false },
|
|
1021
|
+
{ path: configPaths.configDir, name: "configDir config", isProject: true },
|
|
1022
|
+
{ path: configPaths.project, name: "project config", isProject: true },
|
|
1023
|
+
]
|
|
1024
|
+
|
|
1025
|
+
for (const layer of layers) {
|
|
1026
|
+
if (!layer.path) {
|
|
1027
|
+
continue
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
const result = loadConfigFile(layer.path)
|
|
1031
|
+
if (result.parseError) {
|
|
1032
|
+
callbacks?.onParseWarning?.(
|
|
1033
|
+
`DCP: Invalid ${layer.name}`,
|
|
1034
|
+
`${layer.path}\n${result.parseError}\nUsing previous/default values`,
|
|
1035
|
+
)
|
|
1036
|
+
continue
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
if (!result.data) {
|
|
1040
|
+
continue
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
callbacks?.onConfigWarning?.(layer.path, result.data, layer.isProject)
|
|
1044
|
+
config = mergeLayer(config, result.data)
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
return config
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
export function getConfigForDirectory(
|
|
1051
|
+
directory?: string,
|
|
1052
|
+
notify?: ConfigWarningNotifier,
|
|
1053
|
+
): PluginConfig {
|
|
1054
|
+
return loadMergedConfig(directory, createConfigWarningCallbacks(notify))
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
export function getConfig(ctx: PluginInput): PluginConfig {
|
|
1058
|
+
return loadMergedConfig(
|
|
1059
|
+
ctx.directory,
|
|
1060
|
+
createConfigWarningCallbacks((title, message) => {
|
|
1061
|
+
ctx.client.tui.showToast({
|
|
1062
|
+
body: {
|
|
1063
|
+
title,
|
|
1064
|
+
message,
|
|
1065
|
+
variant: "warning",
|
|
1066
|
+
duration: 7000,
|
|
1067
|
+
},
|
|
1068
|
+
})
|
|
1069
|
+
}),
|
|
1070
|
+
)
|
|
1071
|
+
}
|