@tarquinen/opencode-dcp 3.1.12 → 3.1.14

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 (106) hide show
  1. package/README.md +14 -9
  2. package/dist/index.js +105 -39
  3. package/dist/index.js.map +1 -1
  4. package/dist/lib/commands/context.d.ts +15 -0
  5. package/dist/lib/commands/context.d.ts.map +1 -1
  6. package/dist/lib/commands/help.d.ts +1 -0
  7. package/dist/lib/commands/help.d.ts.map +1 -1
  8. package/dist/lib/commands/manual.d.ts +1 -1
  9. package/dist/lib/commands/manual.d.ts.map +1 -1
  10. package/dist/lib/commands/stats.d.ts +10 -0
  11. package/dist/lib/commands/stats.d.ts.map +1 -1
  12. package/dist/lib/compress/pipeline.d.ts.map +1 -1
  13. package/dist/lib/hooks.d.ts.map +1 -1
  14. package/dist/lib/state/persistence.d.ts +3 -0
  15. package/dist/lib/state/persistence.d.ts.map +1 -1
  16. package/dist/lib/state/state.d.ts +1 -0
  17. package/dist/lib/state/state.d.ts.map +1 -1
  18. package/dist/lib/tui/commands.d.ts +3 -0
  19. package/dist/lib/tui/commands.d.ts.map +1 -0
  20. package/dist/lib/tui/data.d.ts +14 -0
  21. package/dist/lib/tui/data.d.ts.map +1 -0
  22. package/dist/lib/tui/dialogs.d.ts +30 -0
  23. package/dist/lib/tui/dialogs.d.ts.map +1 -0
  24. package/dist/lib/tui/format.d.ts +4 -0
  25. package/dist/lib/tui/format.d.ts.map +1 -0
  26. package/dist/lib/tui/modals.d.ts +10 -0
  27. package/dist/lib/tui/modals.d.ts.map +1 -0
  28. package/dist/lib/tui/types.d.ts +15 -0
  29. package/dist/lib/tui/types.d.ts.map +1 -0
  30. package/dist/lib/tui/ui.d.ts +48 -0
  31. package/dist/lib/tui/ui.d.ts.map +1 -0
  32. package/dist/tui.d.ts +7 -0
  33. package/dist/tui.d.ts.map +1 -0
  34. package/lib/auth.ts +37 -0
  35. package/lib/commands/compression-targets.ts +137 -0
  36. package/lib/commands/context.ts +305 -0
  37. package/lib/commands/decompress.ts +275 -0
  38. package/lib/commands/help.ts +76 -0
  39. package/lib/commands/index.ts +11 -0
  40. package/lib/commands/manual.ts +127 -0
  41. package/lib/commands/recompress.ts +224 -0
  42. package/lib/commands/stats.ts +159 -0
  43. package/lib/commands/sweep.ts +268 -0
  44. package/lib/compress/index.ts +3 -0
  45. package/lib/compress/message-utils.ts +250 -0
  46. package/lib/compress/message.ts +145 -0
  47. package/lib/compress/pipeline.ts +108 -0
  48. package/lib/compress/protected-content.ts +208 -0
  49. package/lib/compress/range-utils.ts +308 -0
  50. package/lib/compress/range.ts +192 -0
  51. package/lib/compress/search.ts +267 -0
  52. package/lib/compress/state.ts +268 -0
  53. package/lib/compress/timing.ts +77 -0
  54. package/lib/compress/types.ts +108 -0
  55. package/lib/compress-permission.ts +25 -0
  56. package/lib/config.ts +1007 -0
  57. package/lib/hooks.ts +375 -0
  58. package/lib/host-permissions.ts +101 -0
  59. package/lib/logger.ts +226 -0
  60. package/lib/message-ids.ts +172 -0
  61. package/lib/messages/index.ts +8 -0
  62. package/lib/messages/inject/inject.ts +215 -0
  63. package/lib/messages/inject/subagent-results.ts +82 -0
  64. package/lib/messages/inject/utils.ts +374 -0
  65. package/lib/messages/priority.ts +102 -0
  66. package/lib/messages/prune.ts +233 -0
  67. package/lib/messages/query.ts +72 -0
  68. package/lib/messages/reasoning-strip.ts +40 -0
  69. package/lib/messages/shape.ts +50 -0
  70. package/lib/messages/sync.ts +124 -0
  71. package/lib/messages/utils.ts +185 -0
  72. package/lib/prompts/compress-message.ts +43 -0
  73. package/lib/prompts/compress-range.ts +60 -0
  74. package/lib/prompts/context-limit-nudge.ts +18 -0
  75. package/lib/prompts/extensions/nudge.ts +43 -0
  76. package/lib/prompts/extensions/system.ts +32 -0
  77. package/lib/prompts/extensions/tool.ts +35 -0
  78. package/lib/prompts/index.ts +29 -0
  79. package/lib/prompts/iteration-nudge.ts +6 -0
  80. package/lib/prompts/store.ts +467 -0
  81. package/lib/prompts/system.ts +33 -0
  82. package/lib/prompts/turn-nudge.ts +10 -0
  83. package/lib/protected-patterns.ts +128 -0
  84. package/lib/state/index.ts +4 -0
  85. package/lib/state/persistence.ts +305 -0
  86. package/lib/state/state.ts +208 -0
  87. package/lib/state/tool-cache.ts +98 -0
  88. package/lib/state/types.ts +111 -0
  89. package/lib/state/utils.ts +345 -0
  90. package/lib/strategies/deduplication.ts +127 -0
  91. package/lib/strategies/index.ts +2 -0
  92. package/lib/strategies/purge-errors.ts +88 -0
  93. package/lib/subagents/subagent-results.ts +74 -0
  94. package/lib/token-utils.ts +164 -0
  95. package/lib/tui/commands.ts +31 -0
  96. package/lib/tui/data.ts +73 -0
  97. package/lib/tui/dialogs.tsx +235 -0
  98. package/lib/tui/format.ts +25 -0
  99. package/lib/tui/modals.tsx +92 -0
  100. package/lib/tui/types.ts +16 -0
  101. package/lib/tui/ui.tsx +219 -0
  102. package/lib/ui/notification.ts +347 -0
  103. package/lib/ui/utils.ts +304 -0
  104. package/lib/update.ts +185 -0
  105. package/package.json +21 -5
  106. package/tui.tsx +26 -0
