@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.
Files changed (215) hide show
  1. package/LICENSE +619 -0
  2. package/README.md +348 -2
  3. package/dist/index.d.ts +7 -0
  4. package/dist/index.d.ts.map +1 -0
  5. package/dist/index.js +8129 -0
  6. package/dist/index.js.map +1 -0
  7. package/dist/lib/commands/compression-targets.d.ts +13 -0
  8. package/dist/lib/commands/compression-targets.d.ts.map +1 -0
  9. package/dist/lib/commands/context.d.ts +67 -0
  10. package/dist/lib/commands/context.d.ts.map +1 -0
  11. package/dist/lib/commands/decompress.d.ts +12 -0
  12. package/dist/lib/commands/decompress.d.ts.map +1 -0
  13. package/dist/lib/commands/help.d.ts +18 -0
  14. package/dist/lib/commands/help.d.ts.map +1 -0
  15. package/dist/lib/commands/index.d.ts +8 -0
  16. package/dist/lib/commands/index.d.ts.map +1 -0
  17. package/dist/lib/commands/manual.d.ts +23 -0
  18. package/dist/lib/commands/manual.d.ts.map +1 -0
  19. package/dist/lib/commands/recompress.d.ts +12 -0
  20. package/dist/lib/commands/recompress.d.ts.map +1 -0
  21. package/dist/lib/commands/stats.d.ts +27 -0
  22. package/dist/lib/commands/stats.d.ts.map +1 -0
  23. package/dist/lib/commands/sweep.d.ts +23 -0
  24. package/dist/lib/commands/sweep.d.ts.map +1 -0
  25. package/dist/lib/compress/index.d.ts +3 -0
  26. package/dist/lib/compress/index.d.ts.map +1 -0
  27. package/dist/lib/compress/pipeline.d.ts +29 -0
  28. package/dist/lib/compress/pipeline.d.ts.map +1 -0
  29. package/dist/lib/compress/protected-content.d.ts +6 -0
  30. package/dist/lib/compress/protected-content.d.ts.map +1 -0
  31. package/dist/lib/compress/rc.d.ts +15 -0
  32. package/dist/lib/compress/rc.d.ts.map +1 -0
  33. package/dist/lib/compress/search.d.ts +12 -0
  34. package/dist/lib/compress/search.d.ts.map +1 -0
  35. package/dist/lib/compress/state.d.ts +10 -0
  36. package/dist/lib/compress/state.d.ts.map +1 -0
  37. package/dist/lib/compress/summary.d.ts +68 -0
  38. package/dist/lib/compress/summary.d.ts.map +1 -0
  39. package/dist/lib/compress/timing.d.ts +18 -0
  40. package/dist/lib/compress/timing.d.ts.map +1 -0
  41. package/dist/lib/compress/types.d.ts +64 -0
  42. package/dist/lib/compress/types.d.ts.map +1 -0
  43. package/dist/lib/compress/usage.d.ts +45 -0
  44. package/dist/lib/compress/usage.d.ts.map +1 -0
  45. package/dist/lib/compress-permission.d.ts +4 -0
  46. package/dist/lib/compress-permission.d.ts.map +1 -0
  47. package/dist/lib/config.d.ts +83 -0
  48. package/dist/lib/config.d.ts.map +1 -0
  49. package/dist/lib/hooks.d.ts +22 -0
  50. package/dist/lib/hooks.d.ts.map +1 -0
  51. package/dist/lib/logger.d.ts +31 -0
  52. package/dist/lib/logger.d.ts.map +1 -0
  53. package/dist/lib/message-ids.d.ts +20 -0
  54. package/dist/lib/message-ids.d.ts.map +1 -0
  55. package/dist/lib/messages/index.d.ts +8 -0
  56. package/dist/lib/messages/index.d.ts.map +1 -0
  57. package/dist/lib/messages/inject/inject.d.ts +7 -0
  58. package/dist/lib/messages/inject/inject.d.ts.map +1 -0
  59. package/dist/lib/messages/inject/subagent-results.d.ts +4 -0
  60. package/dist/lib/messages/inject/subagent-results.d.ts.map +1 -0
  61. package/dist/lib/messages/inject/utils.d.ts +23 -0
  62. package/dist/lib/messages/inject/utils.d.ts.map +1 -0
  63. package/dist/lib/messages/prune.d.ts +4 -0
  64. package/dist/lib/messages/prune.d.ts.map +1 -0
  65. package/dist/lib/messages/query.d.ts +7 -0
  66. package/dist/lib/messages/query.d.ts.map +1 -0
  67. package/dist/lib/messages/reasoning-strip.d.ts +8 -0
  68. package/dist/lib/messages/reasoning-strip.d.ts.map +1 -0
  69. package/dist/lib/messages/shape.d.ts +5 -0
  70. package/dist/lib/messages/shape.d.ts.map +1 -0
  71. package/dist/lib/messages/sync.d.ts +4 -0
  72. package/dist/lib/messages/sync.d.ts.map +1 -0
  73. package/dist/lib/messages/utils.d.ts +27 -0
  74. package/dist/lib/messages/utils.d.ts.map +1 -0
  75. package/dist/lib/prompts/compress-rc.d.ts +4 -0
  76. package/dist/lib/prompts/compress-rc.d.ts.map +1 -0
  77. package/dist/lib/prompts/context-limit-nudge.d.ts +2 -0
  78. package/dist/lib/prompts/context-limit-nudge.d.ts.map +1 -0
  79. package/dist/lib/prompts/extensions/nudge.d.ts +4 -0
  80. package/dist/lib/prompts/extensions/nudge.d.ts.map +1 -0
  81. package/dist/lib/prompts/extensions/system.d.ts +4 -0
  82. package/dist/lib/prompts/extensions/system.d.ts.map +1 -0
  83. package/dist/lib/prompts/extensions/tool.d.ts +3 -0
  84. package/dist/lib/prompts/extensions/tool.d.ts.map +1 -0
  85. package/dist/lib/prompts/index.d.ts +4 -0
  86. package/dist/lib/prompts/index.d.ts.map +1 -0
  87. package/dist/lib/prompts/iteration-nudge.d.ts +2 -0
  88. package/dist/lib/prompts/iteration-nudge.d.ts.map +1 -0
  89. package/dist/lib/prompts/store.d.ts +26 -0
  90. package/dist/lib/prompts/store.d.ts.map +1 -0
  91. package/dist/lib/prompts/system.d.ts +4 -0
  92. package/dist/lib/prompts/system.d.ts.map +1 -0
  93. package/dist/lib/prompts/turn-nudge.d.ts +2 -0
  94. package/dist/lib/prompts/turn-nudge.d.ts.map +1 -0
  95. package/dist/lib/protected-patterns.d.ts +6 -0
  96. package/dist/lib/protected-patterns.d.ts.map +1 -0
  97. package/dist/lib/state/index.d.ts +5 -0
  98. package/dist/lib/state/index.d.ts.map +1 -0
  99. package/dist/lib/state/persistence.d.ts +47 -0
  100. package/dist/lib/state/persistence.d.ts.map +1 -0
  101. package/dist/lib/state/state.d.ts +9 -0
  102. package/dist/lib/state/state.d.ts.map +1 -0
  103. package/dist/lib/state/tool-cache.d.ts +13 -0
  104. package/dist/lib/state/tool-cache.d.ts.map +1 -0
  105. package/dist/lib/state/types.d.ts +105 -0
  106. package/dist/lib/state/types.d.ts.map +1 -0
  107. package/dist/lib/state/utils.d.ts +22 -0
  108. package/dist/lib/state/utils.d.ts.map +1 -0
  109. package/dist/lib/strategies/deduplication.d.ts +10 -0
  110. package/dist/lib/strategies/deduplication.d.ts.map +1 -0
  111. package/dist/lib/strategies/index.d.ts +3 -0
  112. package/dist/lib/strategies/index.d.ts.map +1 -0
  113. package/dist/lib/strategies/purge-errors.d.ts +13 -0
  114. package/dist/lib/strategies/purge-errors.d.ts.map +1 -0
  115. package/dist/lib/subagents/subagent-results.d.ts +5 -0
  116. package/dist/lib/subagents/subagent-results.d.ts.map +1 -0
  117. package/dist/lib/token-utils.d.ts +19 -0
  118. package/dist/lib/token-utils.d.ts.map +1 -0
  119. package/dist/lib/tui/dialogs.d.ts +29 -0
  120. package/dist/lib/tui/dialogs.d.ts.map +1 -0
  121. package/dist/lib/tui/format.d.ts +4 -0
  122. package/dist/lib/tui/format.d.ts.map +1 -0
  123. package/dist/lib/tui/types.d.ts +18 -0
  124. package/dist/lib/tui/types.d.ts.map +1 -0
  125. package/dist/lib/tui/ui.d.ts +48 -0
  126. package/dist/lib/tui/ui.d.ts.map +1 -0
  127. package/dist/lib/ui/notification.d.ts +17 -0
  128. package/dist/lib/ui/notification.d.ts.map +1 -0
  129. package/dist/lib/ui/utils.d.ts +10 -0
  130. package/dist/lib/ui/utils.d.ts.map +1 -0
  131. package/dist/lib/v2/index.d.ts +5 -0
  132. package/dist/lib/v2/index.d.ts.map +1 -0
  133. package/dist/lib/v2/messages.d.ts +13 -0
  134. package/dist/lib/v2/messages.d.ts.map +1 -0
  135. package/dist/lib/v2/rpc.d.ts +75 -0
  136. package/dist/lib/v2/rpc.d.ts.map +1 -0
  137. package/dist/lib/v2/theme.d.ts +55 -0
  138. package/dist/lib/v2/theme.d.ts.map +1 -0
  139. package/dist/lib/v2/tui.d.ts +4 -0
  140. package/dist/lib/v2/tui.d.ts.map +1 -0
  141. package/dist/lib/v2/usage.d.ts +44 -0
  142. package/dist/lib/v2/usage.d.ts.map +1 -0
  143. package/dist/tui.d.ts +7 -0
  144. package/dist/tui.d.ts.map +1 -0
  145. package/lib/commands/compression-targets.ts +54 -0
  146. package/lib/commands/context.ts +305 -0
  147. package/lib/commands/decompress.ts +273 -0
  148. package/lib/commands/help.ts +76 -0
  149. package/lib/commands/index.ts +11 -0
  150. package/lib/commands/manual.ts +126 -0
  151. package/lib/commands/recompress.ts +222 -0
  152. package/lib/commands/stats.ts +180 -0
  153. package/lib/commands/sweep.ts +266 -0
  154. package/lib/compress/index.ts +2 -0
  155. package/lib/compress/pipeline.ts +116 -0
  156. package/lib/compress/protected-content.ts +163 -0
  157. package/lib/compress/rc.ts +494 -0
  158. package/lib/compress/search.ts +277 -0
  159. package/lib/compress/state.ts +272 -0
  160. package/lib/compress/summary.ts +320 -0
  161. package/lib/compress/timing.ts +77 -0
  162. package/lib/compress/types.ts +70 -0
  163. package/lib/compress/usage.ts +168 -0
  164. package/lib/compress-permission.ts +9 -0
  165. package/lib/config.ts +995 -0
  166. package/lib/hooks.ts +205 -0
  167. package/lib/logger.ts +226 -0
  168. package/lib/message-ids.ts +181 -0
  169. package/lib/messages/index.ts +7 -0
  170. package/lib/messages/inject/inject.ts +215 -0
  171. package/lib/messages/inject/subagent-results.ts +84 -0
  172. package/lib/messages/inject/utils.ts +304 -0
  173. package/lib/messages/prune.ts +238 -0
  174. package/lib/messages/query.ts +71 -0
  175. package/lib/messages/reasoning-strip.ts +40 -0
  176. package/lib/messages/shape.ts +50 -0
  177. package/lib/messages/sync.ts +124 -0
  178. package/lib/messages/utils.ts +190 -0
  179. package/lib/prompts/compress-rc.ts +25 -0
  180. package/lib/prompts/context-limit-nudge.ts +18 -0
  181. package/lib/prompts/extensions/nudge.ts +34 -0
  182. package/lib/prompts/extensions/system.ts +32 -0
  183. package/lib/prompts/extensions/tool.ts +21 -0
  184. package/lib/prompts/index.ts +29 -0
  185. package/lib/prompts/iteration-nudge.ts +6 -0
  186. package/lib/prompts/store.ts +452 -0
  187. package/lib/prompts/system.ts +41 -0
  188. package/lib/prompts/turn-nudge.ts +10 -0
  189. package/lib/protected-patterns.ts +159 -0
  190. package/lib/state/index.ts +4 -0
  191. package/lib/state/persistence.ts +321 -0
  192. package/lib/state/state.ts +214 -0
  193. package/lib/state/tool-cache.ts +99 -0
  194. package/lib/state/types.ts +117 -0
  195. package/lib/state/utils.ts +345 -0
  196. package/lib/strategies/deduplication.ts +125 -0
  197. package/lib/strategies/index.ts +2 -0
  198. package/lib/strategies/purge-errors.ts +86 -0
  199. package/lib/subagents/subagent-results.ts +75 -0
  200. package/lib/token-utils.ts +164 -0
  201. package/lib/tui/dialogs.tsx +261 -0
  202. package/lib/tui/format.ts +25 -0
  203. package/lib/tui/types.ts +25 -0
  204. package/lib/tui/ui.tsx +245 -0
  205. package/lib/ui/notification.ts +347 -0
  206. package/lib/ui/utils.ts +304 -0
  207. package/lib/v2/index.ts +448 -0
  208. package/lib/v2/messages.ts +252 -0
  209. package/lib/v2/rpc.ts +60 -0
  210. package/lib/v2/theme.ts +67 -0
  211. package/lib/v2/tui.tsx +115 -0
  212. package/lib/v2/usage.ts +423 -0
  213. package/package.json +91 -5
  214. package/server.js +1 -0
  215. package/tui.tsx +7 -0
