@tarquinen/opencode-dcp 3.1.11 → 3.1.13
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/README.md +14 -9
- package/dist/index.js +106 -49
- package/dist/index.js.map +1 -1
- package/dist/lib/commands/context.d.ts +15 -0
- package/dist/lib/commands/context.d.ts.map +1 -1
- package/dist/lib/commands/help.d.ts +1 -0
- package/dist/lib/commands/help.d.ts.map +1 -1
- package/dist/lib/commands/manual.d.ts +1 -1
- package/dist/lib/commands/manual.d.ts.map +1 -1
- package/dist/lib/commands/stats.d.ts +10 -0
- package/dist/lib/commands/stats.d.ts.map +1 -1
- package/dist/lib/compress/pipeline.d.ts.map +1 -1
- package/dist/lib/hooks.d.ts +0 -2
- package/dist/lib/hooks.d.ts.map +1 -1
- package/dist/lib/messages/index.d.ts +0 -1
- package/dist/lib/messages/index.d.ts.map +1 -1
- package/dist/lib/messages/inject/utils.d.ts +0 -7
- package/dist/lib/messages/inject/utils.d.ts.map +1 -1
- package/dist/lib/state/persistence.d.ts +3 -0
- package/dist/lib/state/persistence.d.ts.map +1 -1
- package/dist/lib/state/state.d.ts +1 -0
- package/dist/lib/state/state.d.ts.map +1 -1
- package/dist/lib/tui/commands.d.ts +3 -0
- package/dist/lib/tui/commands.d.ts.map +1 -0
- package/dist/lib/tui/data.d.ts +14 -0
- package/dist/lib/tui/data.d.ts.map +1 -0
- package/dist/lib/tui/dialogs.d.ts +30 -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/modals.d.ts +10 -0
- package/dist/lib/tui/modals.d.ts.map +1 -0
- package/dist/lib/tui/types.d.ts +15 -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/tui.d.ts +7 -0
- package/dist/tui.d.ts.map +1 -0
- package/lib/auth.ts +37 -0
- package/lib/commands/compression-targets.ts +137 -0
- package/lib/commands/context.ts +305 -0
- package/lib/commands/decompress.ts +275 -0
- package/lib/commands/help.ts +76 -0
- package/lib/commands/index.ts +11 -0
- package/lib/commands/manual.ts +127 -0
- package/lib/commands/recompress.ts +224 -0
- package/lib/commands/stats.ts +159 -0
- package/lib/commands/sweep.ts +268 -0
- package/lib/compress/index.ts +3 -0
- package/lib/compress/message-utils.ts +250 -0
- package/lib/compress/message.ts +145 -0
- package/lib/compress/pipeline.ts +108 -0
- package/lib/compress/protected-content.ts +208 -0
- package/lib/compress/range-utils.ts +308 -0
- package/lib/compress/range.ts +192 -0
- package/lib/compress/search.ts +267 -0
- package/lib/compress/state.ts +268 -0
- package/lib/compress/timing.ts +77 -0
- package/lib/compress/types.ts +108 -0
- package/lib/compress-permission.ts +25 -0
- package/lib/config.ts +1007 -0
- package/lib/hooks.ts +375 -0
- package/lib/host-permissions.ts +101 -0
- package/lib/logger.ts +226 -0
- package/lib/message-ids.ts +172 -0
- package/lib/messages/index.ts +8 -0
- package/lib/messages/inject/inject.ts +215 -0
- package/lib/messages/inject/subagent-results.ts +82 -0
- package/lib/messages/inject/utils.ts +374 -0
- package/lib/messages/priority.ts +102 -0
- package/lib/messages/prune.ts +233 -0
- package/lib/messages/query.ts +72 -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 +185 -0
- package/lib/prompts/compress-message.ts +43 -0
- package/lib/prompts/compress-range.ts +60 -0
- package/lib/prompts/context-limit-nudge.ts +18 -0
- package/lib/prompts/extensions/nudge.ts +43 -0
- package/lib/prompts/extensions/system.ts +32 -0
- package/lib/prompts/extensions/tool.ts +35 -0
- package/lib/prompts/index.ts +29 -0
- package/lib/prompts/iteration-nudge.ts +6 -0
- package/lib/prompts/store.ts +467 -0
- package/lib/prompts/system.ts +33 -0
- package/lib/prompts/turn-nudge.ts +10 -0
- package/lib/protected-patterns.ts +128 -0
- package/lib/state/index.ts +4 -0
- package/lib/state/persistence.ts +305 -0
- package/lib/state/state.ts +208 -0
- package/lib/state/tool-cache.ts +98 -0
- package/lib/state/types.ts +111 -0
- package/lib/state/utils.ts +345 -0
- package/lib/strategies/deduplication.ts +127 -0
- package/lib/strategies/index.ts +2 -0
- package/lib/strategies/purge-errors.ts +88 -0
- package/lib/subagents/subagent-results.ts +74 -0
- package/lib/token-utils.ts +164 -0
- package/lib/tui/commands.ts +31 -0
- package/lib/tui/data.ts +73 -0
- package/lib/tui/dialogs.tsx +235 -0
- package/lib/tui/format.ts +25 -0
- package/lib/tui/modals.tsx +92 -0
- package/lib/tui/types.ts +16 -0
- package/lib/tui/ui.tsx +219 -0
- package/lib/ui/notification.ts +347 -0
- package/lib/ui/utils.ts +304 -0
- package/lib/update.ts +185 -0
- package/package.json +17 -5
- package/tui.tsx +26 -0
package/lib/ui/utils.ts
ADDED
|
@@ -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.
|
|
4
|
+
"version": "3.1.13",
|
|
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,12 +44,12 @@
|
|
|
40
44
|
],
|
|
41
45
|
"repository": {
|
|
42
46
|
"type": "git",
|
|
43
|
-
"url": "git+https://github.com/
|
|
47
|
+
"url": "git+https://github.com/Opencode-DCP/opencode-dynamic-context-pruning.git"
|
|
44
48
|
},
|
|
45
49
|
"bugs": {
|
|
46
|
-
"url": "https://github.com/
|
|
50
|
+
"url": "https://github.com/Opencode-DCP/opencode-dynamic-context-pruning/issues"
|
|
47
51
|
},
|
|
48
|
-
"homepage": "https://github.com/
|
|
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": {
|
|
@@ -54,8 +58,10 @@
|
|
|
54
58
|
"dependencies": {
|
|
55
59
|
"@anthropic-ai/tokenizer": "^0.0.4",
|
|
56
60
|
"@opencode-ai/sdk": "^1.4.3",
|
|
61
|
+
"@opentui/core": "^0.2.16",
|
|
62
|
+
"@opentui/solid": "^0.2.16",
|
|
57
63
|
"jsonc-parser": "^3.3.1",
|
|
58
|
-
"
|
|
64
|
+
"solid-js": "^1.9.12"
|
|
59
65
|
},
|
|
60
66
|
"devDependencies": {
|
|
61
67
|
"@opencode-ai/plugin": "^1.4.3",
|
|
@@ -65,8 +71,14 @@
|
|
|
65
71
|
"tsx": "^4.21.0",
|
|
66
72
|
"typescript": "^6.0.2"
|
|
67
73
|
},
|
|
74
|
+
"overrides": {
|
|
75
|
+
"@babel/core": "7.29.7",
|
|
76
|
+
"esbuild": "0.28.1"
|
|
77
|
+
},
|
|
68
78
|
"files": [
|
|
69
79
|
"dist/",
|
|
80
|
+
"lib/",
|
|
81
|
+
"tui.tsx",
|
|
70
82
|
"README.md",
|
|
71
83
|
"LICENSE"
|
|
72
84
|
]
|
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
|