@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 +54 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +186 -0
- package/dist/tools.d.ts +15 -0
- package/dist/tools.js +52 -0
- package/opencode-dcp-bridge.darwin-arm64.node +0 -0
- package/package.json +60 -0
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
|
package/dist/index.d.ts
ADDED
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 };
|
package/dist/tools.d.ts
ADDED
|
@@ -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
|
+
}
|
|
Binary file
|
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
|
+
}
|