@@ -0,0 +1,304 @@
1
+ import { SessionState, ToolParameterEntry, WithParts } from "../state"
2
+ import { countTokens } from "../token-utils"
3
+ import { isIgnoredUserMessage } from "../messages/query"
4
+
5
+ function extractParameterKey(tool: string, parameters: any): string {
6
+ if (!parameters) return ""
7
+
8
+ if (tool === "read" && parameters.filePath) {
9
+ const offset = parameters.offset
10
+ const limit = parameters.limit
11
+ if (offset !== undefined && limit !== undefined) {
12
+ return `${parameters.filePath} (lines ${offset}-${offset + limit})`
13
+ }
14
+ if (offset !== undefined) {
15
+ return `${parameters.filePath} (lines ${offset}+)`
16
+ }
17
+ if (limit !== undefined) {
18
+ return `${parameters.filePath} (lines 0-${limit})`
19
+ }
20
+ return parameters.filePath
21
+ }
22
+
23
+ if ((tool === "write" || tool === "edit" || tool === "multiedit") && parameters.filePath) {
24
+ return parameters.filePath
25
+ }
26
+
27
+ if (tool === "apply_patch" && typeof parameters.patchText === "string") {
28
+ const pathRegex = /\*\*\* (?:Add|Delete|Update) File: ([^\n\r]+)/g
29
+ const paths: string[] = []
30
+ let match
31
+ while ((match = pathRegex.exec(parameters.patchText)) !== null) {
32
+ paths.push(match[1].trim())
33
+ }
34
+ if (paths.length > 0) {
35
+ const uniquePaths = [...new Set(paths)]
36
+ const count = uniquePaths.length
37
+ const plural = count > 1 ? "s" : ""
38
+ if (count === 1) return uniquePaths[0]
39
+ if (count === 2) return uniquePaths.join(", ")
40
+ return `${count} file${plural}: ${uniquePaths[0]}, ${uniquePaths[1]}...`
41
+ }
42
+ return "patch"
43
+ }
44
+
45
+ if (tool === "list") {
46
+ return parameters.path || "(current directory)"
47
+ }
48
+
49
+ if (tool === "glob") {
50
+ if (parameters.pattern) {
51
+ const pathInfo = parameters.path ? ` in ${parameters.path}` : ""
52
+ return `"${parameters.pattern}"${pathInfo}`
53
+ }
54
+ return "(unknown pattern)"
55
+ }
56
+
57
+ if (tool === "grep") {
58
+ if (parameters.pattern) {
59
+ const pathInfo = parameters.path ? ` in ${parameters.path}` : ""
60
+ return `"${parameters.pattern}"${pathInfo}`
61
+ }
62
+ return "(unknown pattern)"
63
+ }
64
+
65
+ if (tool === "bash") {
66
+ if (parameters.description) return parameters.description
67
+ if (parameters.command) {
68
+ return parameters.command.length > 50
69
+ ? parameters.command.substring(0, 50) + "..."
70
+ : parameters.command
71
+ }
72
+ }
73
+
74
+ if (tool === "webfetch" && parameters.url) {
75
+ return parameters.url
76
+ }
77
+ if (tool === "websearch" && parameters.query) {
78
+ return `"${parameters.query}"`
79
+ }
80
+ if (tool === "codesearch" && parameters.query) {
81
+ return `"${parameters.query}"`
82
+ }
83
+
84
+ if (tool === "todowrite") {
85
+ return `${parameters.todos?.length || 0} todos`
86
+ }
87
+ if (tool === "todoread") {
88
+ return "read todo list"
89
+ }
90
+
91
+ if (tool === "task" && parameters.description) {
92
+ return parameters.description
93
+ }
94
+ if (tool === "skill" && parameters.name) {
95
+ return parameters.name
96
+ }
97
+
98
+ if (tool === "lsp") {
99
+ const op = parameters.operation || "lsp"
100
+ const path = parameters.filePath || ""
101
+ const line = parameters.line
102
+ const char = parameters.character
103
+ if (path && line !== undefined && char !== undefined) {
104
+ return `${op} ${path}:${line}:${char}`
105
+ }
106
+ if (path) {
107
+ return `${op} ${path}`
108
+ }
109
+ return op
110
+ }
111
+
112
+ if (tool === "question") {
113
+ const questions = parameters.questions
114
+ if (Array.isArray(questions) && questions.length > 0) {
115
+ const headers = questions
116
+ .map((q: any) => q.header || "")
117
+ .filter(Boolean)
118
+ .slice(0, 3)
119
+
120
+ const count = questions.length
121
+ const plural = count > 1 ? "s" : ""
122
+
123
+ if (headers.length > 0) {
124
+ const suffix = count > 3 ? ` (+${count - 3} more)` : ""
125
+ return `${count} question${plural}: ${headers.join(", ")}${suffix}`
126
+ }
127
+ return `${count} question${plural}`
128
+ }
129
+ return "question"
130
+ }
131
+
132
+ const paramStr = JSON.stringify(parameters)
133
+ if (paramStr === "{}" || paramStr === "[]" || paramStr === "null") {
134
+ return ""
135
+ }
136
+
137
+ return paramStr.substring(0, 50)
138
+ }
139
+
140
+ export function formatStatsHeader(totalTokensSaved: number, pruneTokenCounter: number): string {
141
+ const totalTokensSavedStr = `~${formatTokenCount(totalTokensSaved + pruneTokenCounter)}`
142
+ return [`▣ DCP | ${totalTokensSavedStr} saved total`].join("\n")
143
+ }
144
+
145
+ export function formatTokenCount(tokens: number, compact?: boolean): string {
146
+ const suffix = compact ? "" : " tokens"
147
+ if (tokens >= 1000) {
148
+ return `${(tokens / 1000).toFixed(1)}K`.replace(".0K", "K") + suffix
149
+ }
150
+ return tokens.toString() + suffix
151
+ }
152
+
153
+ export function truncate(str: string, maxLen: number = 60): string {
154
+ if (str.length <= maxLen) return str
155
+ return str.slice(0, maxLen - 3) + "..."
156
+ }
157
+
158
+ export function formatProgressBar(
159
+ messageIds: string[],
160
+ prunedMessages: Map<string, number>,
161
+ recentMessageIds: string[],
162
+ width: number = 50,
163
+ ): string {
164
+ const ACTIVE = "█"
165
+ const PRUNED = "░"
166
+ const RECENT = "⣿"
167
+ const recentSet = new Set(recentMessageIds)
168
+
169
+ const total = messageIds.length
170
+ if (total === 0) return `│${PRUNED.repeat(width)}│`
171
+
172
+ const bar = new Array(width).fill(ACTIVE)
173
+
174
+ for (let m = 0; m < total; m++) {
175
+ const msgId = messageIds[m]
176
+ const start = Math.floor((m / total) * width)
177
+ const end = Math.floor(((m + 1) / total) * width)
178
+
179
+ if (recentSet.has(msgId)) {
180
+ for (let i = start; i < end; i++) {
181
+ bar[i] = RECENT
182
+ }
183
+ } else if (prunedMessages.has(msgId)) {
184
+ for (let i = start; i < end; i++) {
185
+ bar[i] = PRUNED
186
+ }
187
+ }
188
+ }
189
+
190
+ return `│${bar.join("")}│`
191
+ }
192
+
193
+ export function cacheSystemPromptTokens(state: SessionState, messages: WithParts[]): void {
194
+ let firstInputTokens = 0
195
+ for (const msg of messages) {
196
+ if (msg.info.role !== "assistant") {
197
+ continue
198
+ }
199
+ const info = msg.info as any
200
+ const input = info?.tokens?.input || 0
201
+ const cacheRead = info?.tokens?.cache?.read || 0
202
+ const cacheWrite = info?.tokens?.cache?.write || 0
203
+ if (input > 0 || cacheRead > 0 || cacheWrite > 0) {
204
+ firstInputTokens = input + cacheRead + cacheWrite
205
+ break
206
+ }
207
+ }
208
+
209
+ if (firstInputTokens <= 0) {
210
+ state.systemPromptTokens = undefined
211
+ return
212
+ }
213
+
214
+ let firstUserText = ""
215
+ for (const msg of messages) {
216
+ if (msg.info.role !== "user" || isIgnoredUserMessage(msg)) {
217
+ continue
218
+ }
219
+ const parts = Array.isArray(msg.parts) ? msg.parts : []
220
+ for (const part of parts) {
221
+ if (part.type === "text" && !(part as any).ignored) {
222
+ firstUserText += part.text
223
+ }
224
+ }
225
+ break
226
+ }
227
+
228
+ const estimatedSystemTokens = Math.max(0, firstInputTokens - countTokens(firstUserText))
229
+ state.systemPromptTokens = estimatedSystemTokens > 0 ? estimatedSystemTokens : undefined
230
+ }
231
+
232
+ export function shortenPath(input: string, workingDirectory?: string): string {
233
+ const inPathMatch = input.match(/^(.+) in (.+)$/)
234
+ if (inPathMatch) {
235
+ const prefix = inPathMatch[1]
236
+ const pathPart = inPathMatch[2]
237
+ const shortenedPath = shortenSinglePath(pathPart, workingDirectory)
238
+ return `${prefix} in ${shortenedPath}`
239
+ }
240
+
241
+ return shortenSinglePath(input, workingDirectory)
242
+ }
243
+
244
+ function shortenSinglePath(path: string, workingDirectory?: string): string {
245
+ if (workingDirectory) {
246
+ if (path.startsWith(workingDirectory + "/")) {
247
+ return path.slice(workingDirectory.length + 1)
248
+ }
249
+ if (path === workingDirectory) {
250
+ return "."
251
+ }
252
+ }
253
+
254
+ return path
255
+ }
256
+
257
+ export function formatPrunedItemsList(
258
+ pruneToolIds: string[],
259
+ toolMetadata: Map<string, ToolParameterEntry>,
260
+ workingDirectory?: string,
261
+ ): string[] {
262
+ const lines: string[] = []
263
+
264
+ for (const id of pruneToolIds) {
265
+ const metadata = toolMetadata.get(id)
266
+
267
+ if (metadata) {
268
+ const paramKey = extractParameterKey(metadata.tool, metadata.parameters)
269
+ if (paramKey) {
270
+ // Use 60 char limit to match notification style
271
+ const displayKey = truncate(shortenPath(paramKey, workingDirectory), 60)
272
+ lines.push(`→ ${metadata.tool}: ${displayKey}`)
273
+ } else {
274
+ lines.push(`→ ${metadata.tool}`)
275
+ }
276
+ }
277
+ }
278
+
279
+ const knownCount = pruneToolIds.filter((id) => toolMetadata.has(id)).length
280
+ const unknownCount = pruneToolIds.length - knownCount
281
+
282
+ if (unknownCount > 0) {
283
+ lines.push(`→ (${unknownCount} tool${unknownCount > 1 ? "s" : ""} with unknown metadata)`)
284
+ }
285
+
286
+ return lines
287
+ }
288
+
289
+ export function formatPruningResultForTool(
290
+ prunedIds: string[],
291
+ toolMetadata: Map<string, ToolParameterEntry>,
292
+ workingDirectory?: string,
293
+ ): string {
294
+ const lines: string[] = []
295
+ lines.push(`Context pruning complete. Pruned ${prunedIds.length} tool outputs.`)
296
+ lines.push("")
297
+
298
+ if (prunedIds.length > 0) {
299
+ lines.push(`Semantically pruned (${prunedIds.length}):`)
300
+ lines.push(...formatPrunedItemsList(prunedIds, toolMetadata, workingDirectory))
301
+ }
302
+
303
+ return lines.join("\n").trim()
304
+ }
package/lib/update.ts ADDED
@@ -0,0 +1,185 @@
1
+ import { readFile, rm } from "node:fs/promises"
2
+ import { basename, dirname, join } from "node:path"
3
+ import { fileURLToPath } from "node:url"
4
+ import type { PluginInput } from "@opencode-ai/plugin"
5
+
6
+ type PackageJson = {
7
+ name?: string
8
+ version?: string
9
+ dependencies?: Record<string, string>
10
+ }
11
+
12
+ type UpdateResult =
13
+ | { updated: true; name: string; current: string; latest: string }
14
+ | { updated: false; error: "remove_failed"; name: string; current: string; latest: string }
15
+ | { updated: false }
16
+
17
+ const PACKAGE_NAME = "@tarquinen/opencode-dcp"
18
+
19
+ export function startAutoUpdate(ctx: PluginInput, enabled: boolean): void {
20
+ if (!enabled) return
21
+
22
+ const controller = new AbortController()
23
+ const timeout = setTimeout(() => controller.abort(), 10_000)
24
+ void checkAutoUpdate(controller.signal)
25
+ .then((result) => {
26
+ if (!result.updated) return
27
+ setTimeout(() => {
28
+ ctx.client.tui.showToast({
29
+ body: {
30
+ title: "DCP update ready",
31
+ message: `Updated ${result.name} from ${result.current} to ${result.latest}. Restart OpenCode to finish.`,
32
+ variant: "info",
33
+ duration: 7000,
34
+ },
35
+ })
36
+ }, 5000)
37
+ })
38
+ .catch(() => {})
39
+ .finally(() => clearTimeout(timeout))
40
+ }
41
+
42
+ export async function checkAutoUpdate(signal: AbortSignal): Promise<UpdateResult> {
43
+ const packageDir = await findPackageDir(PACKAGE_NAME)
44
+ if (!packageDir) return { updated: false }
45
+
46
+ const pkg = await readPackageJson(join(packageDir, "package.json"))
47
+ if (!pkg?.name || !pkg.version) return { updated: false }
48
+
49
+ const latest = await fetchLatestVersion(pkg.name, signal)
50
+ if (!latest || !isVersionNewer(latest, pkg.version)) return { updated: false }
51
+
52
+ const removeDir = await updateRemoveDir(packageDir, pkg.name)
53
+ if (!removeDir) return { updated: false }
54
+
55
+ try {
56
+ await rm(removeDir, { recursive: true, force: true })
57
+ } catch {
58
+ return {
59
+ updated: false,
60
+ error: "remove_failed",
61
+ name: pkg.name,
62
+ current: pkg.version,
63
+ latest,
64
+ }
65
+ }
66
+
67
+ return { updated: true, name: pkg.name, current: pkg.version, latest }
68
+ }
69
+
70
+ async function findPackageDir(name: string) {
71
+ let dir = dirname(fileURLToPath(import.meta.url))
72
+ for (;;) {
73
+ const pkg = await readPackageJson(join(dir, "package.json"))
74
+ if (pkg?.name === name) return dir
75
+
76
+ const parent = dirname(dir)
77
+ if (parent === dir) return undefined
78
+ dir = parent
79
+ }
80
+ }
81
+
82
+ export async function updateRemoveDir(packageDir: string, name: string) {
83
+ const packageParent = dirname(packageDir)
84
+ const nodeModulesDir = basename(packageParent).startsWith("@")
85
+ ? dirname(packageParent)
86
+ : packageParent
87
+ if (basename(nodeModulesDir) !== "node_modules") return undefined
88
+
89
+ const wrapperDir = dirname(nodeModulesDir)
90
+ const wrapperPkg = await readPackageJson(join(wrapperDir, "package.json"))
91
+ const spec = wrapperSpec(wrapperDir, name) ?? wrapperPkg?.dependencies?.[name]
92
+ if (!spec || !isAutoUpdatableSpec(spec)) return undefined
93
+
94
+ return wrapperDir
95
+ }
96
+
97
+ function wrapperSpec(wrapperDir: string, name: string) {
98
+ if (name.startsWith("@")) {
99
+ const [scope, pkg] = name.split("/")
100
+ if (!scope || !pkg || basename(dirname(wrapperDir)) !== scope) return undefined
101
+ const prefix = `${pkg}@`
102
+ const base = basename(wrapperDir)
103
+ return base.startsWith(prefix) ? base.slice(prefix.length) : undefined
104
+ }
105
+
106
+ const prefix = `${name}@`
107
+ const base = basename(wrapperDir)
108
+ return base.startsWith(prefix) ? base.slice(prefix.length) : undefined
109
+ }
110
+
111
+ export function isAutoUpdatableSpec(spec: string) {
112
+ const value = spec.trim()
113
+ if (!value) return false
114
+ if (value === "latest" || value === "*") return true
115
+ if (/^[~^]/.test(value)) return true
116
+ if (/^(?:>=|>|<=|<)/.test(value)) return true
117
+ if (/\s+(?:\|\||-|[<>=])\s+/.test(value)) return true
118
+ return false
119
+ }
120
+
121
+ async function readPackageJson(path: string): Promise<PackageJson | undefined> {
122
+ try {
123
+ const data = JSON.parse(await readFile(path, "utf-8"))
124
+ return data && typeof data === "object" ? (data as PackageJson) : undefined
125
+ } catch {
126
+ return undefined
127
+ }
128
+ }
129
+
130
+ async function fetchLatestVersion(name: string, signal: AbortSignal) {
131
+ try {
132
+ const response = await fetch(
133
+ `https://registry.npmjs.org/${encodeURIComponent(name)}/latest`,
134
+ {
135
+ signal,
136
+ },
137
+ )
138
+ if (!response.ok) return undefined
139
+ const data: unknown = await response.json()
140
+ if (!data || typeof data !== "object") return undefined
141
+ const version = (data as { version?: unknown }).version
142
+ return typeof version === "string" ? version : undefined
143
+ } catch {
144
+ return undefined
145
+ }
146
+ }
147
+
148
+ export function isVersionNewer(latest: string, current: string) {
149
+ const next = parseVersion(latest)
150
+ const prev = parseVersion(current)
151
+ if (!next || !prev) return false
152
+
153
+ for (let i = 0; i < 3; i++) {
154
+ if (next.parts[i] !== prev.parts[i]) return next.parts[i] > prev.parts[i]
155
+ }
156
+
157
+ if (!next.pre.length && prev.pre.length) return true
158
+ if (next.pre.length && !prev.pre.length) return false
159
+
160
+ for (let i = 0; i < Math.max(next.pre.length, prev.pre.length); i++) {
161
+ const a = next.pre[i]
162
+ const b = prev.pre[i]
163
+ if (a === undefined) return false
164
+ if (b === undefined) return true
165
+ if (a === b) continue
166
+
167
+ const aNumber = /^\d+$/.test(a) ? Number(a) : undefined
168
+ const bNumber = /^\d+$/.test(b) ? Number(b) : undefined
169
+ if (aNumber !== undefined && bNumber !== undefined) return aNumber > bNumber
170
+ if (aNumber !== undefined) return false
171
+ if (bNumber !== undefined) return true
172
+ return a > b
173
+ }
174
+
175
+ return false
176
+ }
177
+
178
+ function parseVersion(version: string) {
179
+ const match = version.match(/^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+.+)?$/)
180
+ if (!match) return undefined
181
+ return {
182
+ parts: [Number(match[1]), Number(match[2]), Number(match[3])],
183
+ pre: match[4]?.split(".") ?? [],
184
+ }
185
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
3
  "name": "@tarquinen/opencode-dcp",
