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