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
18 changes: 6 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,15 +71,8 @@ DCP uses its own config file:
"debug": false,
// Notification display: "off", "minimal", or "detailed"
"pruneNotification": "detailed",
// Enable or disable slash commands
"commands": {
"context": {
"enabled": true,
},
"stats": {
"enabled": true,
},
},
// Enable or disable slash commands (/dcp)
"commands": true,
// Protect from pruning for <turns> message turns
"turnProtection": {
"enabled": false,
Expand Down Expand Up @@ -137,10 +130,11 @@ DCP uses its own config file:

### Commands

DCP provides two slash commands for visibility into context usage:
DCP provides a `/dcp` slash command:

- `/dcp-context` — Shows a breakdown of your current session's token usage by category (system, user, assistant, tools, etc.) and how much has been saved through pruning.
- `/dcp-stats` — Shows cumulative pruning statistics across all sessions.
- `/dcp` — Shows available DCP commands
- `/dcp context` — Shows a breakdown of your current session's token usage by category (system, user, assistant, tools, etc.) and how much has been saved through pruning.
- `/dcp stats` — Shows cumulative pruning statistics across all sessions.

### Turn Protection

Expand Down
32 changes: 3 additions & 29 deletions dcp.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,35 +27,9 @@
"description": "Level of notification shown when pruning occurs"
},
"commands": {
"type": "object",
"description": "Enable or disable slash commands",
"additionalProperties": false,
"properties": {
"context": {
"type": "object",
"description": "Configuration for /dcp-context command",
"additionalProperties": false,
"properties": {
"enabled": {
"type": "boolean",
"default": true,
"description": "Enable the /dcp-context command"
}
}
},
"stats": {
"type": "object",
"description": "Configuration for /dcp-stats command",
"additionalProperties": false,
"properties": {
"enabled": {
"type": "boolean",
"default": true,
"description": "Enable the /dcp-stats command"
}
}
}
}
"type": "boolean",
"default": true,
"description": "Enable DCP slash commands (/dcp)"
},
"turnProtection": {
"type": "object",
Expand Down
15 changes: 6 additions & 9 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,16 +68,13 @@ const plugin: Plugin = (async (ctx) => {
}),
},
config: async (opencodeConfig) => {
opencodeConfig.command ??= {}
opencodeConfig.command["dcp-stats"] = {
template: "",
description: "Show DCP pruning statistics",
}
opencodeConfig.command["dcp-context"] = {
template: "",
description: "Show token usage breakdown for current session",
if (config.commands) {
opencodeConfig.command ??= {}
opencodeConfig.command["dcp"] = {
template: "",
description: "Show available DCP commands",
}
}
logger.info("Registered /dcp-stats and /dcp-context commands")

const toolsToAdd: string[] = []
if (config.tools.discard.enabled) toolsToAdd.push("discard")
Expand Down
47 changes: 47 additions & 0 deletions lib/commands/help.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/**
* DCP Help command handler.
* Shows available DCP commands and their descriptions.
*/

import type { Logger } from "../logger"
import type { SessionState, WithParts } from "../state"
import { sendIgnoredMessage } from "../ui/notification"
import { getCurrentParams } from "../strategies/utils"

export interface HelpCommandContext {
client: any
state: SessionState
logger: Logger
sessionId: string
messages: WithParts[]
}

function formatHelpMessage(): string {
const lines: string[] = []

lines.push("╭───────────────────────────────────────────────────────────╮")
lines.push("│ DCP Commands │")
lines.push("╰───────────────────────────────────────────────────────────╯")
lines.push("")
lines.push("Available commands:")
lines.push(" context - Show token usage breakdown for current session")
lines.push(" stats - Show DCP pruning statistics")
lines.push("")
lines.push("Examples:")
lines.push(" /dcp context")
lines.push(" /dcp stats")
lines.push("")

return lines.join("\n")
}

export async function handleHelpCommand(ctx: HelpCommandContext): Promise<void> {
const { client, state, logger, sessionId, messages } = ctx

const message = formatHelpMessage()

const params = getCurrentParams(state, messages, logger)
await sendIgnoredMessage(client, sessionId, message, params, logger)

logger.info("Help command executed")
}
78 changes: 14 additions & 64 deletions lib/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,20 +45,11 @@ export interface TurnProtection {
turns: number
}

export interface CommandConfig {
enabled: boolean
}

export interface Commands {
context: CommandConfig
stats: CommandConfig
}

export interface PluginConfig {
enabled: boolean
debug: boolean
pruneNotification: "off" | "minimal" | "detailed"
commands: Commands
commands: boolean
turnProtection: TurnProtection
protectedFilePatterns: string[]
tools: Tools
Expand Down Expand Up @@ -95,10 +86,6 @@ export const VALID_CONFIG_KEYS = new Set([
"turnProtection.turns",
"protectedFilePatterns",
"commands",
"commands.context",
"commands.context.enabled",
"commands.stats",
"commands.stats.enabled",
"tools",
"tools.settings",
"tools.settings.nudgeEnabled",
Expand Down Expand Up @@ -211,26 +198,14 @@ function validateConfigTypes(config: Record<string, any>): ValidationError[] {
}
}

// Commands validators
// Commands validator
const commands = config.commands
if (commands) {
if (
commands.context?.enabled !== undefined &&
typeof commands.context.enabled !== "boolean"
) {
errors.push({
key: "commands.context.enabled",
expected: "boolean",
actual: typeof commands.context.enabled,
})
}
if (commands.stats?.enabled !== undefined && typeof commands.stats.enabled !== "boolean") {
errors.push({
key: "commands.stats.enabled",
expected: "boolean",
actual: typeof commands.stats.enabled,
})
}
if (commands !== undefined && typeof commands !== "boolean") {
errors.push({
key: "commands",
expected: "boolean",
actual: typeof commands,
})
}

// Tools validators
Expand Down Expand Up @@ -425,14 +400,7 @@ const defaultConfig: PluginConfig = {
enabled: true,
debug: false,
pruneNotification: "detailed",
commands: {
context: {
enabled: true,
},
stats: {
enabled: true,
},
},
commands: true,
turnProtection: {
enabled: false,
turns: 4,
Expand Down Expand Up @@ -543,15 +511,8 @@ function createDefaultConfig(): void {
"debug": false,
// Notification display: "off", "minimal", or "detailed"
"pruneNotification": "detailed",
// Enable or disable slash commands
"commands": {
"context": {
"enabled": true
},
"stats": {
"enabled": true
}
},
// Enable or disable slash commands (/dcp)
"commands": true,
// Protect from pruning for <turns> message turns
"turnProtection": {
"enabled": false,
Expand Down Expand Up @@ -695,25 +656,14 @@ function mergeCommands(
base: PluginConfig["commands"],
override?: Partial<PluginConfig["commands"]>,
): PluginConfig["commands"] {
if (!override) return base

return {
context: {
enabled: override.context?.enabled ?? base.context.enabled,
},
stats: {
enabled: override.stats?.enabled ?? base.stats.enabled,
},
}
if (override === undefined) return base
return override as boolean
}

function deepCloneConfig(config: PluginConfig): PluginConfig {
return {
...config,
commands: {
context: { ...config.commands.context },
stats: { ...config.commands.stats },
},
commands: config.commands,
turnProtection: { ...config.turnProtection },
protectedFilePatterns: [...config.protectedFilePatterns],
tools: {
Expand Down
56 changes: 34 additions & 22 deletions lib/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { checkSession } from "./state"
import { loadPrompt } from "./prompts"
import { handleStatsCommand } from "./commands/stats"
import { handleContextCommand } from "./commands/context"
import { handleHelpCommand } from "./commands/help"

const INTERNAL_AGENT_SIGNATURES = [
"You are a title generator",
Expand Down Expand Up @@ -89,39 +90,50 @@ export function createCommandExecuteHandler(
input: { command: string; sessionID: string; arguments: string },
_output: { parts: any[] },
) => {
if (input.command === "dcp-stats") {
if (!config.commands.stats.enabled) {
return
}
const messagesResponse = await client.session.messages({
path: { id: input.sessionID },
})
const messages = (messagesResponse.data || messagesResponse) as WithParts[]
await handleStatsCommand({
client,
state,
logger,
sessionId: input.sessionID,
messages,
})
throw new Error("__DCP_STATS_HANDLED__")
if (!config.commands) {
return
}
if (input.command === "dcp-context") {
if (!config.commands.context.enabled) {
return
}

if (input.command === "dcp") {
const args = (input.arguments || "").trim().split(/\s+/).filter(Boolean)
const subcommand = args[0]?.toLowerCase() || ""
const _subArgs = args.slice(1)

const messagesResponse = await client.session.messages({
path: { id: input.sessionID },
})
const messages = (messagesResponse.data || messagesResponse) as WithParts[]
await handleContextCommand({

if (subcommand === "context") {
await handleContextCommand({
client,
state,
logger,
sessionId: input.sessionID,
messages,
})
throw new Error("__DCP_CONTEXT_HANDLED__")
}

if (subcommand === "stats") {
await handleStatsCommand({
client,
state,
logger,
sessionId: input.sessionID,
messages,
})
throw new Error("__DCP_STATS_HANDLED__")
}

await handleHelpCommand({
client,
state,
logger,
sessionId: input.sessionID,
messages,
})
throw new Error("__DCP_CONTEXT_HANDLED__")
throw new Error("__DCP_HELP_HANDLED__")
}
}
}