@qdang46/opencode-dcp-plugin 0.2.0

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 ADDED
@@ -0,0 +1,54 @@
1
+ # @qdang46/opencode-dcp-plugin
2
+
3
+ NAPI-RS native Dynamic Context Pruning plugin for [OpenCode](https://github.com/anomalyco/opencode).
4
+
5
+ Automatically reduces token usage in OpenCode sessions by pruning obsolete tool outputs,
6
+ deduplicating repeated calls, purging errored tool inputs, and compressing stale conversation
7
+ content — all powered by a Rust native addon for zero-serialization performance.
8
+
9
+ ## Installation
10
+
11
+ ```bash
12
+ opencode plugin add @qdang46/opencode-dcp-plugin
13
+ ```
14
+
15
+ Or globally:
16
+
17
+ ```bash
18
+ opencode plugin @qdang46/opencode-dcp-plugin@latest --global
19
+ ```
20
+
21
+ ## Features
22
+
23
+ | Feature | Description |
24
+ |---------|-------------|
25
+ | **Message transform** | Automatically prunes, deduplicates, and compresses messages before each LLM request |
26
+ | **System prompt injection** | Appends DCP instructions so the model can use the compress tool |
27
+ | **Compress tool** | LLM-driven compression of stale conversation ranges into technical summaries |
28
+ | **Decompress / Recompress** | Restore or re-activate compressed blocks on demand |
29
+ | **Slash commands** | `/dcp context`, `/dcp stats`, `/dcp sweep`, `/dcp manual`, `/dcp decompress`, `/dcp recompress` |
30
+ | **Config cascade** | JSONC config loaded from 4-tier cascade (builtin → global → custom → project) |
31
+ | **Cache stability** | `agent-message`, `aggressive`, or `manual` modes for prompt cache preservation |
32
+ | **Protected tools** | Glob patterns to keep specific tool outputs from being pruned |
33
+
34
+ ## Configuration
35
+
36
+ DCP loads configuration from `.dynamic_context_pruning/config.jsonc` in your project directory.
37
+ See the [dcp.schema.json](./dcp.schema.json) for the full config schema.
38
+
39
+ ## Development
40
+
41
+ ```bash
42
+ # Build the Rust native addon
43
+ cargo build --release -p opencode-dcp-bridge
44
+
45
+ # Compile TypeScript
46
+ cd opencode-dcp-plugin && npx tsc
47
+
48
+ # Test
49
+ node -e "require('./opencode-dcp-bridge.darwin-arm64.node')"
50
+ ```
51
+
52
+ ## License
53
+
54
+ MIT
@@ -0,0 +1,5 @@
1
+ import type { Plugin } from "@opencode-ai/plugin";
2
+ declare const _default: {
3
+ server: Plugin;
4
+ };
5
+ export default _default;
package/dist/index.js ADDED
@@ -0,0 +1,186 @@
1
+ import { createTools } from "./tools.js";
2
+ import { createRequire } from "module";
3
+ import { fileURLToPath } from "url";
4
+ import { dirname, resolve, join } from "path";
5
+ const __filename = fileURLToPath(import.meta.url);
6
+ const __dirname = dirname(__filename);
7
+ const _require = createRequire(import.meta.url);
8
+ function loadBridge() {
9
+ const root = resolve(__dirname, "..");
10
+ const candidates = [
11
+ join(root, "opencode-dcp-bridge.darwin-arm64.node"),
12
+ join(root, "opencode-dcp-bridge.darwin-x64.node"),
13
+ join(root, "opencode-dcp-bridge.linux-x64-gnu.node"),
14
+ join(root, "opencode-dcp-bridge.win32-x64-msvc.node"),
15
+ ];
16
+ for (const name of candidates) {
17
+ try {
18
+ return _require(name);
19
+ }
20
+ catch {
21
+ /* try next */
22
+ }
23
+ }
24
+ throw new Error("Cannot load opencode-dcp-bridge native addon.\n" +
25
+ "Build: cd ~/Projects/dynamic_context_pruning && cargo build -p opencode-dcp-bridge");
26
+ }
27
+ function formatHelpText() {
28
+ return [
29
+ "╭──────────────────────────────────────────────────────────────╮",
30
+ "│ DCP Commands │",
31
+ "╰──────────────────────────────────────────────────────────────╯",
32
+ "",
33
+ " /dcp Show this help message",
34
+ " /dcp context Show context analysis (turns, blocks, tokens)",
35
+ " /dcp stats Show pruning statistics",
36
+ " /dcp sweep Flush pending prune strategies",
37
+ " /dcp manual <on|off> Toggle manual mode",
38
+ " /dcp decompress <id> Restore a compressed block",
39
+ " /dcp recompress <id> Re-activate a decompressed block",
40
+ " /dcp-compress [focus] Trigger manual compression",
41
+ "",
42
+ " Tools (available to the LLM):",
43
+ " compress Replace stale content with summaries",
44
+ " decompress Restore compressed blocks",
45
+ " recompress Re-activate decompressed blocks",
46
+ "",
47
+ ].join("\n");
48
+ }
49
+ const createPlugin = async (_ctx) => {
50
+ const nativeBridge = loadBridge();
51
+ const configJson = nativeBridge.loadDcpConfig();
52
+ const config = JSON.parse(configJson);
53
+ const pruner = new nativeBridge.DcpPruner(configJson);
54
+ return {
55
+ // ─── Message pipeline ─────────────────────────────────────────
56
+ "experimental.chat.messages.transform": async (_input, output) => {
57
+ try {
58
+ if (!output.messages || output.messages.length === 0)
59
+ return;
60
+ const json = JSON.stringify(output.messages);
61
+ const transformed = pruner.transformMessages(json);
62
+ const parsed = JSON.parse(transformed);
63
+ output.messages.length = 0;
64
+ output.messages.push(...parsed);
65
+ }
66
+ catch (err) {
67
+ console.error("[DCP] transform_messages error:", err);
68
+ }
69
+ },
70
+ // ─── System prompt ────────────────────────────────────────────
71
+ "experimental.chat.system.transform": async (_input, output) => {
72
+ try {
73
+ const joined = output.system.join("\n");
74
+ const result = pruner.transformSystem(joined);
75
+ if (result !== joined) {
76
+ output.system[output.system.length - 1] = result;
77
+ }
78
+ }
79
+ catch (err) {
80
+ console.error("[DCP] transform_system error:", err);
81
+ }
82
+ },
83
+ // ─── Slash commands ──────────────────────────────────────────
84
+ "command.execute.before": async (input, output) => {
85
+ try {
86
+ const parts = input.command.split(/\s+/);
87
+ let cmd = parts[0];
88
+ // Strip leading slash if present (OpenCode passes "/dcp")
89
+ if (cmd.startsWith("/")) {
90
+ cmd = cmd.slice(1);
91
+ }
92
+ if (cmd === "dcp" || cmd === "dcp-compress") {
93
+ const subcommand = parts.length > 1 ? parts[1] : "help";
94
+ const args = parts.slice(2);
95
+ // Handle help in TypeScript (purely presentational)
96
+ if (subcommand === "help") {
97
+ output.parts.length = 0;
98
+ output.parts.push({
99
+ type: "text",
100
+ text: formatHelpText(),
101
+ id: `dcp-${Date.now()}`,
102
+ sessionID: input.sessionID,
103
+ messageID: `cmd-${Date.now()}`,
104
+ synthetic: true,
105
+ });
106
+ return;
107
+ }
108
+ const actualCmd = cmd === "dcp-compress" ? "compress" : subcommand;
109
+ const resultJson = pruner.handleCommand(actualCmd, JSON.stringify(args), "[]");
110
+ const result = JSON.parse(resultJson);
111
+ output.parts.length = 0;
112
+ output.parts.push({
113
+ type: "text",
114
+ text: result.status === "ok"
115
+ ? result.text
116
+ : `⚠️ ${result.text}`,
117
+ id: `dcp-${Date.now()}`,
118
+ sessionID: input.sessionID,
119
+ messageID: `cmd-${Date.now()}`,
120
+ synthetic: true,
121
+ });
122
+ }
123
+ }
124
+ catch (err) {
125
+ console.error("[DCP] command.execute.before error:", err);
126
+ }
127
+ },
128
+ // ─── Event tracking ──────────────────────────────────────────
129
+ event: async (input) => {
130
+ try {
131
+ if (input.event?.type) {
132
+ pruner.notifyEvent(JSON.stringify(input.event));
133
+ }
134
+ }
135
+ catch (err) {
136
+ console.error("[DCP] event error:", err);
137
+ }
138
+ },
139
+ // ─── Config hook: register slash commands + negotiate permissions ─
140
+ config: async (opencodeConfig) => {
141
+ try {
142
+ if (config.compress?.permission !== "deny") {
143
+ opencodeConfig.command ??= {};
144
+ // Register /dcp as a slash command (shows in palette)
145
+ opencodeConfig.command["dcp"] = {
146
+ template: "",
147
+ description: "DCP: context, stats, sweep, manual, decompress, recompress",
148
+ };
149
+ // Register /dcp-compress as a slash command
150
+ opencodeConfig.command["dcp-compress"] = {
151
+ template: "",
152
+ description: "Trigger DCP manual compression with: /dcp-compress [focus]",
153
+ };
154
+ // Add compress as a primary tool so it's always available
155
+ const existingPrimaryTools = opencodeConfig.experimental?.primary_tools ?? [];
156
+ opencodeConfig.experimental = {
157
+ ...opencodeConfig.experimental,
158
+ primary_tools: [...existingPrimaryTools, "compress"],
159
+ };
160
+ // Set compress tool permission to match config
161
+ const permission = opencodeConfig.permission ?? {};
162
+ opencodeConfig.permission = {
163
+ ...permission,
164
+ compress: config.compress?.permission ?? "allow",
165
+ };
166
+ }
167
+ }
168
+ catch (err) {
169
+ console.error("[DCP] config error:", err);
170
+ }
171
+ },
172
+ // ─── Cleanup on plugin reload ────────────────────────────────
173
+ dispose: async () => {
174
+ try {
175
+ pruner.setSessionId("__dispose__");
176
+ pruner.notifyEvent(JSON.stringify({ type: "plugin.dispose" }));
177
+ }
178
+ catch (err) {
179
+ console.error("[DCP] dispose error:", err);
180
+ }
181
+ },
182
+ // ─── Tools exposed to the LLM ────────────────────────────────
183
+ tool: createTools(pruner),
184
+ };
185
+ };
186
+ export default { server: createPlugin };
@@ -0,0 +1,15 @@
1
+ import { type ToolDefinition } from "@opencode-ai/plugin";
2
+ interface DcpPruner {
3
+ transformMessages(messagesJson: string): string;
4
+ transformSystem(system: string): string;
5
+ handleCompress(argsJson: string, messagesJson: string): string;
6
+ decompress(blockId: number): string;
7
+ recompress(blockId: number): string;
8
+ handleCommand(cmd: string, argsJson: string, messagesJson: string): string;
9
+ notifyEvent(eventJson: string): void;
10
+ hasPendingWork(): boolean;
11
+ stats(): string;
12
+ setSessionId(sessionId: string): void;
13
+ }
14
+ export declare function createTools(pruner: DcpPruner): Record<string, ToolDefinition>;
15
+ export {};
package/dist/tools.js ADDED
@@ -0,0 +1,52 @@
1
+ import { tool } from "@opencode-ai/plugin";
2
+ export function createTools(pruner) {
3
+ return {
4
+ compress: tool({
5
+ description: "Replace stale conversation content with technical summaries. " +
6
+ "Use for closed/discussed topics to free context space.",
7
+ args: {
8
+ topic: tool.schema
9
+ .string()
10
+ .describe("Short label (3-5 words) for the batch - e.g. 'Auth Exploration'"),
11
+ content: tool.schema
12
+ .array(tool.schema.object({
13
+ startId: tool.schema
14
+ .string()
15
+ .describe("Message or block ID beginning of range (e.g. m0001, b2)"),
16
+ endId: tool.schema
17
+ .string()
18
+ .describe("Message or block ID end of range (e.g. m0012, b5)"),
19
+ summary: tool.schema
20
+ .string()
21
+ .describe("Complete technical summary replacing all content in range"),
22
+ }))
23
+ .describe("One or more ranges to compress"),
24
+ },
25
+ async execute(args) {
26
+ const resultJson = pruner.handleCompress(JSON.stringify(args), "[]");
27
+ const result = JSON.parse(resultJson);
28
+ return `Compressed ${result.blocks?.length || result.compressed_count || 0} messages.`;
29
+ },
30
+ }),
31
+ decompress: tool({
32
+ description: "Restore a compressed block to its original messages.",
33
+ args: {
34
+ blockId: tool.schema.number().describe("Block ID to restore (e.g. 1, 2, 3)"),
35
+ },
36
+ async execute(args) {
37
+ pruner.decompress(args.blockId);
38
+ return `Decompressed block ${args.blockId}.`;
39
+ },
40
+ }),
41
+ recompress: tool({
42
+ description: "Re-activate a user-decompressed block for future compression.",
43
+ args: {
44
+ blockId: tool.schema.number().describe("Block ID to re-compress (e.g. 1, 2, 3)"),
45
+ },
46
+ async execute(args) {
47
+ pruner.recompress(args.blockId);
48
+ return `Recompressed block ${args.blockId}.`;
49
+ },
50
+ }),
51
+ };
52
+ }
package/package.json ADDED
@@ -0,0 +1,60 @@
1
+ {
2
+ "name": "@qdang46/opencode-dcp-plugin",
3
+ "version": "0.2.0",
4
+ "private": false,
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "napi": {
9
+ "name": "opencode-dcp-bridge",
10
+ "triples": {
11
+ "defaults": true
12
+ }
13
+ },
14
+ "scripts": {
15
+ "build": "cd .. && cargo build -p opencode-dcp-bridge && cp target/debug/libopencode_dcp_bridge.dylib opencode-dcp-plugin/opencode-dcp-bridge.darwin-arm64.node",
16
+ "build:release": "cd .. && cargo build --release -p opencode-dcp-bridge && cp target/release/libopencode_dcp_bridge.dylib opencode-dcp-plugin/opencode-dcp-bridge.darwin-arm64.node",
17
+ "typecheck": "tsc --noEmit",
18
+ "clean": "rm -rf dist && rm -f *.node",
19
+ "prepublishOnly": "npm run build:release && npx tsc",
20
+ "postpublish": "echo \"Published! Install with: opencode plugin add @qdang46/opencode-dcp-plugin\""
21
+ },
22
+ "peerDependencies": {
23
+ "@opencode-ai/plugin": ">=1.4.3"
24
+ },
25
+ "devDependencies": {
26
+ "@opencode-ai/plugin": "^1.17.9",
27
+ "@types/node": "^26.0.0",
28
+ "typescript": "^5.9.3",
29
+ "zod": "^3.25.76"
30
+ },
31
+ "files": [
32
+ "dist/",
33
+ "opencode-dcp-bridge.*.node",
34
+ "npm/",
35
+ "README.md"
36
+ ],
37
+ "description": "NAPI-RS native OpenCode plugin for Dynamic Context Pruning \u2014 intelligently manages conversation context to optimize token usage",
38
+ "keywords": [
39
+ "opencode",
40
+ "opencode-plugin",
41
+ "plugin",
42
+ "context",
43
+ "pruning",
44
+ "dcp",
45
+ "tokens"
46
+ ],
47
+ "license": "MIT",
48
+ "homepage": "https://github.com/quangdang46/dynamic_context_pruning",
49
+ "bugs": {
50
+ "url": "https://github.com/quangdang46/dynamic_context_pruning/issues"
51
+ },
52
+ "repository": {
53
+ "type": "git",
54
+ "url": "git+https://github.com/quangdang46/dynamic_context_pruning.git",
55
+ "directory": "opencode-dcp-plugin"
56
+ },
57
+ "engines": {
58
+ "node": ">=18"
59
+ }
60
+ }