@dev4s/opencode-dcp 0.1.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.
Files changed (116) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +160 -0
  3. package/dist/index.d.ts +4 -0
  4. package/dist/index.d.ts.map +1 -0
  5. package/dist/index.js +87 -0
  6. package/dist/index.js.map +1 -0
  7. package/dist/lib/config.d.ts +58 -0
  8. package/dist/lib/config.d.ts.map +1 -0
  9. package/dist/lib/config.js +696 -0
  10. package/dist/lib/config.js.map +1 -0
  11. package/dist/lib/hooks.d.ts +10 -0
  12. package/dist/lib/hooks.d.ts.map +1 -0
  13. package/dist/lib/hooks.js +45 -0
  14. package/dist/lib/hooks.js.map +1 -0
  15. package/dist/lib/logger.d.ts +31 -0
  16. package/dist/lib/logger.d.ts.map +1 -0
  17. package/dist/lib/logger.js +183 -0
  18. package/dist/lib/logger.js.map +1 -0
  19. package/dist/lib/messages/index.d.ts +2 -0
  20. package/dist/lib/messages/index.d.ts.map +1 -0
  21. package/dist/lib/messages/index.js +2 -0
  22. package/dist/lib/messages/index.js.map +1 -0
  23. package/dist/lib/messages/prune.d.ts +6 -0
  24. package/dist/lib/messages/prune.d.ts.map +1 -0
  25. package/dist/lib/messages/prune.js +187 -0
  26. package/dist/lib/messages/prune.js.map +1 -0
  27. package/dist/lib/messages/utils.d.ts +9 -0
  28. package/dist/lib/messages/utils.d.ts.map +1 -0
  29. package/dist/lib/messages/utils.js +124 -0
  30. package/dist/lib/messages/utils.js.map +1 -0
  31. package/dist/lib/model-selector.d.ts +17 -0
  32. package/dist/lib/model-selector.d.ts.map +1 -0
  33. package/dist/lib/model-selector.js +141 -0
  34. package/dist/lib/model-selector.js.map +1 -0
  35. package/dist/lib/prompt.d.ts +3 -0
  36. package/dist/lib/prompt.d.ts.map +1 -0
  37. package/dist/lib/prompt.js +128 -0
  38. package/dist/lib/prompt.js.map +1 -0
  39. package/dist/lib/prompts/discard-tool-spec.txt +41 -0
  40. package/dist/lib/prompts/extract-tool-spec.txt +47 -0
  41. package/dist/lib/prompts/on-idle-analysis.txt +30 -0
  42. package/dist/lib/prompts/user/nudge/nudge-both.txt +10 -0
  43. package/dist/lib/prompts/user/nudge/nudge-discard.txt +9 -0
  44. package/dist/lib/prompts/user/nudge/nudge-extract.txt +9 -0
  45. package/dist/lib/prompts/user/system/system-prompt-both.txt +58 -0
  46. package/dist/lib/prompts/user/system/system-prompt-discard.txt +49 -0
  47. package/dist/lib/prompts/user/system/system-prompt-extract.txt +49 -0
  48. package/dist/lib/shared-utils.d.ts +4 -0
  49. package/dist/lib/shared-utils.d.ts.map +1 -0
  50. package/dist/lib/shared-utils.js +13 -0
  51. package/dist/lib/shared-utils.js.map +1 -0
  52. package/dist/lib/state/index.d.ts +4 -0
  53. package/dist/lib/state/index.d.ts.map +1 -0
  54. package/dist/lib/state/index.js +4 -0
  55. package/dist/lib/state/index.js.map +1 -0
  56. package/dist/lib/state/persistence.d.ts +16 -0
  57. package/dist/lib/state/persistence.d.ts.map +1 -0
  58. package/dist/lib/state/persistence.js +73 -0
  59. package/dist/lib/state/persistence.js.map +1 -0
  60. package/dist/lib/state/state.d.ts +8 -0
  61. package/dist/lib/state/state.d.ts.map +1 -0
  62. package/dist/lib/state/state.js +112 -0
  63. package/dist/lib/state/state.js.map +1 -0
  64. package/dist/lib/state/tool-cache.d.ts +13 -0
  65. package/dist/lib/state/tool-cache.d.ts.map +1 -0
  66. package/dist/lib/state/tool-cache.js +74 -0
  67. package/dist/lib/state/tool-cache.js.map +1 -0
  68. package/dist/lib/state/types.d.ts +32 -0
  69. package/dist/lib/state/types.d.ts.map +1 -0
  70. package/dist/lib/state/types.js +2 -0
  71. package/dist/lib/state/types.js.map +1 -0
  72. package/dist/lib/state/utils.d.ts +2 -0
  73. package/dist/lib/state/utils.d.ts.map +1 -0
  74. package/dist/lib/state/utils.js +10 -0
  75. package/dist/lib/state/utils.js.map +1 -0
  76. package/dist/lib/strategies/deduplication.d.ts +10 -0
  77. package/dist/lib/strategies/deduplication.d.ts.map +1 -0
  78. package/dist/lib/strategies/deduplication.js +89 -0
  79. package/dist/lib/strategies/deduplication.js.map +1 -0
  80. package/dist/lib/strategies/index.d.ts +6 -0
  81. package/dist/lib/strategies/index.d.ts.map +1 -0
  82. package/dist/lib/strategies/index.js +6 -0
  83. package/dist/lib/strategies/index.js.map +1 -0
  84. package/dist/lib/strategies/on-idle.d.ts +14 -0
  85. package/dist/lib/strategies/on-idle.d.ts.map +1 -0
  86. package/dist/lib/strategies/on-idle.js +220 -0
  87. package/dist/lib/strategies/on-idle.js.map +1 -0
  88. package/dist/lib/strategies/purge-errors.d.ts +13 -0
  89. package/dist/lib/strategies/purge-errors.d.ts.map +1 -0
  90. package/dist/lib/strategies/purge-errors.js +54 -0
  91. package/dist/lib/strategies/purge-errors.js.map +1 -0
  92. package/dist/lib/strategies/supersede-writes.d.ts +13 -0
  93. package/dist/lib/strategies/supersede-writes.d.ts.map +1 -0
  94. package/dist/lib/strategies/supersede-writes.js +80 -0
  95. package/dist/lib/strategies/supersede-writes.js.map +1 -0
  96. package/dist/lib/strategies/tools.d.ts +14 -0
  97. package/dist/lib/strategies/tools.d.ts.map +1 -0
  98. package/dist/lib/strategies/tools.js +137 -0
  99. package/dist/lib/strategies/tools.js.map +1 -0
  100. package/dist/lib/strategies/utils.d.ts +12 -0
  101. package/dist/lib/strategies/utils.d.ts.map +1 -0
  102. package/dist/lib/strategies/utils.js +82 -0
  103. package/dist/lib/strategies/utils.js.map +1 -0
  104. package/dist/lib/tokenizer.d.ts +24 -0
  105. package/dist/lib/tokenizer.d.ts.map +1 -0
  106. package/dist/lib/tokenizer.js +45 -0
  107. package/dist/lib/tokenizer.js.map +1 -0
  108. package/dist/lib/ui/notification.d.ts +9 -0
  109. package/dist/lib/ui/notification.d.ts.map +1 -0
  110. package/dist/lib/ui/notification.js +70 -0
  111. package/dist/lib/ui/notification.js.map +1 -0
  112. package/dist/lib/ui/utils.d.ts +15 -0
  113. package/dist/lib/ui/utils.d.ts.map +1 -0
  114. package/dist/lib/ui/utils.js +87 -0
  115. package/dist/lib/ui/utils.js.map +1 -0
  116. package/package.json +57 -0