4
- "version": "3.1.12",
4
+ "version": "3.1.14",
5
5
  "type": "module",
6
6
  "description": "OpenCode plugin that optimizes token usage by pruning obsolete tool outputs from conversation context",
7
7
  "main": "./dist/index.js",
@@ -14,6 +14,10 @@
14
14
  "./server": {
15
15
  "types": "./dist/index.d.ts",
16
16
  "import": "./dist/index.js"
17
+ },
18
+ "./tui": {
19
+ "types": "./dist/tui.d.ts",
20
+ "import": "./tui.tsx"
17
21
  }
18
22
  },
19
23
  "scripts": {
@@ -40,16 +44,19 @@
40
44
  ],
41
45
  "repository": {
42
46
  "type": "git",
43
- "url": "git+https://github.com/Tarquinen/opencode-dynamic-context-pruning.git"
47
+ "url": "git+https://github.com/Opencode-DCP/opencode-dynamic-context-pruning.git"
44
48
  },
45
49
  "bugs": {
46
- "url": "https://github.com/Tarquinen/opencode-dynamic-context-pruning/issues"
50
+ "url": "https://github.com/Opencode-DCP/opencode-dynamic-context-pruning/issues"
47
51
  },
48
- "homepage": "https://github.com/Tarquinen/opencode-dynamic-context-pruning#readme",
52
+ "homepage": "https://github.com/Opencode-DCP/opencode-dynamic-context-pruning#readme",
49
53
  "author": "tarquinen",
