@tarquinen/opencode-dcp 3.1.12 → 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.
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 +18 -5
  106. package/tui.tsx +26 -0
@@ -0,0 +1,192 @@
1
+ import { tool } from "@opencode-ai/plugin"
2
+ import type { ToolContext } from "./types"
3
+ import { countTokens } from "../token-utils"
4
+ import { RANGE_FORMAT_EXTENSION } from "../prompts/extensions/tool"
5
+ import { finalizeSession, prepareSession, type NotificationEntry } from "./pipeline"
6
+ import {
7
+ appendProtectedPromptInfo,
8
+ appendProtectedTools,
9
+ appendProtectedUserMessages,
10
+ } from "./protected-content"
11
+ import {
12
+ appendMissingBlockSummaries,
13
+ injectBlockPlaceholders,
14
+ parseBlockPlaceholders,
15
+ resolveRanges,
16
+ validateArgs,
17
+ validateNonOverlapping,
18
+ validateSummaryPlaceholders,
19
+ } from "./range-utils"
20
+ import {
21
+ COMPRESSED_BLOCK_HEADER,
22
+ allocateBlockId,
23
+ allocateRunId,
24
+ applyCompressionState,
25
+ wrapCompressedSummary,
26
+ } from "./state"
27
+ import type { CompressRangeToolArgs } from "./types"
28
+
29
+ function buildSchema() {
30
+ return {
31
+ topic: tool.schema
32
+ .string()
33
+ .describe("Short label (3-5 words) for display - e.g., 'Auth System Exploration'"),
34
+ content: tool.schema
35
+ .array(
36
+ tool.schema.object({
37
+ startId: tool.schema
38
+ .string()
39
+ .describe(
40
+ "Message or block ID marking the beginning of range (e.g. m0001, b2)",
41
+ ),
42
+ endId: tool.schema
43
+ .string()
44
+ .describe("Message or block ID marking the end of range (e.g. m0012, b5)"),
45
+ summary: tool.schema
46
+ .string()
47
+ .describe("Complete technical summary replacing all content in range"),
48
+ }),
49
+ )
50
+ .describe(
51
+ "One or more ranges to compress, each with start/end boundaries and a summary",
52
+ ),
53
+ }
54
+ }
55
+
56
+ export function createCompressRangeTool(ctx: ToolContext): ReturnType<typeof tool> {
57
+ ctx.prompts.reload()
58
+ const runtimePrompts = ctx.prompts.getRuntimePrompts()
59
+
60
+ return tool({
61
+ description: runtimePrompts.compressRange + RANGE_FORMAT_EXTENSION,
62
+ args: buildSchema(),
63
+ async execute(args, toolCtx) {
64
+ const input = args as CompressRangeToolArgs
65
+ validateArgs(input)
66
+ const callId =
67
+ typeof (toolCtx as unknown as { callID?: unknown }).callID === "string"
68
+ ? (toolCtx as unknown as { callID: string }).callID
69
+ : undefined
70
+
71
+ const { rawMessages, searchContext } = await prepareSession(
72
+ ctx,
73
+ toolCtx,
74
+ `Compress Range: ${input.topic}`,
75
+ )
76
+ const resolvedPlans = resolveRanges(input, searchContext, ctx.state)
77
+ validateNonOverlapping(resolvedPlans)
78
+
79
+ const notifications: NotificationEntry[] = []
80
+ const preparedPlans: Array<{
81
+ entry: (typeof resolvedPlans)[number]["entry"]
82
+ selection: (typeof resolvedPlans)[number]["selection"]
83
+ anchorMessageId: string
84
+ finalSummary: string
85
+ consumedBlockIds: number[]
86
+ }> = []
87
+ let totalCompressedMessages = 0
88
+
89
+ for (const plan of resolvedPlans) {
90
+ const parsedPlaceholders = parseBlockPlaceholders(plan.entry.summary)
91
+ const missingBlockIds = validateSummaryPlaceholders(
92
+ parsedPlaceholders,
93
+ plan.selection.requiredBlockIds,
94
+ plan.selection.startReference,
95
+ plan.selection.endReference,
96
+ searchContext.summaryByBlockId,
97
+ )
98
+
99
+ const injected = injectBlockPlaceholders(
100
+ plan.entry.summary,
101
+ parsedPlaceholders,
102
+ searchContext.summaryByBlockId,
103
+ plan.selection.startReference,
104
+ plan.selection.endReference,
105
+ )
106
+
107
+ const summaryWithUsers = appendProtectedUserMessages(
108
+ injected.expandedSummary,
109
+ plan.selection,
110
+ searchContext,
111
+ ctx.state,
112
+ ctx.config.compress.protectUserMessages,
113
+ )
114
+
115
+ const summaryWithPromptInfo = appendProtectedPromptInfo(
116
+ summaryWithUsers,
117
+ plan.selection,
118
+ searchContext,
119
+ ctx.state,
120
+ ctx.config.compress.protectTags,
121
+ )
122
+
123
+ const summaryWithTools = await appendProtectedTools(
124
+ ctx.client,
125
+ ctx.state,
126
+ ctx.config.experimental.allowSubAgents,
127
+ summaryWithPromptInfo,
128
+ plan.selection,
129
+ searchContext,
130
+ ctx.config.compress.protectedTools,
131
+ ctx.config.protectedFilePatterns,
132
+ )
133
+
134
+ const completedSummary = appendMissingBlockSummaries(
135
+ summaryWithTools,
136
+ missingBlockIds,
137
+ searchContext.summaryByBlockId,
138
+ injected.consumedBlockIds,
139
+ )
140
+
141
+ preparedPlans.push({
142
+ entry: plan.entry,
143
+ selection: plan.selection,
144
+ anchorMessageId: plan.anchorMessageId,
145
+ finalSummary: completedSummary.expandedSummary,
146
+ consumedBlockIds: completedSummary.consumedBlockIds,
147
+ })
148
+ }
149
+
150
+ const runId = allocateRunId(ctx.state)
151
+
152
+ for (const preparedPlan of preparedPlans) {
153
+ const blockId = allocateBlockId(ctx.state)
154
+ const storedSummary = wrapCompressedSummary(blockId, preparedPlan.finalSummary)
155
+ const summaryTokens = countTokens(storedSummary)
156
+
157
+ const applied = applyCompressionState(
158
+ ctx.state,
159
+ {
160
+ topic: input.topic,
161
+ batchTopic: input.topic,
162
+ startId: preparedPlan.entry.startId,
163
+ endId: preparedPlan.entry.endId,
164
+ mode: "range",
165
+ runId,
166
+ compressMessageId: toolCtx.messageID,
167
+ compressCallId: callId,
168
+ summaryTokens,
169
+ },
170
+ preparedPlan.selection,
171
+ preparedPlan.anchorMessageId,
172
+ blockId,
173
+ storedSummary,
174
+ preparedPlan.consumedBlockIds,
175
+ )
176
+
177
+ totalCompressedMessages += applied.messageIds.length
178
+
179
+ notifications.push({
180
+ blockId,
181
+ runId,
182
+ summary: preparedPlan.finalSummary,
183
+ summaryTokens,
184
+ })
185
+ }
186
+
187
+ await finalizeSession(ctx, toolCtx, rawMessages, notifications, input.topic)
188
+
189
+ return `Compressed ${totalCompressedMessages} messages into ${COMPRESSED_BLOCK_HEADER}.`
190
+ },
191
+ })
192
+ }
@@ -0,0 +1,267 @@
1
+ import type { SessionState, WithParts } from "../state"
2
+ import { formatBlockRef, parseBoundaryId } from "../message-ids"
3
+ import { isIgnoredUserMessage } from "../messages/query"
4
+ import { filterMessages } from "../messages/shape"
5
+ import { countAllMessageTokens } from "../token-utils"
6
+ import type { BoundaryReference, SearchContext, SelectionResolution } from "./types"
7
+
8
+ export async function fetchSessionMessages(client: any, sessionId: string): Promise<WithParts[]> {
9
+ const response = await client.session.messages({
10
+ path: { id: sessionId },
11
+ })
12
+
13
+ return filterMessages(response?.data || response)
14
+ }
15
+
16
+ export function buildSearchContext(state: SessionState, rawMessages: WithParts[]): SearchContext {
17
+ const rawMessagesById = new Map<string, WithParts>()
18
+ const rawIndexById = new Map<string, number>()
19
+ for (const msg of rawMessages) {
20
+ rawMessagesById.set(msg.info.id, msg)
21
+ }
22
+ for (let index = 0; index < rawMessages.length; index++) {
23
+ const message = rawMessages[index]
24
+ if (!message) {
25
+ continue
26
+ }
27
+ rawIndexById.set(message.info.id, index)
28
+ }
29
+
30
+ const summaryByBlockId = new Map()
31
+ for (const [blockId, block] of state.prune.messages.blocksById) {
32
+ if (!block.active) {
33
+ continue
34
+ }
35
+ summaryByBlockId.set(blockId, block)
36
+ }
37
+
38
+ return {
39
+ rawMessages,
40
+ rawMessagesById,
41
+ rawIndexById,
42
+ summaryByBlockId,
43
+ }
44
+ }
45
+
46
+ export function resolveBoundaryIds(
47
+ context: SearchContext,
48
+ state: SessionState,
49
+ startId: string,
50
+ endId: string,
51
+ ): { startReference: BoundaryReference; endReference: BoundaryReference } {
52
+ const lookup = buildBoundaryLookup(context, state)
53
+ const issues: string[] = []
54
+ const parsedStartId = parseBoundaryId(startId)
55
+ const parsedEndId = parseBoundaryId(endId)
56
+
57
+ if (parsedStartId === null) {
58
+ issues.push("startId is invalid. Use an injected message ID (mNNNN) or block ID (bN).")
59
+ }
60
+
61
+ if (parsedEndId === null) {
62
+ issues.push("endId is invalid. Use an injected message ID (mNNNN) or block ID (bN).")
63
+ }
64
+
65
+ if (issues.length > 0) {
66
+ throw new Error(
67
+ issues.length === 1 ? issues[0] : issues.map((issue) => `- ${issue}`).join("\n"),
68
+ )
69
+ }
70
+
71
+ if (!parsedStartId || !parsedEndId) {
72
+ throw new Error("Invalid boundary ID(s)")
73
+ }
74
+
75
+ const startReference = lookup.get(parsedStartId.ref)
76
+ const endReference = lookup.get(parsedEndId.ref)
77
+
78
+ if (!startReference) {
79
+ issues.push(
80
+ `startId ${parsedStartId.ref} is not available in the current conversation context. Choose an injected ID visible in context.`,
81
+ )
82
+ }
83
+
84
+ if (!endReference) {
85
+ issues.push(
86
+ `endId ${parsedEndId.ref} is not available in the current conversation context. Choose an injected ID visible in context.`,
87
+ )
88
+ }
89
+
90
+ if (issues.length > 0) {
91
+ throw new Error(
92
+ issues.length === 1 ? issues[0] : issues.map((issue) => `- ${issue}`).join("\n"),
93
+ )
94
+ }
95
+
96
+ if (!startReference || !endReference) {
97
+ throw new Error("Failed to resolve boundary IDs")
98
+ }
99
+
100
+ if (startReference.rawIndex > endReference.rawIndex) {
101
+ throw new Error(
102
+ `startId ${parsedStartId.ref} appears after endId ${parsedEndId.ref} in the conversation. Start must come before end.`,
103
+ )
104
+ }
105
+
106
+ return { startReference, endReference }
107
+ }
108
+
109
+ export function resolveSelection(
110
+ context: SearchContext,
111
+ startReference: BoundaryReference,
112
+ endReference: BoundaryReference,
113
+ ): SelectionResolution {
114
+ const startRawIndex = startReference.rawIndex
115
+ const endRawIndex = endReference.rawIndex
116
+ const messageIds: string[] = []
117
+ const messageSeen = new Set<string>()
118
+ const toolIds: string[] = []
119
+ const toolSeen = new Set<string>()
120
+ const requiredBlockIds: number[] = []
121
+ const requiredBlockSeen = new Set<number>()
122
+ const messageTokenById = new Map<string, number>()
123
+
124
+ for (let index = startRawIndex; index <= endRawIndex; index++) {
125
+ const rawMessage = context.rawMessages[index]
126
+ if (!rawMessage) {
127
+ continue
128
+ }
129
+ if (isIgnoredUserMessage(rawMessage)) {
130
+ continue
131
+ }
132
+
133
+ const messageId = rawMessage.info.id
134
+ if (!messageSeen.has(messageId)) {
135
+ messageSeen.add(messageId)
136
+ messageIds.push(messageId)
137
+ }
138
+
139
+ if (!messageTokenById.has(messageId)) {
140
+ messageTokenById.set(messageId, countAllMessageTokens(rawMessage))
141
+ }
142
+
143
+ const parts = Array.isArray(rawMessage.parts) ? rawMessage.parts : []
144
+ for (const part of parts) {
145
+ if (part.type !== "tool" || !part.callID) {
146
+ continue
147
+ }
148
+ if (toolSeen.has(part.callID)) {
149
+ continue
150
+ }
151
+ toolSeen.add(part.callID)
152
+ toolIds.push(part.callID)
153
+ }
154
+ }
155
+
156
+ const selectedMessageIds = new Set(messageIds)
157
+ const summariesInSelection: Array<{ blockId: number; rawIndex: number }> = []
158
+ for (const summary of context.summaryByBlockId.values()) {
159
+ if (!selectedMessageIds.has(summary.anchorMessageId)) {
160
+ continue
161
+ }
162
+
163
+ const anchorIndex = context.rawIndexById.get(summary.anchorMessageId)
164
+ if (anchorIndex === undefined) {
165
+ continue
166
+ }
167
+
168
+ summariesInSelection.push({
169
+ blockId: summary.blockId,
170
+ rawIndex: anchorIndex,
171
+ })
172
+ }
173
+
174
+ summariesInSelection.sort((a, b) => a.rawIndex - b.rawIndex || a.blockId - b.blockId)
175
+ for (const summary of summariesInSelection) {
176
+ if (requiredBlockSeen.has(summary.blockId)) {
177
+ continue
178
+ }
179
+ requiredBlockSeen.add(summary.blockId)
180
+ requiredBlockIds.push(summary.blockId)
181
+ }
182
+
183
+ if (messageIds.length === 0) {
184
+ throw new Error(
185
+ "Failed to map boundary matches back to raw messages. Choose boundaries that include original conversation messages.",
186
+ )
187
+ }
188
+
189
+ return {
190
+ startReference,
191
+ endReference,
192
+ messageIds,
193
+ messageTokenById,
194
+ toolIds,
195
+ requiredBlockIds,
196
+ }
197
+ }
198
+
199
+ export function resolveAnchorMessageId(startReference: BoundaryReference): string {
200
+ if (startReference.kind === "compressed-block") {
201
+ if (!startReference.anchorMessageId) {
202
+ throw new Error("Failed to map boundary matches back to raw messages")
203
+ }
204
+ return startReference.anchorMessageId
205
+ }
206
+
207
+ if (!startReference.messageId) {
208
+ throw new Error("Failed to map boundary matches back to raw messages")
209
+ }
210
+ return startReference.messageId
211
+ }
212
+
213
+ function buildBoundaryLookup(
214
+ context: SearchContext,
215
+ state: SessionState,
216
+ ): Map<string, BoundaryReference> {
217
+ const lookup = new Map<string, BoundaryReference>()
218
+
219
+ for (const [messageRef, messageId] of state.messageIds.byRef) {
220
+ const rawMessage = context.rawMessagesById.get(messageId)
221
+ if (!rawMessage) {
222
+ continue
223
+ }
224
+ if (isIgnoredUserMessage(rawMessage)) {
225
+ continue
226
+ }
227
+
228
+ const rawIndex = context.rawIndexById.get(messageId)
229
+ if (rawIndex === undefined) {
230
+ continue
231
+ }
232
+ lookup.set(messageRef, {
233
+ kind: "message",
234
+ rawIndex,
235
+ messageId,
236
+ })
237
+ }
238
+
239
+ const summaries = Array.from(context.summaryByBlockId.values()).sort(
240
+ (a, b) => a.blockId - b.blockId,
241
+ )
242
+ for (const summary of summaries) {
243
+ const anchorMessage = context.rawMessagesById.get(summary.anchorMessageId)
244
+ if (!anchorMessage) {
245
+ continue
246
+ }
247
+ if (isIgnoredUserMessage(anchorMessage)) {
248
+ continue
249
+ }
250
+
251
+ const rawIndex = context.rawIndexById.get(summary.anchorMessageId)
252
+ if (rawIndex === undefined) {
253
+ continue
254
+ }
255
+ const blockRef = formatBlockRef(summary.blockId)
256
+ if (!lookup.has(blockRef)) {
257
+ lookup.set(blockRef, {
258
+ kind: "compressed-block",
259
+ rawIndex,
260
+ blockId: summary.blockId,
261
+ anchorMessageId: summary.anchorMessageId,
262
+ })
263
+ }
264
+ }
265
+
266
+ return lookup
267
+ }