@@ -0,0 +1,696 @@
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
+ const DEFAULT_PROTECTED_TOOLS = ["task", "todowrite", "todoread", "discard", "extract", "batch"];
6
+ // Valid config keys for validation against user config
7
+ export const VALID_CONFIG_KEYS = new Set([
8
+ // Top-level keys
9
+ "enabled",
10
+ "debug",
11
+ "showUpdateToasts", // Deprecated but kept for backwards compatibility
12
+ "pruneNotification",
13
+ "turnProtection",
14
+ "turnProtection.enabled",
15
+ "turnProtection.turns",
16
+ "tools",
17
+ "tools.settings",
18
+ "tools.settings.nudgeEnabled",
19
+ "tools.settings.nudgeFrequency",
20
+ "tools.settings.protectedTools",
21
+ "tools.discard",
22
+ "tools.discard.enabled",
23
+ "tools.extract",
24
+ "tools.extract.enabled",
25
+ "tools.extract.showDistillation",
26
+ "strategies",
27
+ // strategies.deduplication
28
+ "strategies.deduplication",
29
+ "strategies.deduplication.enabled",
30
+ "strategies.deduplication.protectedTools",
31
+ // strategies.supersedeWrites
32
+ "strategies.supersedeWrites",
33
+ "strategies.supersedeWrites.enabled",
34
+ // strategies.purgeErrors
35
+ "strategies.purgeErrors",
36
+ "strategies.purgeErrors.enabled",
37
+ "strategies.purgeErrors.turns",
38
+ "strategies.purgeErrors.protectedTools",
39
+ // strategies.onIdle
40
+ "strategies.onIdle",
41
+ "strategies.onIdle.enabled",
42
+ "strategies.onIdle.model",
43
+ "strategies.onIdle.showModelErrorToasts",
44
+ "strategies.onIdle.strictModelSelection",
45
+ "strategies.onIdle.protectedTools",
46
+ ]);
47
+ // Extract all key paths from a config object for validation
48
+ function getConfigKeyPaths(obj, prefix = "") {
49
+ const keys = [];
50
+ for (const key of Object.keys(obj)) {
51
+ const fullKey = prefix ? `${prefix}.${key}` : key;
52
+ keys.push(fullKey);
53
+ if (obj[key] && typeof obj[key] === "object" && !Array.isArray(obj[key])) {
54
+ keys.push(...getConfigKeyPaths(obj[key], fullKey));
55
+ }
56
+ }
57
+ return keys;
58
+ }
59
+ // Returns invalid keys found in user config
60
+ export function getInvalidConfigKeys(userConfig) {
61
+ const userKeys = getConfigKeyPaths(userConfig);
62
+ return userKeys.filter((key) => !VALID_CONFIG_KEYS.has(key));
63
+ }
64
+ function validateConfigTypes(config) {
65
+ const errors = [];
66
+ // Top-level validators
67
+ if (config.enabled !== undefined && typeof config.enabled !== "boolean") {
68
+ errors.push({ key: "enabled", expected: "boolean", actual: typeof config.enabled });
69
+ }
70
+ if (config.debug !== undefined && typeof config.debug !== "boolean") {
71
+ errors.push({ key: "debug", expected: "boolean", actual: typeof config.debug });
72
+ }
73
+ if (config.pruneNotification !== undefined) {
74
+ const validValues = ["off", "minimal", "detailed"];
75
+ if (!validValues.includes(config.pruneNotification)) {
76
+ errors.push({
77
+ key: "pruneNotification",
78
+ expected: '"off" | "minimal" | "detailed"',
79
+ actual: JSON.stringify(config.pruneNotification),
80
+ });
81
+ }
82
+ }
83
+ // Top-level turnProtection validator
84
+ if (config.turnProtection) {
85
+ if (config.turnProtection.enabled !== undefined &&
86
+ typeof config.turnProtection.enabled !== "boolean") {
87
+ errors.push({
88
+ key: "turnProtection.enabled",
89
+ expected: "boolean",
90
+ actual: typeof config.turnProtection.enabled,
91
+ });
92
+ }
93
+ if (config.turnProtection.turns !== undefined &&
94
+ typeof config.turnProtection.turns !== "number") {
95
+ errors.push({
96
+ key: "turnProtection.turns",
97
+ expected: "number",
98
+ actual: typeof config.turnProtection.turns,
99
+ });
100
+ }
101
+ }
102
+ // Tools validators
103
+ const tools = config.tools;
104
+ if (tools) {
105
+ if (tools.settings) {
106
+ if (tools.settings.nudgeEnabled !== undefined &&
107
+ typeof tools.settings.nudgeEnabled !== "boolean") {
108
+ errors.push({
109
+ key: "tools.settings.nudgeEnabled",
110
+ expected: "boolean",
111
+ actual: typeof tools.settings.nudgeEnabled,
112
+ });
113
+ }
114
+ if (tools.settings.nudgeFrequency !== undefined &&
115
+ typeof tools.settings.nudgeFrequency !== "number") {
116
+ errors.push({
117
+ key: "tools.settings.nudgeFrequency",
118
+ expected: "number",
119
+ actual: typeof tools.settings.nudgeFrequency,
120
+ });
121
+ }
122
+ if (tools.settings.protectedTools !== undefined &&
123
+ !Array.isArray(tools.settings.protectedTools)) {
124
+ errors.push({
125
+ key: "tools.settings.protectedTools",
126
+ expected: "string[]",
127
+ actual: typeof tools.settings.protectedTools,
128
+ });
129
+ }
130
+ }
131
+ if (tools.discard) {
132
+ if (tools.discard.enabled !== undefined && typeof tools.discard.enabled !== "boolean") {
133
+ errors.push({
134
+ key: "tools.discard.enabled",
135
+ expected: "boolean",
136
+ actual: typeof tools.discard.enabled,
137
+ });
138
+ }
139
+ }
140
+ if (tools.extract) {
141
+ if (tools.extract.enabled !== undefined && typeof tools.extract.enabled !== "boolean") {
142
+ errors.push({
143
+ key: "tools.extract.enabled",
144
+ expected: "boolean",
145
+ actual: typeof tools.extract.enabled,
146
+ });
147
+ }
148
+ if (tools.extract.showDistillation !== undefined &&
149
+ typeof tools.extract.showDistillation !== "boolean") {
150
+ errors.push({
151
+ key: "tools.extract.showDistillation",
152
+ expected: "boolean",
153
+ actual: typeof tools.extract.showDistillation,
154
+ });
155
+ }
156
+ }
157
+ }
158
+ // Strategies validators
159
+ const strategies = config.strategies;
160
+ if (strategies) {
161
+ // deduplication
162
+ if (strategies.deduplication?.enabled !== undefined &&
163
+ typeof strategies.deduplication.enabled !== "boolean") {
164
+ errors.push({
165
+ key: "strategies.deduplication.enabled",
166
+ expected: "boolean",
167
+ actual: typeof strategies.deduplication.enabled,
168
+ });
169
+ }
170
+ if (strategies.deduplication?.protectedTools !== undefined &&
171
+ !Array.isArray(strategies.deduplication.protectedTools)) {
172
+ errors.push({
173
+ key: "strategies.deduplication.protectedTools",
174
+ expected: "string[]",
175
+ actual: typeof strategies.deduplication.protectedTools,
176
+ });
177
+ }
178
+ // onIdle
179
+ if (strategies.onIdle) {
180
+ if (strategies.onIdle.enabled !== undefined &&
181
+ typeof strategies.onIdle.enabled !== "boolean") {
182
+ errors.push({
183
+ key: "strategies.onIdle.enabled",
184
+ expected: "boolean",
185
+ actual: typeof strategies.onIdle.enabled,
186
+ });
187
+ }
188
+ if (strategies.onIdle.model !== undefined &&
189
+ typeof strategies.onIdle.model !== "string") {
190
+ errors.push({
191
+ key: "strategies.onIdle.model",
192
+ expected: "string",
193
+ actual: typeof strategies.onIdle.model,
194
+ });
195
+ }
196
+ if (strategies.onIdle.showModelErrorToasts !== undefined &&
197
+ typeof strategies.onIdle.showModelErrorToasts !== "boolean") {
198
+ errors.push({
199
+ key: "strategies.onIdle.showModelErrorToasts",
200
+ expected: "boolean",
201
+ actual: typeof strategies.onIdle.showModelErrorToasts,
202
+ });
203
+ }
204
+ if (strategies.onIdle.strictModelSelection !== undefined &&
205
+ typeof strategies.onIdle.strictModelSelection !== "boolean") {
206
+ errors.push({
207
+ key: "strategies.onIdle.strictModelSelection",
208
+ expected: "boolean",
209
+ actual: typeof strategies.onIdle.strictModelSelection,
210
+ });
211
+ }
212
+ if (strategies.onIdle.protectedTools !== undefined &&
213
+ !Array.isArray(strategies.onIdle.protectedTools)) {
214
+ errors.push({
215
+ key: "strategies.onIdle.protectedTools",
216
+ expected: "string[]",
217
+ actual: typeof strategies.onIdle.protectedTools,
218
+ });
219
+ }
220
+ }
221
+ // supersedeWrites
222
+ if (strategies.supersedeWrites) {
223
+ if (strategies.supersedeWrites.enabled !== undefined &&
224
+ typeof strategies.supersedeWrites.enabled !== "boolean") {
225
+ errors.push({
226
+ key: "strategies.supersedeWrites.enabled",
227
+ expected: "boolean",
228
+ actual: typeof strategies.supersedeWrites.enabled,
229
+ });
230
+ }
231
+ }
232
+ // purgeErrors
233
+ if (strategies.purgeErrors) {
234
+ if (strategies.purgeErrors.enabled !== undefined &&
235
+ typeof strategies.purgeErrors.enabled !== "boolean") {
236
+ errors.push({
237
+ key: "strategies.purgeErrors.enabled",
238
+ expected: "boolean",
239
+ actual: typeof strategies.purgeErrors.enabled,
240
+ });
241
+ }
242
+ if (strategies.purgeErrors.turns !== undefined &&
243
+ typeof strategies.purgeErrors.turns !== "number") {
244
+ errors.push({
245
+ key: "strategies.purgeErrors.turns",
246
+ expected: "number",
247
+ actual: typeof strategies.purgeErrors.turns,
248
+ });
249
+ }
250
+ if (strategies.purgeErrors.protectedTools !== undefined &&
251
+ !Array.isArray(strategies.purgeErrors.protectedTools)) {
252
+ errors.push({
253
+ key: "strategies.purgeErrors.protectedTools",
254
+ expected: "string[]",
255
+ actual: typeof strategies.purgeErrors.protectedTools,
256
+ });
257
+ }
258
+ }
259
+ }
260
+ return errors;
261
+ }
262
+ // Show validation warnings for a config file
263
+ function showConfigValidationWarnings(ctx, configPath, configData, isProject) {
264
+ const invalidKeys = getInvalidConfigKeys(configData);
265
+ const typeErrors = validateConfigTypes(configData);
266
+ if (invalidKeys.length === 0 && typeErrors.length === 0) {
267
+ return;
268
+ }
269
+ const configType = isProject ? "project config" : "config";
270
+ const messages = [];
271
+ if (invalidKeys.length > 0) {
272
+ const keyList = invalidKeys.slice(0, 3).join(", ");
273
+ const suffix = invalidKeys.length > 3 ? ` (+${invalidKeys.length - 3} more)` : "";
274
+ messages.push(`Unknown keys: ${keyList}${suffix}`);
275
+ }
276
+ if (typeErrors.length > 0) {
277
+ for (const err of typeErrors.slice(0, 2)) {
278
+ messages.push(`${err.key}: expected ${err.expected}, got ${err.actual}`);
279
+ }
280
+ if (typeErrors.length > 2) {
281
+ messages.push(`(+${typeErrors.length - 2} more type errors)`);
282
+ }
283
+ }
284
+ setTimeout(() => {
285
+ try {
286
+ ctx.client.tui.showToast({
287
+ body: {
288
+ title: `DCP: Invalid ${configType}`,
289
+ message: `${configPath}\n${messages.join("\n")}`,
290
+ variant: "warning",
291
+ duration: 7000,
292
+ },
293
+ });
294
+ }
295
+ catch { }
296
+ }, 7000);
297
+ }
298
+ const defaultConfig = {
299
+ enabled: true,
300
+ debug: false,
301
+ pruneNotification: "detailed",
302
+ turnProtection: {
303
+ enabled: false,
304
+ turns: 4,
305
+ },
306
+ tools: {
307
+ settings: {
308
+ nudgeEnabled: true,
309
+ nudgeFrequency: 10,
310
+ protectedTools: [...DEFAULT_PROTECTED_TOOLS],
311
+ },
312
+ discard: {
313
+ enabled: true,
314
+ },
315
+ extract: {
316
+ enabled: true,
317
+ showDistillation: false,
318
+ },
319
+ },
320
+ strategies: {
321
+ deduplication: {
322
+ enabled: true,
323
+ protectedTools: [...DEFAULT_PROTECTED_TOOLS],
324
+ },
325
+ supersedeWrites: {
326
+ enabled: true,
327
+ },
328
+ purgeErrors: {
329
+ enabled: true,
330
+ turns: 4,
331
+ protectedTools: [...DEFAULT_PROTECTED_TOOLS],
332
+ },
333
+ onIdle: {
334
+ enabled: false,
335
+ protectedTools: [...DEFAULT_PROTECTED_TOOLS],
336
+ showModelErrorToasts: true,
337
+ strictModelSelection: false,
338
+ },
339
+ },
340
+ };
341
+ const GLOBAL_CONFIG_DIR = join(homedir(), ".config", "opencode");
342
+ const GLOBAL_CONFIG_PATH_JSONC = join(GLOBAL_CONFIG_DIR, "dcp.jsonc");
343
+ const GLOBAL_CONFIG_PATH_JSON = join(GLOBAL_CONFIG_DIR, "dcp.json");
344
+ function findOpencodeDir(startDir) {
345
+ let current = startDir;
346
+ while (current !== "/") {
347
+ const candidate = join(current, ".opencode");
348
+ if (existsSync(candidate) && statSync(candidate).isDirectory()) {
349
+ return candidate;
350
+ }
351
+ const parent = dirname(current);
352
+ if (parent === current)
353
+ break;
354
+ current = parent;
355
+ }
356
+ return null;
357
+ }
358
+ function getConfigPaths(ctx) {
359
+ // Global: ~/.config/opencode/dcp.jsonc|json
360
+ let globalPath = null;
361
+ if (existsSync(GLOBAL_CONFIG_PATH_JSONC)) {
362
+ globalPath = GLOBAL_CONFIG_PATH_JSONC;
363
+ }
364
+ else if (existsSync(GLOBAL_CONFIG_PATH_JSON)) {
365
+ globalPath = GLOBAL_CONFIG_PATH_JSON;
366
+ }
367
+ // Custom config directory: $OPENCODE_CONFIG_DIR/dcp.jsonc|json
368
+ let configDirPath = null;
369
+ const opencodeConfigDir = process.env.OPENCODE_CONFIG_DIR;
370
+ if (opencodeConfigDir) {
371
+ const configJsonc = join(opencodeConfigDir, "dcp.jsonc");
372
+ const configJson = join(opencodeConfigDir, "dcp.json");
373
+ if (existsSync(configJsonc)) {
374
+ configDirPath = configJsonc;
375
+ }
376
+ else if (existsSync(configJson)) {
377
+ configDirPath = configJson;
378
+ }
379
+ }
380
+ // Project: <project>/.opencode/dcp.jsonc|json
381
+ let projectPath = null;
382
+ if (ctx?.directory) {
383
+ const opencodeDir = findOpencodeDir(ctx.directory);
384
+ if (opencodeDir) {
385
+ const projectJsonc = join(opencodeDir, "dcp.jsonc");
386
+ const projectJson = join(opencodeDir, "dcp.json");
387
+ if (existsSync(projectJsonc)) {
388
+ projectPath = projectJsonc;
389
+ }
390
+ else if (existsSync(projectJson)) {
391
+ projectPath = projectJson;
392
+ }
393
+ }
394
+ }
395
+ return { global: globalPath, configDir: configDirPath, project: projectPath };
396
+ }
397
+ function createDefaultConfig() {
398
+ if (!existsSync(GLOBAL_CONFIG_DIR)) {
399
+ mkdirSync(GLOBAL_CONFIG_DIR, { recursive: true });
400
+ }
401
+ const configContent = `{
402
+ // Enable or disable the plugin
403
+ "enabled": true,
404
+ // Enable debug logging to ~/.config/opencode/logs/dcp/
405
+ "debug": false,
406
+ // Notification display: "off", "minimal", or "detailed"
407
+ "pruneNotification": "detailed",
408
+ // Protect from pruning for <turns> message turns
409
+ "turnProtection": {
410
+ "enabled": false,
411
+ "turns": 4
412
+ },
413
+ // LLM-driven context pruning tools
414
+ "tools": {
415
+ // Shared settings for all prune tools
416
+ "settings": {
417
+ // Nudge the LLM to use prune tools (every <nudgeFrequency> tool results)
418
+ "nudgeEnabled": true,
419
+ "nudgeFrequency": 10,
420
+ // Additional tools to protect from pruning
421
+ "protectedTools": []
422
+ },
423
+ // Removes tool content from context without preservation (for completed tasks or noise)
424
+ "discard": {
425
+ "enabled": true
426
+ },
427
+ // Distills key findings into preserved knowledge before removing raw content
428
+ "extract": {
429
+ "enabled": true,
430
+ // Show distillation content as an ignored message notification
431
+ "showDistillation": false
432
+ }
433
+ },
434
+ // Automatic pruning strategies
435
+ "strategies": {
436
+ // Remove duplicate tool calls (same tool with same arguments)
437
+ "deduplication": {
438
+ "enabled": true,
439
+ // Additional tools to protect from pruning
440
+ "protectedTools": []
441
+ },
442
+ // Prune write tool inputs when the file has been subsequently read
443
+ "supersedeWrites": {
444
+ "enabled": true
445
+ },
446
+ // Prune tool inputs for errored tools after X turns
447
+ "purgeErrors": {
448
+ "enabled": true,
449
+ // Number of turns before errored tool inputs are pruned
450
+ "turns": 4,
451
+ // Additional tools to protect from pruning
452
+ "protectedTools": []
453
+ },
454
+ // (Legacy) Run an LLM to analyze what tool calls are no longer relevant on idle
455
+ "onIdle": {
456
+ "enabled": false,
457
+ // Additional tools to protect from pruning
458
+ "protectedTools": [],
459
+ // Override model for analysis (format: "provider/model")
460
+ // "model": "anthropic/claude-haiku-4-5",
461
+ // Show toast notifications when model selection fails
462
+ "showModelErrorToasts": true,
463
+ // When true, fallback models are not permitted
464
+ "strictModelSelection": false
465
+ }
466
+ }
467
+ }
468
+ `;
469
+ writeFileSync(GLOBAL_CONFIG_PATH_JSONC, configContent, "utf-8");
470
+ }
471
+ function loadConfigFile(configPath) {
472
+ let fileContent;
473
+ try {
474
+ fileContent = readFileSync(configPath, "utf-8");
475
+ }
476
+ catch {
477
+ // File doesn't exist or can't be read - not a parse error
478
+ return { data: null };
479
+ }
480
+ try {
481
+ const parsed = parse(fileContent);
482
+ if (parsed === undefined || parsed === null) {
483
+ return { data: null, parseError: "Config file is empty or invalid" };
484
+ }
485
+ return { data: parsed };
486
+ }
487
+ catch (error) {
488
+ return { data: null, parseError: error.message || "Failed to parse config" };
489
+ }
490
+ }
491
+ function mergeStrategies(base, override) {
492
+ if (!override)
493
+ return base;
494
+ return {
495
+ deduplication: {
496
+ enabled: override.deduplication?.enabled ?? base.deduplication.enabled,
497
+ protectedTools: [
498
+ ...new Set([
499
+ ...base.deduplication.protectedTools,
500
+ ...(override.deduplication?.protectedTools ?? []),
501
+ ]),
502
+ ],
503
+ },
504
+ supersedeWrites: {
505
+ enabled: override.supersedeWrites?.enabled ?? base.supersedeWrites.enabled,
506
+ },
507
+ purgeErrors: {
508
+ enabled: override.purgeErrors?.enabled ?? base.purgeErrors.enabled,
509
+ turns: override.purgeErrors?.turns ?? base.purgeErrors.turns,
510
+ protectedTools: [
511
+ ...new Set([
512
+ ...base.purgeErrors.protectedTools,
513
+ ...(override.purgeErrors?.protectedTools ?? []),
514
+ ]),
515
+ ],
516
+ },
517
+ onIdle: {
518
+ enabled: override.onIdle?.enabled ?? base.onIdle.enabled,
519
+ model: override.onIdle?.model ?? base.onIdle.model,
520
+ showModelErrorToasts: override.onIdle?.showModelErrorToasts ?? base.onIdle.showModelErrorToasts,
521
+ strictModelSelection: override.onIdle?.strictModelSelection ?? base.onIdle.strictModelSelection,
522
+ protectedTools: [
523
+ ...new Set([
524
+ ...base.onIdle.protectedTools,
525
+ ...(override.onIdle?.protectedTools ?? []),
526
+ ]),
527
+ ],
528
+ },
529
+ };
530
+ }
531
+ function mergeTools(base, override) {
532
+ if (!override)
533
+ return base;
534
+ return {
535
+ settings: {
536
+ nudgeEnabled: override.settings?.nudgeEnabled ?? base.settings.nudgeEnabled,
537
+ nudgeFrequency: override.settings?.nudgeFrequency ?? base.settings.nudgeFrequency,
538
+ protectedTools: [
539
+ ...new Set([
540
+ ...base.settings.protectedTools,
541
+ ...(override.settings?.protectedTools ?? []),
542
+ ]),
543
+ ],
544
+ },
545
+ discard: {
546
+ enabled: override.discard?.enabled ?? base.discard.enabled,
547
+ },
548
+ extract: {
549
+ enabled: override.extract?.enabled ?? base.extract.enabled,
550
+ showDistillation: override.extract?.showDistillation ?? base.extract.showDistillation,
551
+ },
552
+ };
553
+ }
554
+ function deepCloneConfig(config) {
555
+ return {
556
+ ...config,
557
+ turnProtection: { ...config.turnProtection },
558
+ tools: {
559
+ settings: {
560
+ ...config.tools.settings,
561
+ protectedTools: [...config.tools.settings.protectedTools],
562
+ },
563
+ discard: { ...config.tools.discard },
564
+ extract: { ...config.tools.extract },
565
+ },
566
+ strategies: {
567
+ deduplication: {
568
+ ...config.strategies.deduplication,
569
+ protectedTools: [...config.strategies.deduplication.protectedTools],
570
+ },
571
+ supersedeWrites: {
572
+ ...config.strategies.supersedeWrites,
573
+ },
574
+ purgeErrors: {
575
+ ...config.strategies.purgeErrors,
576
+ protectedTools: [...config.strategies.purgeErrors.protectedTools],
577
+ },
578
+ onIdle: {
579
+ ...config.strategies.onIdle,
580
+ protectedTools: [...config.strategies.onIdle.protectedTools],
581
+ },
582
+ },
583
+ };
584
+ }
585
+ export function getConfig(ctx) {
586
+ let config = deepCloneConfig(defaultConfig);
587
+ const configPaths = getConfigPaths(ctx);
588
+ // Load and merge global config
589
+ if (configPaths.global) {
590
+ const result = loadConfigFile(configPaths.global);
591
+ if (result.parseError) {
592
+ setTimeout(async () => {
593
+ try {
594
+ ctx.client.tui.showToast({
595
+ body: {
596
+ title: "DCP: Invalid config",
597
+ message: `${configPaths.global}\n${result.parseError}\nUsing default values`,
598
+ variant: "warning",
599
+ duration: 7000,
600
+ },
601
+ });
602
+ }
603
+ catch { }
604
+ }, 7000);
605
+ }
606
+ else if (result.data) {
607
+ // Validate config keys and types
608
+ showConfigValidationWarnings(ctx, configPaths.global, result.data, false);
609
+ config = {
610
+ enabled: result.data.enabled ?? config.enabled,
611
+ debug: result.data.debug ?? config.debug,
612
+ pruneNotification: result.data.pruneNotification ?? config.pruneNotification,
613
+ turnProtection: {
614
+ enabled: result.data.turnProtection?.enabled ?? config.turnProtection.enabled,
615
+ turns: result.data.turnProtection?.turns ?? config.turnProtection.turns,
616
+ },
617
+ tools: mergeTools(config.tools, result.data.tools),
618
+ strategies: mergeStrategies(config.strategies, result.data.strategies),
619
+ };
620
+ }
621
+ }
622
+ else {
623
+ // No config exists, create default
624
+ createDefaultConfig();
625
+ }
626
+ // Load and merge $OPENCODE_CONFIG_DIR/dcp.jsonc|json (overrides global)
627
+ if (configPaths.configDir) {
628
+ const result = loadConfigFile(configPaths.configDir);
629
+ if (result.parseError) {
630
+ setTimeout(async () => {
631
+ try {
632
+ ctx.client.tui.showToast({
633
+ body: {
634
+ title: "DCP: Invalid configDir config",
635
+ message: `${configPaths.configDir}\n${result.parseError}\nUsing global/default values`,
636
+ variant: "warning",
637
+ duration: 7000,
638
+ },
639
+ });
640
+ }
641
+ catch { }
642
+ }, 7000);
643
+ }
644
+ else if (result.data) {
645
+ // Validate config keys and types
646
+ showConfigValidationWarnings(ctx, configPaths.configDir, result.data, true);
647
+ config = {
648
+ enabled: result.data.enabled ?? config.enabled,
649
+ debug: result.data.debug ?? config.debug,
650
+ pruneNotification: result.data.pruneNotification ?? config.pruneNotification,
651
+ turnProtection: {
652
+ enabled: result.data.turnProtection?.enabled ?? config.turnProtection.enabled,
653
+ turns: result.data.turnProtection?.turns ?? config.turnProtection.turns,
654
+ },
655
+ tools: mergeTools(config.tools, result.data.tools),
656
+ strategies: mergeStrategies(config.strategies, result.data.strategies),
657
+ };
658
+ }
659
+ }
660
+ // Load and merge project config (overrides global)
661
+ if (configPaths.project) {
662
+ const result = loadConfigFile(configPaths.project);
663
+ if (result.parseError) {
664
+ setTimeout(async () => {
665
+ try {
666
+ ctx.client.tui.showToast({
667
+ body: {
668
+ title: "DCP: Invalid project config",
669
+ message: `${configPaths.project}\n${result.parseError}\nUsing global/default values`,
670
+ variant: "warning",
671
+ duration: 7000,
672
+ },
673
+ });
674
+ }
675
+ catch { }
676
+ }, 7000);
677
+ }
678
+ else if (result.data) {
679
+ // Validate config keys and types
680
+ showConfigValidationWarnings(ctx, configPaths.project, result.data, true);
681
+ config = {
682
+ enabled: result.data.enabled ?? config.enabled,
683
+ debug: result.data.debug ?? config.debug,
684
+ pruneNotification: result.data.pruneNotification ?? config.pruneNotification,
685
+ turnProtection: {
686
+ enabled: result.data.turnProtection?.enabled ?? config.turnProtection.enabled,
687
+ turns: result.data.turnProtection?.turns ?? config.turnProtection.turns,
688
+ },
689
+ tools: mergeTools(config.tools, result.data.tools),
690
+ strategies: mergeStrategies(config.strategies, result.data.strategies),
691
+ };
692
+ }
693
+ }
694
+ return config;
695
+ }
696
+ //# sourceMappingURL=config.js.map