Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
15 changes: 8 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

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

![DCP in action](dcp-demo3.png)
![DCP in action](dcp-demo.png)

## Installation

Expand All @@ -13,7 +13,7 @@ Add to your OpenCode config:
```jsonc
// opencode.jsonc
{
"plugin": ["@tarquinen/opencode-dcp"],
"plugin": ["@tarquinen/opencode-dcp@0.4.2"],
"experimental": {
"primary_tools": ["prune"]
}
Expand All @@ -22,7 +22,9 @@ Add to your OpenCode config:

The `experimental.primary_tools` setting ensures the `prune` tool is only available to the primary agent (not subagents).

DCP automatically checks for new versions in the background. You'll see a toast notification when an update is available. To enable automatic background updates, set `"autoUpdate": true` in your DCP config.
When a new version is available, DCP will show a toast notification. Update by changing the version number in your config.

> **Note:** Using `@latest` (e.g. `@tarquinen/opencode-dcp@latest`) does not reliably force the latest update in Opencode. Please use specific version numbers (e.g. `@0.4.2`).

Restart OpenCode. The plugin will automatically start optimizing your sessions.

Expand Down Expand Up @@ -63,15 +65,14 @@ DCP uses its own config file (`~/.config/opencode/dcp.jsonc` or `.opencode/dcp.j
| `model` | (session) | Model for analysis (e.g., `"anthropic/claude-haiku-4-5"`) |
| `showModelErrorToasts` | `true` | Show notifications on model fallback |
| `showUpdateToasts` | `true` | Show notifications when a new version is available |
| `autoUpdate` | `false` | Automatically download new versions (restart to apply) |
| `strictModelSelection` | `false` | Only run AI analysis with session or configured model (disables fallback models) |
| `pruning_summary` | `"detailed"` | `"off"`, `"minimal"`, or `"detailed"` |
| `nudge_freq` | `10` | How often to remind AI to prune (lower = more frequent) |
| `protectedTools` | `["task", "todowrite", "todoread", "prune", "batch", "edit", "write"]` | Tools that are never pruned |
| `protectedTools` | `["task", "todowrite", "todoread", "prune"]` | Tools that are never pruned |
| `strategies.onIdle` | `["ai-analysis"]` | Strategies for automatic pruning |
| `strategies.onTool` | `["ai-analysis"]` | Strategies when AI calls `prune` |

**Strategies:** `"ai-analysis"` uses LLM to identify prunable outputs. Empty array disables that trigger. Deduplication always runs automatically. More strategies coming soon.
**Strategies:** `"ai-analysis"` uses LLM to identify prunable outputs. Empty array disables that trigger. Deduplication runs automatically on every request.

```jsonc
{
Expand All @@ -80,7 +81,7 @@ DCP uses its own config file (`~/.config/opencode/dcp.jsonc` or `.opencode/dcp.j
"onIdle": ["ai-analysis"],
"onTool": ["ai-analysis"]
},
"protectedTools": ["task", "todowrite", "todoread", "prune", "batch", "edit", "write"]
"protectedTools": ["task", "todowrite", "todoread", "prune"]
}
```

Expand Down
5 changes: 1 addition & 4 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,7 @@ const plugin: Plugin = (async (ctx) => {

// Check for updates after a delay
setTimeout(() => {
checkForUpdates(ctx.client, logger, {
showToast: config.showUpdateToasts ?? true,
autoUpdate: config.autoUpdate ?? false
}).catch(() => { })
checkForUpdates(ctx.client, logger, config.showUpdateToasts ?? true).catch(() => { })
}, 5000)

// Show migration toast if there were config migrations
Expand Down
9 changes: 1 addition & 8 deletions lib/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ export interface PluginConfig {
model?: string
showModelErrorToasts?: boolean
showUpdateToasts?: boolean
autoUpdate?: boolean
strictModelSelection?: boolean
pruning_summary: "off" | "minimal" | "detailed"
nudge_freq: number
Expand All @@ -32,10 +31,9 @@ export interface ConfigResult {
const defaultConfig: PluginConfig = {
enabled: true,
debug: false,
protectedTools: ['task', 'todowrite', 'todoread', 'prune', 'batch', 'edit', 'write'],
protectedTools: ['task', 'todowrite', 'todoread', 'prune', 'batch'],
showModelErrorToasts: true,
showUpdateToasts: true,
autoUpdate: false,
strictModelSelection: false,
pruning_summary: 'detailed',
nudge_freq: 10,
Expand All @@ -52,7 +50,6 @@ const VALID_CONFIG_KEYS = new Set([
'model',
'showModelErrorToasts',
'showUpdateToasts',
'autoUpdate',
'strictModelSelection',
'pruning_summary',
'nudge_freq',
Expand Down Expand Up @@ -118,8 +115,6 @@ function createDefaultConfig(): void {
"showModelErrorToasts": true,
// Show toast notifications when a new version is available
"showUpdateToasts": true,
// Automatically update to new versions (restart required to apply)
"autoUpdate": false,
// Only run AI analysis with session model or configured model (disables fallback models)
"strictModelSelection": false,
// AI analysis strategies (deduplication runs automatically on every request)
Expand Down Expand Up @@ -210,7 +205,6 @@ export function getConfig(ctx?: PluginInput): ConfigResult {
model: globalConfig.model ?? config.model,
showModelErrorToasts: globalConfig.showModelErrorToasts ?? config.showModelErrorToasts,
showUpdateToasts: globalConfig.showUpdateToasts ?? config.showUpdateToasts,
autoUpdate: globalConfig.autoUpdate ?? config.autoUpdate,
strictModelSelection: globalConfig.strictModelSelection ?? config.strictModelSelection,
strategies: mergeStrategies(config.strategies, globalConfig.strategies as any),
pruning_summary: globalConfig.pruning_summary ?? config.pruning_summary,
Expand Down Expand Up @@ -243,7 +237,6 @@ export function getConfig(ctx?: PluginInput): ConfigResult {
model: projectConfig.model ?? config.model,
showModelErrorToasts: projectConfig.showModelErrorToasts ?? config.showModelErrorToasts,
showUpdateToasts: projectConfig.showUpdateToasts ?? config.showUpdateToasts,
autoUpdate: projectConfig.autoUpdate ?? config.autoUpdate,
strictModelSelection: projectConfig.strictModelSelection ?? config.strictModelSelection,
strategies: mergeStrategies(config.strategies, projectConfig.strategies as any),
pruning_summary: projectConfig.pruning_summary ?? config.pruning_summary,
Expand Down
118 changes: 16 additions & 102 deletions lib/version-checker.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { readFileSync, writeFileSync, existsSync } from 'fs'
import { readFileSync } from 'fs'
import { join, dirname } from 'path'
import { fileURLToPath } from 'url'
import { homedir } from 'os'

export const PACKAGE_NAME = '@tarquinen/opencode-dcp'
export const NPM_REGISTRY_URL = `https://registry.npmjs.org/${PACKAGE_NAME}/latest`
Expand All @@ -11,20 +10,9 @@ const __dirname = dirname(__filename)

