Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
0d8147e
WIP: dev stash changes
Tarquinen Jan 24, 2026
cd77e3d
feat: add squash tool and combo prompt variants
Tarquinen Jan 25, 2026
278f862
refactor: use ulid for synthetic message ID generation
Tarquinen Jan 26, 2026
6ce83d6
refactor: move squashSummaries to top-level state
Tarquinen Jan 26, 2026
41a7c08
refactor: move squash utility functions to tools/utils.ts
Tarquinen Jan 26, 2026
adfdd71
fix: add secure mode authentication support
Tarquinen Jan 27, 2026
ca972e0
refactor: append context info to existing assistant messages instead …
Tarquinen Jan 28, 2026
8e57d9d
fix: improve discard/extract robustness and fix missing cache sync (D…
Tarquinen Jan 28, 2026
db08fc3
v1.3.0-beta.1 - Bump beta version
Tarquinen Jan 28, 2026
0e803ea
docs: sync schema and README with implementation of protected tools
Tarquinen Jan 28, 2026
80f1a7e
docs: add ko-fi badge to README
Tarquinen Jan 28, 2026
a39c766
swap readme buttons
Tarquinen Jan 28, 2026
ff132f1
validate extraction is array
Tarquinen Jan 28, 2026
3470c06
improve squash tool error messages to specify which boundary string f…
Tarquinen Jan 28, 2026
d3cbf5d
revert prompt style to dev branch format with semantic improvements
Tarquinen Jan 28, 2026
74cdce5
v1.3.1-beta.0 - Bump version
Tarquinen Jan 28, 2026
286b309
docs: move demo images to assets/images directory
Tarquinen Jan 28, 2026
ce86682
fix: ensure tool count accuracy in context breakdown using unique cal…
Tarquinen Jan 29, 2026
2255691
cleanup
Tarquinen Jan 29, 2026
0a381d8
refactor: inject assistant text parts instead of tool parts
Tarquinen Jan 29, 2026
f963d8a
refactor: hybrid injection strategy for DeepSeek/Kimi models
Tarquinen Jan 29, 2026
e1f5312
injection guide comments
Tarquinen Jan 29, 2026
a74cb8b
refactor: new prompt structure and dx cli
spoons-and-mirrors Jan 29, 2026
0bcec1d
inline tags to avoid unwanted linebreak
spoons-and-mirrors Jan 29, 2026
c645e69
nudge
spoons-and-mirrors Jan 29, 2026
06ce131
gitignore
spoons-and-mirrors Jan 29, 2026
53e2c15
DCP: Distill Compress Prune
spoons-and-mirrors Jan 29, 2026
5863a1d
reorder tool registering
spoons-and-mirrors Jan 29, 2026
3d9e4df
Merge pull request #327 from Opencode-DCP/prompt/refactor
Tarquinen Jan 30, 2026
efead8f
fix: generate .ts from .md at build time for bundler compatibility
Tarquinen Jan 30, 2026
cdb5862
refactor: consolidate cli/ into scripts/
Tarquinen Jan 30, 2026
f123eb3
Fix context summary output to handle cases where only tools or only m…
Tarquinen Jan 30, 2026
2cd85fc
revert: show cooldown for errored tools to prevent loop behavior
Tarquinen Jan 30, 2026
866bdc1
spell check
Tarquinen Jan 30, 2026
d66a6c2
docs: standardize DCP tool order to distill, compress, prune
Tarquinen Jan 30, 2026
22f5848
Rename compress tool config from showSummary to showCompression
Tarquinen Jan 30, 2026
9ad912c
chore: bump version to 1.3.1-beta.2
Tarquinen Jan 30, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ Thumbs.db
# OpenCode
.opencode/

# Generated prompt files (from scripts/generate-prompts.ts)
lib/prompts/*.generated.ts

# Tests (local development only)
tests/
notes/
Expand All @@ -36,3 +39,5 @@ test-update.ts
# Documentation (local development only)
docs/
SCHEMA_NOTES.md

repomix-output.xml
9 changes: 9 additions & 0 deletions .repomixignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
.github/
.logs/
.opencode/
dist/
.repomixignore
repomix-output.xml
bun.lock
package-lock.jsonc
LICENCE
25 changes: 17 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
# Dynamic Context Pruning Plugin

[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/dansmolsky)
[![npm version](https://img.shields.io/npm/v/@tarquinen/opencode-dcp.svg)](https://www.npmjs.com/package/@tarquinen/opencode-dcp)

Automatically reduces token usage in OpenCode by removing obsolete tools from conversation history.

![DCP in action](dcp-demo5.png)
![DCP in action](assets/images/dcp-demo5.png)

## Installation

Expand All @@ -27,9 +28,11 @@ DCP uses multiple tools and strategies to reduce context size:

### Tools

**Discard** — Exposes a `discard` tool that the AI can call to remove completed or noisy tool content from context.
**Distill** — Exposes a `distill` tool that the AI can call to distill valuable context into concise summaries before removing the tool content.

**Extract** — Exposes an `extract` tool that the AI can call to distill valuable context into concise summaries before removing the tool content.
**Compress** — Exposes a `compress` tool that the AI can call to collapse a large section of conversation (messages and tools) into a single summary.

**Prune** — Exposes a `prune` tool that the AI can call to remove completed or noisy tool content from context.

### Strategies

Expand Down Expand Up @@ -57,7 +60,7 @@ DCP uses its own config file:

- Global: `~/.config/opencode/dcp.jsonc` (or `dcp.json`), created automatically on first run
- Custom config directory: `$OPENCODE_CONFIG_DIR/dcp.jsonc` (or `dcp.json`), if `OPENCODE_CONFIG_DIR` is set
- Project: `.opencode/dcp.jsonc` (or `dcp.json`) in your projects `.opencode` directory
- Project: `.opencode/dcp.jsonc` (or `dcp.json`) in your project's `.opencode` directory

<details>
<summary><strong>Default Configuration</strong> (click to expand)</summary>
Expand Down Expand Up @@ -96,15 +99,21 @@ DCP uses its own config file:
"protectedTools": [],
},
// Removes tool content from context without preservation (for completed tasks or noise)
"discard": {
"prune": {
"enabled": true,
},
// Distills key findings into preserved knowledge before removing raw content
"extract": {
"distill": {
"enabled": true,
// Show distillation content as an ignored message notification
"showDistillation": false,
},
// Collapses a range of conversation content into a single summary
"compress": {
"enabled": true,
// Show summary content as an ignored message notification
"showCompression": true,
},
},
// Automatic pruning strategies
"strategies": {
Expand Down Expand Up @@ -143,12 +152,12 @@ DCP provides a `/dcp` slash command:

### Turn Protection

When enabled, turn protection prevents tool outputs from being pruned for a configurable number of message turns. This gives the AI time to reference recent tool outputs before they become prunable. Applies to both `discard` and `extract` tools, as well as automatic strategies.
When enabled, turn protection prevents tool outputs from being pruned for a configurable number of message turns. This gives the AI time to reference recent tool outputs before they become prunable. Applies to both `prune` and `distill` tools, as well as automatic strategies.

### Protected Tools

By default, these tools are always protected from pruning across all strategies:
`task`, `todowrite`, `todoread`, `discard`, `extract`, `batch`, `write`, `edit`
`task`, `todowrite`, `todoread`, `distill`, `compress`, `prune`, `batch`, `write`, `edit`, `plan_enter`, `plan_exit`

The `protectedTools` arrays in each section add to this default list.

Expand Down
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
68 changes: 29 additions & 39 deletions dcp.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -100,46 +100,54 @@
"items": {
"type": "string"
},
"default": [
"task",
"todowrite",
"todoread",
"discard",
"extract",
"batch",
"write",
"edit"
],
"default": [],
"description": "Tool names that should be protected from automatic pruning"
}
}
},
"discard": {
"distill": {
"type": "object",
"description": "Configuration for the discard tool",
"description": "Configuration for the distill tool",
"additionalProperties": false,
"properties": {
"enabled": {
"type": "boolean",
"default": true,
"description": "Enable the discard tool"
"description": "Enable the distill tool"
},
"showDistillation": {
"type": "boolean",
"default": false,
"description": "Show distillation output in the UI"
}
}
},
"extract": {
"compress": {
"type": "object",
"description": "Configuration for the extract tool",
"description": "Configuration for the compress tool",
"additionalProperties": false,
"properties": {
"enabled": {
"type": "boolean",
"default": true,
"description": "Enable the extract tool"
"description": "Enable the compress tool"
},
"showDistillation": {
"showCompression": {
"type": "boolean",
"default": false,
"description": "Show distillation output in the UI"
"default": true,
"description": "Show summary output in the UI"
}
}
},
"prune": {
"type": "object",
"description": "Configuration for the prune tool",
"additionalProperties": false,
"properties": {
"enabled": {
"type": "boolean",
"default": true,
"description": "Enable the prune tool"
}
}
}
Expand All @@ -165,16 +173,7 @@
"items": {
"type": "string"
},
"default": [
"task",
"todowrite",
"todoread",
"discard",
"extract",
"batch",
"write",
"edit"
],
"default": [],
"description": "Tool names excluded from deduplication"
}
}
Expand Down Expand Up @@ -211,16 +210,7 @@
"items": {
"type": "string"
},
"default": [
"task",
"todowrite",
"todoread",
"discard",
"extract",
"batch",
"write",
"edit"
],
"default": [],
"description": "Tool names excluded from error purging"
}
}
Expand Down
30 changes: 23 additions & 7 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@ import type { Plugin } from "@opencode-ai/plugin"
import { getConfig } from "./lib/config"
import { Logger } from "./lib/logger"
import { createSessionState } from "./lib/state"
import { createDiscardTool, createExtractTool } from "./lib/strategies"
import { createPruneTool, createDistillTool, createCompressTool } from "./lib/strategies"
import {
createChatMessageTransformHandler,
createCommandExecuteHandler,
createSystemPromptHandler,
} from "./lib/hooks"
import { configureClientAuth, isSecureMode } from "./lib/auth"

const plugin: Plugin = (async (ctx) => {
const config = getConfig(ctx)
Expand All @@ -19,6 +20,11 @@ const plugin: Plugin = (async (ctx) => {
const logger = new Logger(config.debug)
const state = createSessionState()

if (isSecureMode()) {
configureClientAuth(ctx.client)
// logger.info("Secure mode detected, configured client authentication")
}

logger.info("DCP initialized", {
strategies: config.strategies,
})
Expand Down Expand Up @@ -55,17 +61,26 @@ const plugin: Plugin = (async (ctx) => {
ctx.directory,
),
tool: {
...(config.tools.discard.enabled && {
discard: createDiscardTool({
...(config.tools.distill.enabled && {
distill: createDistillTool({
client: ctx.client,
state,
logger,
config,
workingDirectory: ctx.directory,
}),
}),
...(config.tools.compress.enabled && {
compress: createCompressTool({
client: ctx.client,
state,
logger,
config,
workingDirectory: ctx.directory,
}),
}),
...(config.tools.extract.enabled && {
extract: createExtractTool({
...(config.tools.prune.enabled && {
prune: createPruneTool({
client: ctx.client,
state,
logger,
Expand All @@ -84,8 +99,9 @@ const plugin: Plugin = (async (ctx) => {
}

const toolsToAdd: string[] = []
if (config.tools.discard.enabled) toolsToAdd.push("discard")
if (config.tools.extract.enabled) toolsToAdd.push("extract")
if (config.tools.prune.enabled) toolsToAdd.push("prune")
if (config.tools.distill.enabled) toolsToAdd.push("distill")
if (config.tools.compress.enabled) toolsToAdd.push("compress")

if (toolsToAdd.length > 0) {
const existingPrimaryTools = opencodeConfig.experimental?.primary_tools ?? []
Expand Down
37 changes: 37 additions & 0 deletions lib/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
export function isSecureMode(): boolean {
return !!process.env.OPENCODE_SERVER_PASSWORD
}

export function getAuthorizationHeader(): string | undefined {
const password = process.env.OPENCODE_SERVER_PASSWORD
if (!password) return undefined

const username = process.env.OPENCODE_SERVER_USERNAME ?? "opencode"
// Use Buffer for Node.js base64 encoding (btoa may not be available in all Node versions)
const credentials = Buffer.from(`${username}:${password}`).toString("base64")
return `Basic ${credentials}`
}

export function configureClientAuth(client: any): any {
const authHeader = getAuthorizationHeader()

if (!authHeader) {
return client
}

// The SDK client has an internal client with request interceptors
// Access the underlying client to add the interceptor
const innerClient = client._client || client.client

if (innerClient?.interceptors?.request) {
innerClient.interceptors.request.use((request: Request) => {
// Only add auth header if not already present
if (!request.headers.has("Authorization")) {
request.headers.set("Authorization", authHeader)
}
return request
})
}

return client
}
Loading