50
54
  "license": "AGPL-3.0-or-later",
51
55
  "peerDependencies": {
52
- "@opencode-ai/plugin": ">=1.4.3"
56
+ "@opencode-ai/plugin": ">=1.4.3",
57
+ "@opentui/core": "^0.4.2",
58
+ "@opentui/solid": "^0.4.2",
59
+ "solid-js": "^1.9.12"
53
60
  },
54
61
  "dependencies": {
55
62
  "@anthropic-ai/tokenizer": "^0.0.4",
@@ -58,14 +65,23 @@
58
65
  },
59
66
  "devDependencies": {
60
67
  "@opencode-ai/plugin": "^1.4.3",
68
+ "@opentui/core": "^0.4.2",
69
+ "@opentui/solid": "^0.4.2",
61
70
  "@types/node": "^25.5.0",
62
71
  "prettier": "^3.8.1",
72
+ "solid-js": "^1.9.12",
63
73
  "tsup": "^8.5.1",
64
74
  "tsx": "^4.21.0",
65
75
  "typescript": "^6.0.2"
66
76
  },
77
+ "overrides": {
78
+ "@babel/core": "7.29.7",
79
+ "esbuild": "0.28.1"
80
+ },
67
81
  "files": [
68
82
  "dist/",
83
+ "lib/",
84
+ "tui.tsx",
69
85
  "README.md",
70
86
  "LICENSE"
71
87
  ]
package/tui.tsx ADDED
@@ -0,0 +1,26 @@
1
+ /** @jsxImportSource @opentui/solid */
2
+
3
+ import type { TuiPluginModule } from "@opencode-ai/plugin/tui"
4
+ import { registerCommands } from "./lib/tui/commands"
5
+ import { loadConfig } from "./lib/tui/data"
6
+ import { openPanelModal } from "./lib/tui/modals"
7
+
8
+ const tui: TuiPluginModule["tui"] = async (api) => {
9
+ const config = loadConfig(api)
10
+ if (!config.enabled || !config.commands.enabled) return
11
+
12
+ registerCommands(api, [
13
+ {
14
+ title: "DCP",
15
+ name: "dcp.panel",
16
+ description: "Open DCP panel",
17
+ slashName: "dcp",
18
+ run: () => openPanelModal(api, config),
19
+ },
20
+ ])
21
+ }
22
+
23
+ export default {
24
+ id: "opencode-dcp",
25
+ tui,
26
+ } satisfies TuiPluginModule