From 8ea93cc13f5c3eb79fde22b75425b17e3331a8e9 Mon Sep 17 00:00:00 2001 From: Daniel Smolsky Date: Tue, 20 Jan 2026 00:14:40 -0500 Subject: [PATCH] refactor: unify commands into single /dcp with subcommands - Replace /dcp-stats and /dcp-context with /dcp stats and /dcp context - Simplify commands config from { enabled: true } to just boolean - Add help screen shown when /dcp is run without arguments - Make argument parsing future-proof for commands with multiple args --- README.md | 18 ++++------ dcp.schema.json | 32 ++---------------- index.ts | 15 ++++----- lib/commands/help.ts | 47 ++++++++++++++++++++++++++ lib/config.ts | 78 ++++++++------------------------------------ lib/hooks.ts | 56 ++++++++++++++++++------------- 6 files changed, 110 insertions(+), 136 deletions(-) create mode 100644 lib/commands/help.ts diff --git a/README.md b/README.md index 36ba46d..c4b09bc 100644 --- a/README.md +++ b/README.md @@ -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 message turns "turnProtection": { "enabled": false, @@ -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 diff --git a/dcp.schema.json b/dcp.schema.json index 94a98f9..6e0b01a 100644 --- a/dcp.schema.json +++ b/dcp.schema.json @@ -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", diff --git a/index.ts b/index.ts index 46ec69b..6b39ed5 100644 --- a/index.ts +++ b/index.ts @@ -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") diff --git a/lib/commands/help.ts b/lib/commands/help.ts new file mode 100644 index 0000000..56d4316 --- /dev/null +++ b/lib/commands/help.ts @@ -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 { + 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") +} diff --git a/lib/config.ts b/lib/config.ts index f96710a..1547dad 100644 --- a/lib/config.ts +++ b/lib/config.ts @@ -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 @@ -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", @@ -211,26 +198,14 @@ function validateConfigTypes(config: Record): 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 @@ -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, @@ -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 message turns "turnProtection": { "enabled": false, @@ -695,25 +656,14 @@ function mergeCommands( base: PluginConfig["commands"], override?: Partial, ): 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: { diff --git a/lib/hooks.ts b/lib/hooks.ts index d42a515..0533e37 100644 --- a/lib/hooks.ts +++ b/lib/hooks.ts @@ -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", @@ -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__") } } }