export function getLocalVersion(): string {
try {
let dir = __dirname
for (let i = 0; i < 5; i++) {
const pkgPath = join(dir, 'package.json')
try {
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'))
if (pkg.name === PACKAGE_NAME) {
return pkg.version
}
} catch {
// Not found at this level, go up
}
dir = join(dir, '..')
}
return '0.0.0'
const pkgPath = join(__dirname, '../../package.json')
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'))
return pkg.version
} catch {
return '0.0.0'
}
Expand Down Expand Up @@ -62,58 +50,7 @@ export function isOutdated(local: string, remote: string): boolean {
return false
}

/**
* Updates config files to pin the new version.
* Checks both global and local project configs.
* Handles: "@tarquinen/opencode-dcp", "@tarquinen/opencode-dcp@latest", "@tarquinen/[email protected]"
*/
export function updateConfigVersion(newVersion: string, logger?: { info: (component: string, message: string, data?: any) => void }): boolean {
const configs = [
join(homedir(), '.config', 'opencode', 'opencode.jsonc'), // Global
join(process.cwd(), '.opencode', 'opencode.jsonc') // Local project
]

let anyUpdated = false

for (const configPath of configs) {
try {
if (!existsSync(configPath)) continue

const content = readFileSync(configPath, 'utf-8')

// Match @tarquinen/opencode-dcp with optional version suffix (latest, 1.2.3, etc)
// The regex matches: " @tarquinen/opencode-dcp (optional @anything) "
const regex = new RegExp(`"${PACKAGE_NAME}(@[^"]*)?"`,'g')
const newEntry = `"${PACKAGE_NAME}@${newVersion}"`

if (!regex.test(content)) {
continue
}

// Reset regex state
regex.lastIndex = 0
const updatedContent = content.replace(regex, newEntry)

if (updatedContent !== content) {
writeFileSync(configPath, updatedContent, 'utf-8')
logger?.info("version", "Config updated", { configPath, newVersion })
anyUpdated = true
}
} catch (err) {
logger?.info("version", "Failed to update config", { configPath, error: (err as Error).message })
}
}

return anyUpdated
}

export async function checkForUpdates(
client: any,
logger?: { info: (component: string, message: string, data?: any) => void },
options: { showToast?: boolean; autoUpdate?: boolean } = {}
): Promise<void> {
const { showToast = true, autoUpdate = false } = options

export async function checkForUpdates(client: any, logger?: { info: (component: string, message: string, data?: any) => void }, showToast: boolean = true): Promise<void> {
try {
const local = getLocalVersion()
const npm = await getNpmVersion()
Expand All @@ -128,43 +65,20 @@ export async function checkForUpdates(
return
}

logger?.info("version", "Update available", { local, npm, autoUpdate })
logger?.info("version", "Update available", { local, npm })

if (autoUpdate) {
// Attempt config update
const updated = updateConfigVersion(npm, logger)
if (!showToast) {
return
}

if (updated && showToast) {
await client.tui.showToast({
body: {
title: "DCP: Updated!",
message: `v${local} → v${npm}\nRestart OpenCode to apply`,
variant: "success",
duration: 6000
}
})
} else if (!updated && showToast) {
// Config update failed or plugin not found in config, show manual instructions
await client.tui.showToast({
body: {
title: "DCP: Update available",
message: `v${local} → v${npm}\nUpdate opencode.jsonc:\n"${PACKAGE_NAME}@${npm}"`,
variant: "info",
duration: 8000
}
})
await client.tui.showToast({
body: {
title: "DCP: Update available",
message: `v${local} → v${npm}\nUpdate opencode.jsonc: ${PACKAGE_NAME}@${npm}`,
variant: "info",
duration: 6000
}
} else if (showToast) {
await client.tui.showToast({
body: {
title: "DCP: Update available",
message: `v${local} → v${npm}\nUpdate opencode.jsonc:\n"${PACKAGE_NAME}@${npm}"`,
variant: "info",
duration: 8000
}
})
}
})
} catch {
// Silently fail version checks
}
}