@royalcat/opencode-dcp-rc 0.0.0-stage → 4.0.1

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