@tarquinen/opencode-dcp 1.0.4 → 1.1.0-beta.2

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