diff --git a/README.md b/README.md index 4261867..36ba46d 100644 --- a/README.md +++ b/README.md @@ -71,6 +71,15 @@ 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, + }, + }, // Protect from pruning for message turns "turnProtection": { "enabled": false, @@ -126,6 +135,13 @@ DCP uses its own config file: +### Commands + +DCP provides two slash commands for visibility into context usage: + +- `/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 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. diff --git a/dcp.schema.json b/dcp.schema.json index 39823f2..94a98f9 100644 --- a/dcp.schema.json +++ b/dcp.schema.json @@ -26,6 +26,37 @@ "default": "detailed", "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" + } + } + } + } + }, "turnProtection": { "type": "object", "description": "Protect recent tool outputs from being pruned", diff --git a/index.ts b/index.ts index b047850..46ec69b 100644 --- a/index.ts +++ b/index.ts @@ -94,7 +94,7 @@ const plugin: Plugin = (async (ctx) => { ) } }, - "command.execute.before": createCommandExecuteHandler(ctx.client, state, logger), + "command.execute.before": createCommandExecuteHandler(ctx.client, state, logger, config), } }) satisfies Plugin diff --git a/lib/commands/context.ts b/lib/commands/context.ts index a7fba87..a61be14 100644 --- a/lib/commands/context.ts +++ b/lib/commands/context.ts @@ -197,12 +197,12 @@ function formatContextMessage(breakdown: TokenBreakdown): string { let labelWithPct: string let valueStr: string if ("isSaved" in cat && cat.isSaved) { - labelWithPct = cat.label.padEnd(16) + labelWithPct = cat.label.padEnd(17) valueStr = `${formatTokenCount(cat.value).replace(" tokens", "").padStart(6)} saved` } else { const percentage = breakdown.total > 0 ? ((cat.value / breakdown.total) * 100).toFixed(1) : "0.0" - labelWithPct = `${cat.label.padEnd(9)} ${percentage.padStart(5)}%` + labelWithPct = `${cat.label.padEnd(9)} ${percentage.padStart(5)}% ` valueStr = formatTokenCount(cat.value).padStart(13) } diff --git a/lib/config.ts b/lib/config.ts index d1bbba5..f96710a 100644 --- a/lib/config.ts +++ b/lib/config.ts @@ -45,10 +45,20 @@ 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 turnProtection: TurnProtection protectedFilePatterns: string[] tools: Tools @@ -84,6 +94,11 @@ export const VALID_CONFIG_KEYS = new Set([ "turnProtection.enabled", "turnProtection.turns", "protectedFilePatterns", + "commands", + "commands.context", + "commands.context.enabled", + "commands.stats", + "commands.stats.enabled", "tools", "tools.settings", "tools.settings.nudgeEnabled", @@ -196,6 +211,28 @@ function validateConfigTypes(config: Record): ValidationError[] { } } + // Commands validators + 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, + }) + } + } + // Tools validators const tools = config.tools if (tools) { @@ -388,6 +425,14 @@ const defaultConfig: PluginConfig = { enabled: true, debug: false, pruneNotification: "detailed", + commands: { + context: { + enabled: true, + }, + stats: { + enabled: true, + }, + }, turnProtection: { enabled: false, turns: 4, @@ -498,6 +543,15 @@ 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 + } + }, // Protect from pruning for message turns "turnProtection": { "enabled": false, @@ -637,9 +691,29 @@ function mergeTools( } } +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, + }, + } +} + function deepCloneConfig(config: PluginConfig): PluginConfig { return { ...config, + commands: { + context: { ...config.commands.context }, + stats: { ...config.commands.stats }, + }, turnProtection: { ...config.turnProtection }, protectedFilePatterns: [...config.protectedFilePatterns], tools: { @@ -693,6 +767,7 @@ export function getConfig(ctx: PluginInput): PluginConfig { enabled: result.data.enabled ?? config.enabled, debug: result.data.debug ?? config.debug, pruneNotification: result.data.pruneNotification ?? config.pruneNotification, + commands: mergeCommands(config.commands, result.data.commands as any), turnProtection: { enabled: result.data.turnProtection?.enabled ?? config.turnProtection.enabled, turns: result.data.turnProtection?.turns ?? config.turnProtection.turns, @@ -735,6 +810,7 @@ export function getConfig(ctx: PluginInput): PluginConfig { enabled: result.data.enabled ?? config.enabled, debug: result.data.debug ?? config.debug, pruneNotification: result.data.pruneNotification ?? config.pruneNotification, + commands: mergeCommands(config.commands, result.data.commands as any), turnProtection: { enabled: result.data.turnProtection?.enabled ?? config.turnProtection.enabled, turns: result.data.turnProtection?.turns ?? config.turnProtection.turns, @@ -774,6 +850,7 @@ export function getConfig(ctx: PluginInput): PluginConfig { enabled: result.data.enabled ?? config.enabled, debug: result.data.debug ?? config.debug, pruneNotification: result.data.pruneNotification ?? config.pruneNotification, + commands: mergeCommands(config.commands, result.data.commands as any), turnProtection: { enabled: result.data.turnProtection?.enabled ?? config.turnProtection.enabled, turns: result.data.turnProtection?.turns ?? config.turnProtection.turns, diff --git a/lib/hooks.ts b/lib/hooks.ts index ba029bf..d42a515 100644 --- a/lib/hooks.ts +++ b/lib/hooks.ts @@ -79,12 +79,20 @@ export function createChatMessageTransformHandler( } } -export function createCommandExecuteHandler(client: any, state: SessionState, logger: Logger) { +export function createCommandExecuteHandler( + client: any, + state: SessionState, + logger: Logger, + config: PluginConfig, +) { return async ( 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 }, }) @@ -99,6 +107,9 @@ export function createCommandExecuteHandler(client: any, state: SessionState, lo throw new Error("__DCP_STATS_HANDLED__") } if (input.command === "dcp-context") { + if (!config.commands.context.enabled) { + return + } const messagesResponse = await client.session.messages({ path: { id: input.sessionID }, })