@@ -0,0 +1,423 @@
1
+ /**
2
+ * Captures token usage of the hidden compression (summary) requests.
3
+ *
4
+ * OpenCode V2 `session.generate` returns only the reply text, so provider
5
+ * usage is observed in two complementary ways: wrapping the resolved language
6
+ * model through the `aisdk.language` hook (AI SDK packages) and reading the
7
+ * raw response body through the session `http.response` hook (native
8
+ * `@opencode/ai` packages, whose model never passes through the core AISDK
9
+ * service). The session `generate` hook records a local input estimate as a
10
+ * fallback for runtimes where neither capture path is available. All state is
11
+ * kept in memory and keyed by the per-request call id embedded in the hidden
12
+ * prompt marker.
13
+ */
14
+
15
+ import type { Plugin } from "@opencode/plugin"
16
+ import type { Logger } from "../logger"
17
+ import { countTokens } from "../token-utils"
18
+ import { extractSummaryCallId } from "../compress/summary"
19
+ import {
20
+ estimatedCompressionUsage,
21
+ normalizeProviderUsage,
22
+ type CompressionUsage,
23
+ } from "../compress/usage"
24
+
25
+ interface PendingUsage {
26
+ provider?: CompressionUsage
27
+ estimatedInput?: number
28
+ }
29
+
30
+ export interface CompressionUsageTracker {
31
+ recordProvider(callId: string, usage: CompressionUsage): void
32
+ recordEstimatedInput(callId: string, inputTokens: number): void
33
+ /** Provider usage when captured, otherwise a tokenizer estimate, otherwise undefined. */
34
+ resolve(callId: string, outputText: string): CompressionUsage | undefined
35
+ discard(callId: string): void
36
+ }
37
+
38
+ export function createCompressionUsageTracker(): CompressionUsageTracker {
39
+ const pending = new Map<string, PendingUsage>()
40
+
41
+ const entry = (callId: string): PendingUsage => {
42
+ let current = pending.get(callId)
43
+ if (!current) {
44
+ current = {}
45
+ pending.set(callId, current)
46
+ }
47
+ return current
48
+ }
49
+
50
+ return {
51
+ recordProvider(callId, usage) {
52
+ entry(callId).provider = usage
53
+ },
54
+ recordEstimatedInput(callId, inputTokens) {
55
+ if (Number.isFinite(inputTokens) && inputTokens > 0) {
56
+ entry(callId).estimatedInput = Math.round(inputTokens)
57
+ }
58
+ },
59
+ resolve(callId, outputText) {
60
+ const current = pending.get(callId)
61
+ if (!current) {
62
+ return undefined
63
+ }
64
+ if (current.provider) {
65
+ return current.provider
66
+ }
67
+ if (current.estimatedInput === undefined) {
68
+ return undefined
69
+ }
70
+ return estimatedCompressionUsage(current.estimatedInput, countTokens(outputText))
71
+ },
72
+ discard(callId) {
73
+ pending.delete(callId)
74
+ },
75
+ }
76
+ }
77
+
78
+ /** Depth-first search of the AI SDK call options for a summary marker. */
79
+ function findSummaryCallId(options: unknown): string | undefined {
80
+ const texts: string[] = []
81
+ collectTexts((options as { prompt?: unknown } | undefined)?.prompt, texts)
82
+ for (const text of texts) {
83
+ const callId = extractSummaryCallId(text)
84
+ if (callId) {
85
+ return callId
86
+ }
87
+ }
88
+ return undefined
89
+ }
90
+
91
+ function collectTexts(value: unknown, out: string[], depth = 0): void {
92
+ if (depth > 8 || out.length > 200) {
93
+ return
94
+ }
95
+ if (typeof value === "string") {
96
+ out.push(value)
97
+ return
98
+ }
99
+ if (Array.isArray(value)) {
100
+ for (const item of value) {
101
+ collectTexts(item, out, depth + 1)
102
+ }
103
+ return
104
+ }
105
+ if (value && typeof value === "object") {
106
+ const record = value as Record<string, unknown>
107
+ if (typeof record.text === "string") {
108
+ out.push(record.text)
109
+ }
110
+ if (typeof record.content === "string") {
111
+ out.push(record.content)
112
+ } else if (Array.isArray(record.content)) {
113
+ collectTexts(record.content, out, depth + 1)
114
+ }
115
+ }
116
+ }
117
+
118
+ function wrapLanguageModel(model: any, tracker: CompressionUsageTracker, logger: Logger): any {
119
+ return new Proxy(model, {
120
+ get(target, property, receiver) {
121
+ if (property === "doStream") {
122
+ return (options: unknown) => trackedStream(target, options, tracker, logger)
123
+ }
124
+ if (property === "doGenerate") {
125
+ return (options: unknown) => trackedGenerate(target, options, tracker, logger)
126
+ }
127
+ return Reflect.get(target, property, receiver)
128
+ },
129
+ })
130
+ }
131
+
132
+ async function trackedStream(
133
+ target: any,
134
+ options: unknown,
135
+ tracker: CompressionUsageTracker,
136
+ logger: Logger,
137
+ ): Promise<any> {
138
+ const result = await target.doStream(options)
139
+ const callId = findSummaryCallId(options)
140
+ logger.debug("Compression usage stream call", { callId: callId ?? null })
141
+ const TransformStreamImpl = (globalThis as any).TransformStream
142
+ if (!callId || typeof result?.stream?.pipeThrough !== "function") {
143
+ return result
144
+ }
145
+ if (typeof TransformStreamImpl !== "function") {
146
+ return result
147
+ }
148
+
149
+ let recorded = false
150
+ const transform = new TransformStreamImpl({
151
+ transform(part: any, controller: any) {
152
+ try {
153
+ if (!recorded && part?.type === "finish" && part.usage) {
154
+ const usage = normalizeProviderUsage(part.usage)
155
+ if (usage) {
156
+ recorded = true
157
+ tracker.recordProvider(callId, usage)
158
+ }
159
+ }
160
+ } catch {
161
+ // Never disturb the model stream because of usage accounting.
162
+ }
163
+ controller.enqueue(part)
164
+ },
165
+ })
166
+ return { ...result, stream: result.stream.pipeThrough(transform) }
167
+ }
168
+
169
+ async function trackedGenerate(
170
+ target: any,
171
+ options: unknown,
172
+ tracker: CompressionUsageTracker,
173
+ logger: Logger,
174
+ ): Promise<any> {
175
+ const result = await target.doGenerate(options)
176
+ const callId = findSummaryCallId(options)
177
+ logger.debug("Compression usage generate call", { callId: callId ?? null })
178
+ if (!callId) {
179
+ return result
180
+ }
181
+ const usage = normalizeProviderUsage(result?.usage)
182
+ if (usage) {
183
+ tracker.recordProvider(callId, usage)
184
+ }
185
+ return result
186
+ }
187
+
188
+ /**
189
+ * Best-effort registration of the language-model wrapper. Runtimes without the
190
+ * `aisdk` hook simply rely on the local estimate fallback.
191
+ */
192
+ export async function installCompressionUsageHook(
193
+ ctx: Plugin.Context,
194
+ tracker: CompressionUsageTracker,
195
+ logger: Logger,
196
+ ): Promise<void> {
197
+ const aisdk = (ctx as any)?.aisdk
198
+ if (!aisdk || typeof aisdk.hook !== "function") {
199
+ logger.debug("aisdk hook unavailable; compression usage will be estimated")
200
+ return
201
+ }
202
+
203
+ try {
204
+ await aisdk.hook("language", (event: any) => {
205
+ try {
206
+ const isModel = (candidate: unknown) =>
207
+ !!candidate && typeof (candidate as any).doStream === "function"
208
+ logger.debug("Compression usage hook event", {
209
+ modelID: event?.model?.modelID ?? event?.model?.id ?? null,
210
+ hasLanguage: isModel(event?.language),
211
+ hasFactory: typeof event?.sdk?.languageModel === "function",
212
+ })
213
+ // Another hook may already have resolved a model; otherwise fall
214
+ // back to the provider SDK factory the core would use itself.
215
+ let model: unknown = isModel(event?.language) ? event.language : undefined
216
+ if (!model && typeof event?.sdk?.languageModel === "function") {
217
+ model = event.sdk.languageModel(
218
+ event?.model?.modelID ?? event?.model?.id,
219
+ ) as unknown
220
+ }
221
+ if (!isModel(model)) {
222
+ return
223
+ }
224
+ event.language = wrapLanguageModel(model, tracker, logger)
225
+ } catch (error: any) {
226
+ logger.debug("Compression usage hook could not wrap language model", {
227
+ error: error?.message,
228
+ })
229
+ }
230
+ })
231
+ logger.debug("Compression usage hook installed")
232
+ } catch (error: any) {
233
+ logger.warn("Compression usage hook registration failed", {
234
+ error: error?.message,
235
+ })
236
+ }
237
+ }
238
+
239
+ interface UsageAccumulator {
240
+ input: number
241
+ output: number
242
+ cacheRead: number
243
+ cacheWrite: number
244
+ reasoning: number
245
+ /** Anthropic reports cache tokens in addition to `input_tokens`. */
246
+ exclusiveInput: boolean
247
+ }
248
+
249
+ function emptyUsageAccumulator(): UsageAccumulator {
250
+ return {
251
+ input: 0,
252
+ output: 0,
253
+ cacheRead: 0,
254
+ cacheWrite: 0,
255
+ reasoning: 0,
256
+ exclusiveInput: false,
257
+ }
258
+ }
259
+
260
+ type UsageField = "input" | "output" | "cacheRead" | "cacheWrite" | "reasoning"
261
+
262
+ function usageField(key: string): UsageField | undefined {
263
+ switch (key) {
264
+ case "input_tokens":
265
+ case "prompt_tokens":
266
+ case "promptTokenCount":
267
+ return "input"
268
+ case "output_tokens":
269
+ case "completion_tokens":
270
+ case "candidatesTokenCount":
271
+ return "output"
272
+ case "cache_read_input_tokens":
273
+ case "cache_creation_input_tokens":
274
+ case "cached_tokens":
275
+ case "cachedContentTokenCount":
276
+ return key === "cache_creation_input_tokens" ? "cacheWrite" : "cacheRead"
277
+ case "reasoning_tokens":
278
+ case "thoughtsTokenCount":
279
+ return "reasoning"
280
+ default:
281
+ return undefined
282
+ }
283
+ }
284
+
285
+ /** Merge every usage-like object found in a provider payload. */
286
+ function scanUsage(value: unknown, acc: UsageAccumulator, depth: number): void {
287
+ if (depth > 8 || !value || typeof value !== "object") {
288
+ return
289
+ }
290
+ if (Array.isArray(value)) {
291
+ for (const item of value) {
292
+ scanUsage(item, acc, depth + 1)
293
+ }
294
+ return
295
+ }
296
+ for (const [key, raw] of Object.entries(value as Record<string, unknown>)) {
297
+ const field = usageField(key)
298
+ if (field && typeof raw === "number" && Number.isFinite(raw)) {
299
+ acc[field] = Math.max(acc[field], raw)
300
+ if (key === "cache_read_input_tokens" || key === "cache_creation_input_tokens") {
301
+ acc.exclusiveInput = true
302
+ }
303
+ continue
304
+ }
305
+ if (raw && typeof raw === "object") {
306
+ scanUsage(raw, acc, depth + 1)
307
+ }
308
+ }
309
+ }
310
+
311
+ function usageFromAccumulator(acc: UsageAccumulator): CompressionUsage | undefined {
312
+ const input = acc.exclusiveInput ? acc.input + acc.cacheRead + acc.cacheWrite : acc.input
313
+ const usage: CompressionUsage = {
314
+ inputTokens: Math.max(0, Math.round(input)),
315
+ outputTokens: Math.max(0, Math.round(acc.output)),
316
+ cacheReadTokens: Math.max(0, Math.round(acc.cacheRead)),
317
+ cacheWriteTokens: Math.max(0, Math.round(acc.cacheWrite)),
318
+ reasoningTokens: Math.max(0, Math.round(acc.reasoning)),
319
+ source: "provider",
320
+ }
321
+ const total =
322
+ usage.inputTokens +
323
+ usage.outputTokens +
324
+ usage.cacheReadTokens +
325
+ usage.cacheWriteTokens +
326
+ usage.reasoningTokens
327
+ return total > 0 ? usage : undefined
328
+ }
329
+
330
+ /**
331
+ * Parse provider usage from a raw model response body. Handles plain JSON
332
+ * bodies and SSE streams from the common provider protocols (OpenAI
333
+ * responses/chat, Anthropic, Google) by merging every usage-like object.
334
+ */
335
+ export function parseProviderUsageFromBody(body: string): CompressionUsage | undefined {
336
+ const acc = emptyUsageAccumulator()
337
+ const trimmed = body.trim()
338
+ let parsedJson = false
339
+ if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
340
+ try {
341
+ scanUsage(JSON.parse(trimmed), acc, 0)
342
+ parsedJson = true
343
+ } catch {
344
+ // Not a single JSON document; treat the body as an SSE stream.
345
+ }
346
+ }
347
+ if (!parsedJson) {
348
+ for (const line of body.split(/\r?\n/)) {
349
+ const match = /^\s*data:\s*(.*)$/.exec(line)
350
+ if (!match) {
351
+ continue
352
+ }
353
+ const payload = match[1].trim()
354
+ if (!payload || payload === "[DONE]") {
355
+ continue
356
+ }
357
+ try {
358
+ scanUsage(JSON.parse(payload), acc, 0)
359
+ } catch {
360
+ // Ignore malformed events.
361
+ }
362
+ }
363
+ }
364
+ return usageFromAccumulator(acc)
365
+ }
366
+
367
+ /**
368
+ * Best-effort capture of provider usage from the raw HTTP response of the
369
+ * hidden summary request. Needed for native `@opencode/ai` provider packages,
370
+ * which bypass the core AISDK service entirely. The hook reads clones of the
371
+ * request/response, so the real traffic is never disturbed, and any failure
372
+ * falls back to the local estimate recorded by the `generate` hook.
373
+ */
374
+ export async function installHttpUsageHook(
375
+ ctx: Plugin.Context,
376
+ tracker: CompressionUsageTracker,
377
+ logger: Logger,
378
+ ): Promise<void> {
379
+ const session = (ctx as any)?.session
380
+ if (!session || typeof session.hook !== "function") {
381
+ return
382
+ }
383
+
384
+ try {
385
+ await session.hook("http.response", async (event: any) => {
386
+ if (event?.kind !== "generate") {
387
+ return
388
+ }
389
+ try {
390
+ const request = event.request
391
+ const response = event.response
392
+ if (!request || typeof request.clone !== "function") {
393
+ return
394
+ }
395
+ const callId = extractSummaryCallId(await request.clone().text())
396
+ if (!callId) {
397
+ return
398
+ }
399
+ if (!response || typeof response.clone !== "function") {
400
+ return
401
+ }
402
+ const usage = parseProviderUsageFromBody(await response.clone().text())
403
+ if (usage) {
404
+ tracker.recordProvider(callId, usage)
405
+ logger.debug("Compression usage captured from http response", {
406
+ callId,
407
+ inputTokens: usage.inputTokens,
408
+ outputTokens: usage.outputTokens,
409
+ })
410
+ }
411
+ } catch (error: any) {
412
+ logger.debug("Compression usage http capture failed", {
413
+ error: error?.message,
414
+ })
415
+ }
416
+ })
417
+ logger.debug("Compression usage http hook installed")
418
+ } catch (error: any) {
419
+ logger.warn("Compression usage http hook registration failed", {
420
+ error: error?.message,
421
+ })
422
+ }
423
+ }
package/package.json CHANGED
@@ -1,6 +1,92 @@
1
1
  {
2
- "name": "@royalcat/opencode-dcp-rc",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
2
+ "$schema": "https://json.schemastore.org/package.json",
3
+ "name": "@royalcat/opencode-dcp-rc",
4
+ "version": "4.0.1",
5
+ "type": "module",
6
+ "workspaces": [
7
+ "tests/logger"
8
+ ],
9
+ "description": "OpenCode DCP fork: hidden context compression with a selection-only compress tool (Sleev-style)",
10
+ "main": "./dist/index.js",
11
+ "types": "./dist/index.d.ts",
12
+ "exports": {
13
+ ".": {
14
+ "types": "./dist/index.d.ts",
15
+ "import": "./dist/index.js"
16
+ },
17
+ "./server": {
18
+ "types": "./dist/index.d.ts",
19
+ "import": "./dist/index.js"
20
+ },
21
+ "./tui": {
22
+ "types": "./dist/tui.d.ts",
23
+ "import": "./tui.tsx"
24
+ }
25
+ },
26
+ "scripts": {
27
+ "clean": "rm -rf dist",
28
+ "build": "npm run clean && tsup && tsc --emitDeclarationOnly",
29
+ "verify:package": "node scripts/verify-package.mjs",
30
+ "check:package": "npm run build && npm run verify:package",
31
+ "prepublishOnly": "npm run check:package",
32
+ "dev": "opencode plugin dev",
33
+ "typecheck": "tsc --noEmit && npm run typecheck --workspace=tests/logger",
34
+ "test": "node --import tsx --test tests/*.test.ts && npm test --workspace=tests/logger",
35
+ "format": "prettier --write .",
36
+ "format:check": "prettier --check .",
37
+ "dcp": "tsx scripts/print.ts",
38
+ "lab:rc": "node scripts/lab-rc.mjs"
39
+ },
40
+ "keywords": [
41
+ "opencode",
42
+ "opencode-plugin",
43
+ "plugin",
44
+ "context",
45
+ "pruning",
46
+ "optimization",
47
+ "tokens"
48
+ ],
49
+ "repository": {
50
+ "type": "git",
51
+ "url": "git+https://github.com/royalcat/opencode-dcp-rc.git"
52
+ },
53
+ "bugs": {
54
+ "url": "https://github.com/royalcat/opencode-dcp-rc/issues"
55
+ },
56
+ "homepage": "https://github.com/royalcat/opencode-dcp-rc#readme",
57
+ "author": "royalcat",
58
+ "license": "AGPL-3.0-or-later",
59
+ "peerDependencies": {
60
+ "@opencode-ai/plugin": ">=1.18.29"
61
+ },
62
+ "dependencies": {
63
+ "@anthropic-ai/tokenizer": "^0.0.4",
64
+ "@opencode-ai/sdk": "^1.4.3",
65
+ "@opentui/core": "^0.5.14",
66
+ "@opentui/solid": "^0.5.14",
67
+ "jsonc-parser": "^3.3.1",
68
+ "solid-js": "^1.9.12"
69
+ },
70
+ "devDependencies": {
71
+ "@opencode-ai/plugin": "^1.18.34",
72
+ "@opencode/plugin": "^2.0.22",
73
+ "@opencode/theme": "^2.0.4",
74
+ "@types/node": "^25.5.0",
75
+ "prettier": "^3.8.1",
76
+ "tsup": "^8.5.1",
77
+ "tsx": "^4.21.0",
78
+ "typescript": "^6.0.2"
79
+ },
80
+ "overrides": {
81
+ "@babel/core": "7.29.7",
82
+ "esbuild": "0.28.1"
83
+ },
84
+ "files": [
85
+ "dist/",
86
+ "lib/",
87
+ "server.js",
88
+ "tui.tsx",
89
+ "README.md",
90
+ "LICENSE"
91
+ ]
92
+ }
package/server.js ADDED
@@ -0,0 +1 @@
1
+ export { default } from "./dist/index.js"
package/tui.tsx ADDED
@@ -0,0 +1,7 @@
1
+ import type { Plugin } from "@opencode/plugin/tui"
2
+ import { setup } from "./lib/v2/tui"
3
+
4
+ export default {
5
+ id: "@royalcat/opencode-dcp-rc",
6
+ setup,
7
+ } satisfies Plugin.Definition