@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.
- package/LICENSE +619 -0
- package/README.md +348 -2
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8129 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/commands/compression-targets.d.ts +13 -0
- package/dist/lib/commands/compression-targets.d.ts.map +1 -0
- package/dist/lib/commands/context.d.ts +67 -0
- package/dist/lib/commands/context.d.ts.map +1 -0
- package/dist/lib/commands/decompress.d.ts +12 -0
- package/dist/lib/commands/decompress.d.ts.map +1 -0
- package/dist/lib/commands/help.d.ts +18 -0
- package/dist/lib/commands/help.d.ts.map +1 -0
- package/dist/lib/commands/index.d.ts +8 -0
- package/dist/lib/commands/index.d.ts.map +1 -0
- package/dist/lib/commands/manual.d.ts +23 -0
- package/dist/lib/commands/manual.d.ts.map +1 -0
- package/dist/lib/commands/recompress.d.ts +12 -0
- package/dist/lib/commands/recompress.d.ts.map +1 -0
- package/dist/lib/commands/stats.d.ts +27 -0
- package/dist/lib/commands/stats.d.ts.map +1 -0
- package/dist/lib/commands/sweep.d.ts +23 -0
- package/dist/lib/commands/sweep.d.ts.map +1 -0
- package/dist/lib/compress/index.d.ts +3 -0
- package/dist/lib/compress/index.d.ts.map +1 -0
- package/dist/lib/compress/pipeline.d.ts +29 -0
- package/dist/lib/compress/pipeline.d.ts.map +1 -0
- package/dist/lib/compress/protected-content.d.ts +6 -0
- package/dist/lib/compress/protected-content.d.ts.map +1 -0
- package/dist/lib/compress/rc.d.ts +15 -0
- package/dist/lib/compress/rc.d.ts.map +1 -0
- package/dist/lib/compress/search.d.ts +12 -0
- package/dist/lib/compress/search.d.ts.map +1 -0
- package/dist/lib/compress/state.d.ts +10 -0
- package/dist/lib/compress/state.d.ts.map +1 -0
- package/dist/lib/compress/summary.d.ts +68 -0
- package/dist/lib/compress/summary.d.ts.map +1 -0
- package/dist/lib/compress/timing.d.ts +18 -0
- package/dist/lib/compress/timing.d.ts.map +1 -0
- package/dist/lib/compress/types.d.ts +64 -0
- package/dist/lib/compress/types.d.ts.map +1 -0
- package/dist/lib/compress/usage.d.ts +45 -0
- package/dist/lib/compress/usage.d.ts.map +1 -0
- package/dist/lib/compress-permission.d.ts +4 -0
- package/dist/lib/compress-permission.d.ts.map +1 -0
- package/dist/lib/config.d.ts +83 -0
- package/dist/lib/config.d.ts.map +1 -0
- package/dist/lib/hooks.d.ts +22 -0
- package/dist/lib/hooks.d.ts.map +1 -0
- package/dist/lib/logger.d.ts +31 -0
- package/dist/lib/logger.d.ts.map +1 -0
- package/dist/lib/message-ids.d.ts +20 -0
- package/dist/lib/message-ids.d.ts.map +1 -0
- package/dist/lib/messages/index.d.ts +8 -0
- package/dist/lib/messages/index.d.ts.map +1 -0
- package/dist/lib/messages/inject/inject.d.ts +7 -0
- package/dist/lib/messages/inject/inject.d.ts.map +1 -0
- package/dist/lib/messages/inject/subagent-results.d.ts +4 -0
- package/dist/lib/messages/inject/subagent-results.d.ts.map +1 -0
- package/dist/lib/messages/inject/utils.d.ts +23 -0
- package/dist/lib/messages/inject/utils.d.ts.map +1 -0
- package/dist/lib/messages/prune.d.ts +4 -0
- package/dist/lib/messages/prune.d.ts.map +1 -0
- package/dist/lib/messages/query.d.ts +7 -0
- package/dist/lib/messages/query.d.ts.map +1 -0
- package/dist/lib/messages/reasoning-strip.d.ts +8 -0
- package/dist/lib/messages/reasoning-strip.d.ts.map +1 -0
- package/dist/lib/messages/shape.d.ts +5 -0
- package/dist/lib/messages/shape.d.ts.map +1 -0
- package/dist/lib/messages/sync.d.ts +4 -0
- package/dist/lib/messages/sync.d.ts.map +1 -0
- package/dist/lib/messages/utils.d.ts +27 -0
- package/dist/lib/messages/utils.d.ts.map +1 -0
- package/dist/lib/prompts/compress-rc.d.ts +4 -0
- package/dist/lib/prompts/compress-rc.d.ts.map +1 -0
- package/dist/lib/prompts/context-limit-nudge.d.ts +2 -0
- package/dist/lib/prompts/context-limit-nudge.d.ts.map +1 -0
- package/dist/lib/prompts/extensions/nudge.d.ts +4 -0
- package/dist/lib/prompts/extensions/nudge.d.ts.map +1 -0
- package/dist/lib/prompts/extensions/system.d.ts +4 -0
- package/dist/lib/prompts/extensions/system.d.ts.map +1 -0
- package/dist/lib/prompts/extensions/tool.d.ts +3 -0
- package/dist/lib/prompts/extensions/tool.d.ts.map +1 -0
- package/dist/lib/prompts/index.d.ts +4 -0
- package/dist/lib/prompts/index.d.ts.map +1 -0
- package/dist/lib/prompts/iteration-nudge.d.ts +2 -0
- package/dist/lib/prompts/iteration-nudge.d.ts.map +1 -0
- package/dist/lib/prompts/store.d.ts +26 -0
- package/dist/lib/prompts/store.d.ts.map +1 -0
- package/dist/lib/prompts/system.d.ts +4 -0
- package/dist/lib/prompts/system.d.ts.map +1 -0
- package/dist/lib/prompts/turn-nudge.d.ts +2 -0
- package/dist/lib/prompts/turn-nudge.d.ts.map +1 -0
- package/dist/lib/protected-patterns.d.ts +6 -0
- package/dist/lib/protected-patterns.d.ts.map +1 -0
- package/dist/lib/state/index.d.ts +5 -0
- package/dist/lib/state/index.d.ts.map +1 -0
- package/dist/lib/state/persistence.d.ts +47 -0
- package/dist/lib/state/persistence.d.ts.map +1 -0
- package/dist/lib/state/state.d.ts +9 -0
- package/dist/lib/state/state.d.ts.map +1 -0
- package/dist/lib/state/tool-cache.d.ts +13 -0
- package/dist/lib/state/tool-cache.d.ts.map +1 -0
- package/dist/lib/state/types.d.ts +105 -0
- package/dist/lib/state/types.d.ts.map +1 -0
- package/dist/lib/state/utils.d.ts +22 -0
- package/dist/lib/state/utils.d.ts.map +1 -0
- package/dist/lib/strategies/deduplication.d.ts +10 -0
- package/dist/lib/strategies/deduplication.d.ts.map +1 -0
- package/dist/lib/strategies/index.d.ts +3 -0
- package/dist/lib/strategies/index.d.ts.map +1 -0
- package/dist/lib/strategies/purge-errors.d.ts +13 -0
- package/dist/lib/strategies/purge-errors.d.ts.map +1 -0
- package/dist/lib/subagents/subagent-results.d.ts +5 -0
- package/dist/lib/subagents/subagent-results.d.ts.map +1 -0
- package/dist/lib/token-utils.d.ts +19 -0
- package/dist/lib/token-utils.d.ts.map +1 -0
- package/dist/lib/tui/dialogs.d.ts +29 -0
- package/dist/lib/tui/dialogs.d.ts.map +1 -0
- package/dist/lib/tui/format.d.ts +4 -0
- package/dist/lib/tui/format.d.ts.map +1 -0
- package/dist/lib/tui/types.d.ts +18 -0
- package/dist/lib/tui/types.d.ts.map +1 -0
- package/dist/lib/tui/ui.d.ts +48 -0
- package/dist/lib/tui/ui.d.ts.map +1 -0
- package/dist/lib/ui/notification.d.ts +17 -0
- package/dist/lib/ui/notification.d.ts.map +1 -0
- package/dist/lib/ui/utils.d.ts +10 -0
- package/dist/lib/ui/utils.d.ts.map +1 -0
- package/dist/lib/v2/index.d.ts +5 -0
- package/dist/lib/v2/index.d.ts.map +1 -0
- package/dist/lib/v2/messages.d.ts +13 -0
- package/dist/lib/v2/messages.d.ts.map +1 -0
- package/dist/lib/v2/rpc.d.ts +75 -0
- package/dist/lib/v2/rpc.d.ts.map +1 -0
- package/dist/lib/v2/theme.d.ts +55 -0
- package/dist/lib/v2/theme.d.ts.map +1 -0
- package/dist/lib/v2/tui.d.ts +4 -0
- package/dist/lib/v2/tui.d.ts.map +1 -0
- package/dist/lib/v2/usage.d.ts +44 -0
- package/dist/lib/v2/usage.d.ts.map +1 -0
- package/dist/tui.d.ts +7 -0
- package/dist/tui.d.ts.map +1 -0
- package/lib/commands/compression-targets.ts +54 -0
- package/lib/commands/context.ts +305 -0
- package/lib/commands/decompress.ts +273 -0
- package/lib/commands/help.ts +76 -0
- package/lib/commands/index.ts +11 -0
- package/lib/commands/manual.ts +126 -0
- package/lib/commands/recompress.ts +222 -0
- package/lib/commands/stats.ts +180 -0
- package/lib/commands/sweep.ts +266 -0
- package/lib/compress/index.ts +2 -0
- package/lib/compress/pipeline.ts +116 -0
- package/lib/compress/protected-content.ts +163 -0
- package/lib/compress/rc.ts +494 -0
- package/lib/compress/search.ts +277 -0
- package/lib/compress/state.ts +272 -0
- package/lib/compress/summary.ts +320 -0
- package/lib/compress/timing.ts +77 -0
- package/lib/compress/types.ts +70 -0
- package/lib/compress/usage.ts +168 -0
- package/lib/compress-permission.ts +9 -0
- package/lib/config.ts +995 -0
- package/lib/hooks.ts +205 -0
- package/lib/logger.ts +226 -0
- package/lib/message-ids.ts +181 -0
- package/lib/messages/index.ts +7 -0
- package/lib/messages/inject/inject.ts +215 -0
- package/lib/messages/inject/subagent-results.ts +84 -0
- package/lib/messages/inject/utils.ts +304 -0
- package/lib/messages/prune.ts +238 -0
- package/lib/messages/query.ts +71 -0
- package/lib/messages/reasoning-strip.ts +40 -0
- package/lib/messages/shape.ts +50 -0
- package/lib/messages/sync.ts +124 -0
- package/lib/messages/utils.ts +190 -0
- package/lib/prompts/compress-rc.ts +25 -0
- package/lib/prompts/context-limit-nudge.ts +18 -0
- package/lib/prompts/extensions/nudge.ts +34 -0
- package/lib/prompts/extensions/system.ts +32 -0
- package/lib/prompts/extensions/tool.ts +21 -0
- package/lib/prompts/index.ts +29 -0
- package/lib/prompts/iteration-nudge.ts +6 -0
- package/lib/prompts/store.ts +452 -0
- package/lib/prompts/system.ts +41 -0
- package/lib/prompts/turn-nudge.ts +10 -0
- package/lib/protected-patterns.ts +159 -0
- package/lib/state/index.ts +4 -0
- package/lib/state/persistence.ts +321 -0
- package/lib/state/state.ts +214 -0
- package/lib/state/tool-cache.ts +99 -0
- package/lib/state/types.ts +117 -0
- package/lib/state/utils.ts +345 -0
- package/lib/strategies/deduplication.ts +125 -0
- package/lib/strategies/index.ts +2 -0
- package/lib/strategies/purge-errors.ts +86 -0
- package/lib/subagents/subagent-results.ts +75 -0
- package/lib/token-utils.ts +164 -0
- package/lib/tui/dialogs.tsx +261 -0
- package/lib/tui/format.ts +25 -0
- package/lib/tui/types.ts +25 -0
- package/lib/tui/ui.tsx +245 -0
- package/lib/ui/notification.ts +347 -0
- package/lib/ui/utils.ts +304 -0
- package/lib/v2/index.ts +448 -0
- package/lib/v2/messages.ts +252 -0
- package/lib/v2/rpc.ts +60 -0
- package/lib/v2/theme.ts +67 -0
- package/lib/v2/tui.tsx +115 -0
- package/lib/v2/usage.ts +423 -0
- package/package.json +91 -5
- package/server.js +1 -0
- package/tui.tsx +7 -0
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, statSync } from "fs"
|
|
2
|
+
import { join, dirname } from "path"
|
|
3
|
+
import { homedir } from "os"
|
|
4
|
+
import type { Logger } from "../logger"
|
|
5
|
+
import { systemPrompt } from "./system"
|
|
6
|
+
import { rcPrompt } from "./compress-rc"
|
|
7
|
+
import type { IdFormat } from "../message-ids"
|
|
8
|
+
import { CONTEXT_LIMIT_NUDGE } from "./context-limit-nudge"
|
|
9
|
+
import { TURN_NUDGE } from "./turn-nudge"
|
|
10
|
+
import { ITERATION_NUDGE } from "./iteration-nudge"
|
|
11
|
+
import { MANUAL_MODE_SYSTEM_EXTENSION, SUBAGENT_SYSTEM_EXTENSION } from "./extensions/system"
|
|
12
|
+
|
|
13
|
+
export type PromptKey =
|
|
14
|
+
| "system"
|
|
15
|
+
| "compress-rc"
|
|
16
|
+
| "context-limit-nudge"
|
|
17
|
+
| "turn-nudge"
|
|
18
|
+
| "iteration-nudge"
|
|
19
|
+
|
|
20
|
+
type EditablePromptField =
|
|
21
|
+
| "system"
|
|
22
|
+
| "compressRc"
|
|
23
|
+
| "contextLimitNudge"
|
|
24
|
+
| "turnNudge"
|
|
25
|
+
| "iterationNudge"
|
|
26
|
+
|
|
27
|
+
interface PromptDefinition {
|
|
28
|
+
key: PromptKey
|
|
29
|
+
fileName: string
|
|
30
|
+
label: string
|
|
31
|
+
description: string
|
|
32
|
+
usage: string
|
|
33
|
+
runtimeField: EditablePromptField
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface PromptOverrideCandidate {
|
|
37
|
+
path: string
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface PromptPaths {
|
|
41
|
+
defaultsDir: string
|
|
42
|
+
globalOverridesDir: string
|
|
43
|
+
configDirOverridesDir: string | null
|
|
44
|
+
projectOverridesDir: string | null
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface RuntimePrompts {
|
|
48
|
+
system: string
|
|
49
|
+
compressRc: string
|
|
50
|
+
contextLimitNudge: string
|
|
51
|
+
turnNudge: string
|
|
52
|
+
iterationNudge: string
|
|
53
|
+
manualExtension: string
|
|
54
|
+
subagentExtension: string
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const PROMPT_DEFINITIONS: PromptDefinition[] = [
|
|
58
|
+
{
|
|
59
|
+
key: "system",
|
|
60
|
+
fileName: "system.md",
|
|
61
|
+
label: "System",
|
|
62
|
+
description: "Core system-level DCP instruction block",
|
|
63
|
+
usage: "Injected into the model system prompt on every request",
|
|
64
|
+
runtimeField: "system",
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
key: "compress-rc",
|
|
68
|
+
fileName: "compress-rc.md",
|
|
69
|
+
label: "Compress RC",
|
|
70
|
+
description: "rc-mode compress tool instructions (model selects IDs only)",
|
|
71
|
+
usage: "Registered as the rc-mode compress tool description",
|
|
72
|
+
runtimeField: "compressRc",
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
key: "context-limit-nudge",
|
|
76
|
+
fileName: "context-limit-nudge.md",
|
|
77
|
+
label: "Context Limit Nudge",
|
|
78
|
+
description: "High-priority nudge when context is over max threshold",
|
|
79
|
+
usage: "Injected when context usage is beyond configured max limits",
|
|
80
|
+
runtimeField: "contextLimitNudge",
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
key: "turn-nudge",
|
|
84
|
+
fileName: "turn-nudge.md",
|
|
85
|
+
label: "Turn Nudge",
|
|
86
|
+
description: "Nudge to compress closed ranges at turn boundaries",
|
|
87
|
+
usage: "Injected when context is between min and max limits at a new user turn",
|
|
88
|
+
runtimeField: "turnNudge",
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
key: "iteration-nudge",
|
|
92
|
+
fileName: "iteration-nudge.md",
|
|
93
|
+
label: "Iteration Nudge",
|
|
94
|
+
description: "Nudge after many iterations without user input",
|
|
95
|
+
usage: "Injected when iteration threshold is crossed",
|
|
96
|
+
runtimeField: "iterationNudge",
|
|
97
|
+
},
|
|
98
|
+
]
|
|
99
|
+
|
|
100
|
+
export const PROMPT_KEYS: PromptKey[] = [
|
|
101
|
+
"system",
|
|
102
|
+
"compress-rc",
|
|
103
|
+
"context-limit-nudge",
|
|
104
|
+
"turn-nudge",
|
|
105
|
+
"iteration-nudge",
|
|
106
|
+
]
|
|
107
|
+
|
|
108
|
+
const HTML_COMMENT_REGEX = /<!--[\s\S]*?-->/g
|
|
109
|
+
const LEGACY_INLINE_COMMENT_LINE_REGEX = /^[ \t]*\/\/.*?\/\/[ \t]*$/gm
|
|
110
|
+
const DCP_SYSTEM_REMINDER_TAG_REGEX =
|
|
111
|
+
/^\s*<dcp-system-reminder\b[^>]*>[\s\S]*<\/dcp-system-reminder>\s*$/i
|
|
112
|
+
const DEFAULTS_README_FILE = "README.md"
|
|
113
|
+
|
|
114
|
+
function bundledPrompts(format: IdFormat): Record<EditablePromptField, string> {
|
|
115
|
+
return {
|
|
116
|
+
system: systemPrompt(format),
|
|
117
|
+
compressRc: rcPrompt(format),
|
|
118
|
+
contextLimitNudge: CONTEXT_LIMIT_NUDGE,
|
|
119
|
+
turnNudge: TURN_NUDGE,
|
|
120
|
+
iterationNudge: ITERATION_NUDGE,
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const INTERNAL_PROMPT_EXTENSIONS = {
|
|
125
|
+
manualExtension: MANUAL_MODE_SYSTEM_EXTENSION,
|
|
126
|
+
subagentExtension: SUBAGENT_SYSTEM_EXTENSION,
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function createBundledRuntimePrompts(bundled: Record<EditablePromptField, string>): RuntimePrompts {
|
|
130
|
+
return {
|
|
131
|
+
...bundled,
|
|
132
|
+
manualExtension: INTERNAL_PROMPT_EXTENSIONS.manualExtension,
|
|
133
|
+
subagentExtension: INTERNAL_PROMPT_EXTENSIONS.subagentExtension,
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function findOpencodeDir(startDir: string): string | null {
|
|
138
|
+
let current = startDir
|
|
139
|
+
while (current !== "/") {
|
|
140
|
+
const candidate = join(current, ".opencode")
|
|
141
|
+
if (existsSync(candidate)) {
|
|
142
|
+
try {
|
|
143
|
+
if (statSync(candidate).isDirectory()) {
|
|
144
|
+
return candidate
|
|
145
|
+
}
|
|
146
|
+
} catch {
|
|
147
|
+
// ignore inaccessible entries while walking upward
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
const parent = dirname(current)
|
|
151
|
+
if (parent === current) {
|
|
152
|
+
break
|
|
153
|
+
}
|
|
154
|
+
current = parent
|
|
155
|
+
}
|
|
156
|
+
return null
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function resolvePromptPaths(workingDirectory: string): PromptPaths {
|
|
160
|
+
const configHome = process.env.XDG_CONFIG_HOME || join(homedir(), ".config")
|
|
161
|
+
const globalRoot = join(configHome, "opencode", "dcp-prompts")
|
|
162
|
+
const defaultsDir = join(globalRoot, "defaults")
|
|
163
|
+
const globalOverridesDir = join(globalRoot, "overrides")
|
|
164
|
+
|
|
165
|
+
const configDirOverridesDir = process.env.OPENCODE_CONFIG_DIR
|
|
166
|
+
? join(process.env.OPENCODE_CONFIG_DIR, "dcp-prompts", "overrides")
|
|
167
|
+
: null
|
|
168
|
+
|
|
169
|
+
const opencodeDir = findOpencodeDir(workingDirectory)
|
|
170
|
+
const projectOverridesDir = opencodeDir ? join(opencodeDir, "dcp-prompts", "overrides") : null
|
|
171
|
+
|
|
172
|
+
return {
|
|
173
|
+
defaultsDir,
|
|
174
|
+
globalOverridesDir,
|
|
175
|
+
configDirOverridesDir,
|
|
176
|
+
projectOverridesDir,
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function stripConditionalTag(content: string, tagName: string): string {
|
|
181
|
+
const regex = new RegExp(`<${tagName}>[\\s\\S]*?<\/${tagName}>`, "gi")
|
|
182
|
+
return content.replace(regex, "")
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function unwrapDcpTagIfWrapped(content: string): string {
|
|
186
|
+
const trimmed = content.trim()
|
|
187
|
+
|
|
188
|
+
if (DCP_SYSTEM_REMINDER_TAG_REGEX.test(trimmed)) {
|
|
189
|
+
return trimmed
|
|
190
|
+
.replace(/^\s*<dcp-system-reminder\b[^>]*>\s*/i, "")
|
|
191
|
+
.replace(/\s*<\/dcp-system-reminder>\s*$/i, "")
|
|
192
|
+
.trim()
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return trimmed
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function normalizeReminderPromptContent(content: string): string {
|
|
199
|
+
const normalized = content.trim()
|
|
200
|
+
|
|
201
|
+
if (!normalized) {
|
|
202
|
+
return ""
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const startsWrapped = /^\s*<dcp-system-reminder\b[^>]*>/i.test(normalized)
|
|
206
|
+
const endsWrapped = /<\/dcp-system-reminder>\s*$/i.test(normalized)
|
|
207
|
+
|
|
208
|
+
if (startsWrapped !== endsWrapped) {
|
|
209
|
+
return ""
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return unwrapDcpTagIfWrapped(normalized)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function stripPromptComments(content: string): string {
|
|
216
|
+
return content
|
|
217
|
+
.replace(/^\uFEFF/, "")
|
|
218
|
+
.replace(/\r\n?/g, "\n")
|
|
219
|
+
.replace(HTML_COMMENT_REGEX, "")
|
|
220
|
+
.replace(LEGACY_INLINE_COMMENT_LINE_REGEX, "")
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function toEditablePromptText(definition: PromptDefinition, rawContent: string): string {
|
|
224
|
+
let normalized = stripPromptComments(rawContent).trim()
|
|
225
|
+
if (!normalized) {
|
|
226
|
+
return ""
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (definition.key === "system") {
|
|
230
|
+
normalized = stripConditionalTag(normalized, "manual")
|
|
231
|
+
normalized = stripConditionalTag(normalized, "subagent")
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
normalized = normalizeReminderPromptContent(normalized)
|
|
235
|
+
|
|
236
|
+
return normalized.trim()
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function wrapRuntimePromptContent(definition: PromptDefinition, editableText: string): string {
|
|
240
|
+
const trimmed = editableText.trim()
|
|
241
|
+
if (!trimmed) {
|
|
242
|
+
return ""
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return `<dcp-system-reminder>\n${trimmed}\n</dcp-system-reminder>`
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function buildDefaultPromptFileContent(bundledEditableText: string): string {
|
|
249
|
+
return `${bundledEditableText.trim()}\n`
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function buildDefaultsReadmeContent(): string {
|
|
253
|
+
const lines: string[] = []
|
|
254
|
+
lines.push("# DCP Prompt Defaults")
|
|
255
|
+
lines.push("")
|
|
256
|
+
lines.push("This directory stores the DCP prompts.")
|
|
257
|
+
lines.push("Each prompt file here should contain plain text only (no XML wrappers).")
|
|
258
|
+
lines.push("")
|
|
259
|
+
lines.push("## Creating Overrides")
|
|
260
|
+
lines.push("")
|
|
261
|
+
lines.push(
|
|
262
|
+
"1. Copy a prompt file from this directory into an overrides directory using the same filename.",
|
|
263
|
+
)
|
|
264
|
+
lines.push("2. Edit the copied file using plain text.")
|
|
265
|
+
lines.push("3. Restart OpenCode.")
|
|
266
|
+
lines.push("")
|
|
267
|
+
lines.push("To reset an override, delete the matching file from your overrides directory.")
|
|
268
|
+
lines.push("")
|
|
269
|
+
lines.push(
|
|
270
|
+
"Do not edit the default prompt files directly, they are just for reference, only files in the overrides directory are used.",
|
|
271
|
+
)
|
|
272
|
+
lines.push("")
|
|
273
|
+
lines.push("Override precedence (highest first):")
|
|
274
|
+
lines.push("1. `.opencode/dcp-prompts/overrides/` (project)")
|
|
275
|
+
lines.push("2. `$OPENCODE_CONFIG_DIR/dcp-prompts/overrides/` (config dir)")
|
|
276
|
+
lines.push("3. `~/.config/opencode/dcp-prompts/overrides/` (global)")
|
|
277
|
+
lines.push("")
|
|
278
|
+
lines.push("## Prompt Files")
|
|
279
|
+
lines.push("")
|
|
280
|
+
|
|
281
|
+
for (const definition of PROMPT_DEFINITIONS) {
|
|
282
|
+
lines.push(`- \`${definition.fileName}\``)
|
|
283
|
+
lines.push(` - Purpose: ${definition.description}.`)
|
|
284
|
+
lines.push(` - Runtime use: ${definition.usage}.`)
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return `${lines.join("\n")}\n`
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function readFileIfExists(filePath: string): string | null {
|
|
291
|
+
if (!existsSync(filePath)) {
|
|
292
|
+
return null
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
try {
|
|
296
|
+
return readFileSync(filePath, "utf-8")
|
|
297
|
+
} catch {
|
|
298
|
+
return null
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
export class PromptStore {
|
|
303
|
+
private readonly logger: Logger
|
|
304
|
+
private readonly paths: PromptPaths
|
|
305
|
+
private readonly customPromptsEnabled: boolean
|
|
306
|
+
private runtimePrompts: RuntimePrompts
|
|
307
|
+
private readonly bundled: Record<EditablePromptField, string>
|
|
308
|
+
|
|
309
|
+
constructor(
|
|
310
|
+
logger: Logger,
|
|
311
|
+
workingDirectory: string,
|
|
312
|
+
customPromptsEnabled = false,
|
|
313
|
+
idFormat: IdFormat = "xml",
|
|
314
|
+
) {
|
|
315
|
+
this.logger = logger
|
|
316
|
+
this.paths = resolvePromptPaths(workingDirectory)
|
|
317
|
+
this.customPromptsEnabled = customPromptsEnabled
|
|
318
|
+
this.bundled = bundledPrompts(idFormat)
|
|
319
|
+
this.runtimePrompts = createBundledRuntimePrompts(this.bundled)
|
|
320
|
+
|
|
321
|
+
if (this.customPromptsEnabled) {
|
|
322
|
+
this.ensureDefaultFiles()
|
|
323
|
+
}
|
|
324
|
+
this.reload()
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
getRuntimePrompts(): RuntimePrompts {
|
|
328
|
+
return { ...this.runtimePrompts }
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
reload(): void {
|
|
332
|
+
const nextPrompts = createBundledRuntimePrompts(this.bundled)
|
|
333
|
+
|
|
334
|
+
if (!this.customPromptsEnabled) {
|
|
335
|
+
this.runtimePrompts = nextPrompts
|
|
336
|
+
return
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
for (const definition of PROMPT_DEFINITIONS) {
|
|
340
|
+
const bundledSource = this.bundled[definition.runtimeField]
|
|
341
|
+
const bundledEditable = toEditablePromptText(definition, bundledSource)
|
|
342
|
+
const bundledRuntime = wrapRuntimePromptContent(definition, bundledEditable)
|
|
343
|
+
const fallbackValue = bundledRuntime || bundledSource.trim()
|
|
344
|
+
let effectiveValue = fallbackValue
|
|
345
|
+
|
|
346
|
+
for (const candidate of this.getOverrideCandidates(definition.fileName)) {
|
|
347
|
+
const rawOverride = readFileIfExists(candidate.path)
|
|
348
|
+
if (rawOverride === null) {
|
|
349
|
+
continue
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const editableOverride = toEditablePromptText(definition, rawOverride)
|
|
353
|
+
if (!editableOverride) {
|
|
354
|
+
this.logger.warn("Prompt override is empty or invalid after normalization", {
|
|
355
|
+
key: definition.key,
|
|
356
|
+
path: candidate.path,
|
|
357
|
+
})
|
|
358
|
+
continue
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const wrappedOverride = wrapRuntimePromptContent(definition, editableOverride)
|
|
362
|
+
if (!wrappedOverride) {
|
|
363
|
+
this.logger.warn("Prompt override could not be wrapped for runtime", {
|
|
364
|
+
key: definition.key,
|
|
365
|
+
path: candidate.path,
|
|
366
|
+
})
|
|
367
|
+
continue
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
effectiveValue = wrappedOverride
|
|
371
|
+
break
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
nextPrompts[definition.runtimeField] = effectiveValue
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
this.runtimePrompts = nextPrompts
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
private getOverrideCandidates(fileName: string): PromptOverrideCandidate[] {
|
|
381
|
+
const candidates: PromptOverrideCandidate[] = []
|
|
382
|
+
|
|
383
|
+
if (this.paths.projectOverridesDir) {
|
|
384
|
+
candidates.push({
|
|
385
|
+
path: join(this.paths.projectOverridesDir, fileName),
|
|
386
|
+
})
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
if (this.paths.configDirOverridesDir) {
|
|
390
|
+
candidates.push({
|
|
391
|
+
path: join(this.paths.configDirOverridesDir, fileName),
|
|
392
|
+
})
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
candidates.push({
|
|
396
|
+
path: join(this.paths.globalOverridesDir, fileName),
|
|
397
|
+
})
|
|
398
|
+
|
|
399
|
+
return candidates
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
private ensureDefaultFiles(): void {
|
|
403
|
+
try {
|
|
404
|
+
mkdirSync(this.paths.defaultsDir, { recursive: true })
|
|
405
|
+
mkdirSync(this.paths.globalOverridesDir, { recursive: true })
|
|
406
|
+
} catch {
|
|
407
|
+
this.logger.warn("Failed to initialize prompt directories", {
|
|
408
|
+
defaultsDir: this.paths.defaultsDir,
|
|
409
|
+
globalOverridesDir: this.paths.globalOverridesDir,
|
|
410
|
+
})
|
|
411
|
+
return
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
for (const definition of PROMPT_DEFINITIONS) {
|
|
415
|
+
const bundledEditable = toEditablePromptText(
|
|
416
|
+
definition,
|
|
417
|
+
this.bundled[definition.runtimeField],
|
|
418
|
+
)
|
|
419
|
+
const managedContent = buildDefaultPromptFileContent(
|
|
420
|
+
bundledEditable || this.bundled[definition.runtimeField],
|
|
421
|
+
)
|
|
422
|
+
const filePath = join(this.paths.defaultsDir, definition.fileName)
|
|
423
|
+
|
|
424
|
+
try {
|
|
425
|
+
const existing = readFileIfExists(filePath)
|
|
426
|
+
if (existing === managedContent) {
|
|
427
|
+
continue
|
|
428
|
+
}
|
|
429
|
+
writeFileSync(filePath, managedContent, "utf-8")
|
|
430
|
+
} catch {
|
|
431
|
+
this.logger.warn("Failed to write default prompt file", {
|
|
432
|
+
key: definition.key,
|
|
433
|
+
path: filePath,
|
|
434
|
+
})
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
const readmePath = join(this.paths.defaultsDir, DEFAULTS_README_FILE)
|
|
439
|
+
const readmeContent = buildDefaultsReadmeContent()
|
|
440
|
+
|
|
441
|
+
try {
|
|
442
|
+
const existing = readFileIfExists(readmePath)
|
|
443
|
+
if (existing !== readmeContent) {
|
|
444
|
+
writeFileSync(readmePath, readmeContent, "utf-8")
|
|
445
|
+
}
|
|
446
|
+
} catch {
|
|
447
|
+
this.logger.warn("Failed to write defaults README", {
|
|
448
|
+
path: readmePath,
|
|
449
|
+
})
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { IdFormat } from "../message-ids"
|
|
2
|
+
|
|
3
|
+
export function systemPrompt(format: IdFormat = "xml"): string {
|
|
4
|
+
const markers =
|
|
5
|
+
format === "compact" ? "`@4@`, `@b1@`, `@blocked@`, priority labels" : "`<dcp-message-id>`"
|
|
6
|
+
return `
|
|
7
|
+
You operate in a context-constrained environment. Manage context continuously to avoid buildup and preserve retrieval quality. Efficient context management is paramount for your agentic performance.
|
|
8
|
+
|
|
9
|
+
The ONLY tool you have for context management is \`compress\`. It replaces older conversation content with compact technical summaries.
|
|
10
|
+
|
|
11
|
+
${markers} and \`<dcp-system-reminder>\` tags are environment-injected metadata. Do not output them.
|
|
12
|
+
|
|
13
|
+
THE PHILOSOPHY OF COMPRESS
|
|
14
|
+
\`compress\` transforms conversation content into dense, high-fidelity summaries. This is not cleanup - it is crystallization. Your summary becomes the authoritative record of what transpired.
|
|
15
|
+
|
|
16
|
+
Think of compression as phase transitions: raw exploration becomes refined understanding. The original context served its purpose; your summary now carries that understanding forward.
|
|
17
|
+
|
|
18
|
+
COMPRESS WHEN
|
|
19
|
+
|
|
20
|
+
A section is genuinely closed and the raw conversation has served its purpose:
|
|
21
|
+
|
|
22
|
+
- Research concluded and findings are clear
|
|
23
|
+
- Implementation finished and verified
|
|
24
|
+
- Exploration exhausted and patterns understood
|
|
25
|
+
- Dead-end noise can be discarded without waiting for a whole chapter to close
|
|
26
|
+
|
|
27
|
+
DO NOT COMPRESS IF
|
|
28
|
+
|
|
29
|
+
- Raw context is still relevant and needed for edits or precise references
|
|
30
|
+
- The target content is still actively in progress
|
|
31
|
+
- You may need exact code, error messages, or file contents in the immediate next steps
|
|
32
|
+
|
|
33
|
+
Before compressing, ask: _"Is this section closed enough to become summary-only right now?"_
|
|
34
|
+
|
|
35
|
+
Evaluate conversation signal-to-noise REGULARLY. Use \`compress\` deliberately with quality-first summaries. Prioritize stale content intelligently to maintain a high-signal context window that supports your agency.
|
|
36
|
+
|
|
37
|
+
It is of your responsibility to keep a sharp, high-quality context window for optimal performance.
|
|
38
|
+
`
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export const SYSTEM = systemPrompt()
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export const TURN_NUDGE = `<dcp-system-reminder>
|
|
2
|
+
Evaluate the conversation for compressible ranges.
|
|
3
|
+
|
|
4
|
+
If any messages are cleanly closed and unlikely to be needed again, use the compress tool on them.
|
|
5
|
+
If direction has shifted, compress earlier ranges that are now less relevant.
|
|
6
|
+
|
|
7
|
+
The goal is to filter noise and distill key information so context accumulation stays under control.
|
|
8
|
+
Keep active context uncompressed.
|
|
9
|
+
</dcp-system-reminder>
|
|
10
|
+
`
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
function normalizePath(input: string): string {
|
|
2
|
+
// A single backslash. In source, "\\" is the one-character string; the
|
|
3
|
+
// previous "\\\\" was a *two*-character string, so it only ever matched a
|
|
4
|
+
// doubled separator -- which a real Windows path does not contain. The
|
|
5
|
+
// normalisation was therefore a no-op on the only platform that needs it.
|
|
6
|
+
return input.replaceAll("\\", "/")
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function escapeRegExpChar(ch: string): string {
|
|
10
|
+
return /[\\.^$+{}()|\[\]]/.test(ch) ? `\\${ch}` : ch
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function matchesGlob(inputPath: string, pattern: string): boolean {
|
|
14
|
+
if (!pattern) return false
|
|
15
|
+
|
|
16
|
+
const input = normalizePath(inputPath)
|
|
17
|
+
const pat = normalizePath(pattern)
|
|
18
|
+
|
|
19
|
+
let regex = "^"
|
|
20
|
+
|
|
21
|
+
for (let i = 0; i < pat.length; i++) {
|
|
22
|
+
const ch = pat[i]
|
|
23
|
+
|
|
24
|
+
if (ch === "*") {
|
|
25
|
+
const next = pat[i + 1]
|
|
26
|
+
if (next === "*") {
|
|
27
|
+
const after = pat[i + 2]
|
|
28
|
+
if (after === "/") {
|
|
29
|
+
// **/ (zero or more directories)
|
|
30
|
+
regex += "(?:.*/)?"
|
|
31
|
+
i += 2
|
|
32
|
+
continue
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// **
|
|
36
|
+
regex += ".*"
|
|
37
|
+
i++
|
|
38
|
+
continue
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// *
|
|
42
|
+
regex += "[^/]*"
|
|
43
|
+
continue
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (ch === "?") {
|
|
47
|
+
regex += "[^/]"
|
|
48
|
+
continue
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (ch === "/") {
|
|
52
|
+
regex += "/"
|
|
53
|
+
continue
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
regex += escapeRegExpChar(ch)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
regex += "$"
|
|
60
|
+
|
|
61
|
+
return new RegExp(regex).test(input)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function getFilePathsFromParameters(tool: string, parameters: unknown): string[] {
|
|
65
|
+
if (typeof parameters !== "object" || parameters === null) {
|
|
66
|
+
return []
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const paths: string[] = []
|
|
70
|
+
const params = parameters as Record<string, any>
|
|
71
|
+
|
|
72
|
+
// 1. apply_patch uses patchText with embedded paths
|
|
73
|
+
if ((tool === "apply_patch" || tool === "patch") && typeof params.patchText === "string") {
|
|
74
|
+
const pathRegex = /\*\*\* (?:Add|Delete|Update) File: ([^\n\r]+)/g
|
|
75
|
+
let match
|
|
76
|
+
while ((match = pathRegex.exec(params.patchText)) !== null) {
|
|
77
|
+
paths.push(match[1].trim())
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// 2. multiedit uses top-level filePath and nested edits array
|
|
82
|
+
if (tool === "multiedit") {
|
|
83
|
+
if (typeof params.filePath === "string") {
|
|
84
|
+
paths.push(params.filePath)
|
|
85
|
+
}
|
|
86
|
+
if (Array.isArray(params.edits)) {
|
|
87
|
+
for (const edit of params.edits) {
|
|
88
|
+
if (edit && typeof edit.filePath === "string") {
|
|
89
|
+
paths.push(edit.filePath)
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// 3. Default check for common filePath parameter (read, write, edit, etc)
|
|
96
|
+
if (typeof params.filePath === "string") {
|
|
97
|
+
paths.push(params.filePath)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// V2 file tools use path rather than filePath.
|
|
101
|
+
if (["read", "write", "edit"].includes(tool) && typeof params.path === "string") {
|
|
102
|
+
paths.push(params.path)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Return unique non-empty paths
|
|
106
|
+
return [...new Set(paths)].filter((p) => p.length > 0)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function isFilePathProtected(filePaths: string[], patterns: string[]): boolean {
|
|
110
|
+
if (!filePaths || filePaths.length === 0) return false
|
|
111
|
+
if (!patterns || patterns.length === 0) return false
|
|
112
|
+
|
|
113
|
+
return filePaths.some((path) => patterns.some((pattern) => matchesGlob(path, pattern)))
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const GLOB_CHARS = /[*?]/
|
|
117
|
+
|
|
118
|
+
export function isToolNameProtected(toolName: string, patterns: string[]): boolean {
|
|
119
|
+
if (!toolName || !patterns || patterns.length === 0) return false
|
|
120
|
+
|
|
121
|
+
const exactPatterns: Set<string> = new Set()
|
|
122
|
+
const globPatterns: string[] = []
|
|
123
|
+
|
|
124
|
+
for (const pattern of patterns) {
|
|
125
|
+
if (GLOB_CHARS.test(pattern)) {
|
|
126
|
+
globPatterns.push(pattern)
|
|
127
|
+
} else {
|
|
128
|
+
exactPatterns.add(pattern)
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (exactPatterns.has(toolName)) {
|
|
133
|
+
return true
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return globPatterns.some((pattern) => matchesGlob(toolName, pattern))
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function isToolProtected(
|
|
140
|
+
tool: string,
|
|
141
|
+
input: unknown,
|
|
142
|
+
tools: string[],
|
|
143
|
+
files: string[],
|
|
144
|
+
metadata?: Record<string, unknown>,
|
|
145
|
+
): boolean {
|
|
146
|
+
const calls = [{ tool, input }]
|
|
147
|
+
// V2 Code Mode keeps nested inputs, but only one combined output. Protect
|
|
148
|
+
// that output as a whole when any nested call matches the user's rules.
|
|
149
|
+
if (tool === "execute" && Array.isArray(metadata?.toolCalls)) {
|
|
150
|
+
for (const call of metadata.toolCalls) {
|
|
151
|
+
if (call && typeof call.tool === "string") calls.push(call)
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return calls.some(
|
|
155
|
+
(call) =>
|
|
156
|
+
isToolNameProtected(call.tool, tools) ||
|
|
157
|
+
isFilePathProtected(getFilePathsFromParameters(call.tool, call.input), files),
|
|
158
|
+
)
|
|
159
|
+
}
|