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
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <turns> message turns
"turnProtection": {
"enabled": false,
Expand Down Expand Up @@ -126,6 +135,13 @@ DCP uses its own config file:

</details>

### 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.
Expand Down
31 changes: 31 additions & 0 deletions dcp.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
4 changes: 2 additions & 2 deletions lib/commands/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down
77 changes: 77 additions & 0 deletions lib/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -196,6 +211,28 @@ function validateConfigTypes(config: Record<string, any>): 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) {
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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 <turns> message turns
"turnProtection": {
"enabled": false,
Expand Down Expand Up @@ -637,9 +691,29 @@ function mergeTools(
}
}

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,
},
}
}

function deepCloneConfig(config: PluginConfig): PluginConfig {
return {
...config,
commands: {
context: { ...config.commands.context },
stats: { ...config.commands.stats },
},
turnProtection: { ...config.turnProtection },
protectedFilePatterns: [...config.protectedFilePatterns],
tools: {
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
13 changes: 12 additions & 1 deletion lib/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
})
Expand All @@ -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 },
})
Expand Down