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
67 changes: 67 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
name: Publish

on:
workflow_dispatch:
push:
branches:
- master

permissions:
contents: read
id-token: write

concurrency:
group: publish-${{ github.ref }}
cancel-in-progress: false

jobs:
npm:
name: Publish to npm
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "22.x"
cache: npm
registry-url: https://registry.npmjs.org

- name: Check npm version
id: package
run: |
NAME=$(node -p "require('./package.json').name")
VERSION=$(node -p "require('./package.json').version")
echo "name=${NAME}" >> "$GITHUB_OUTPUT"
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"

if npm view "${NAME}@${VERSION}" version >/dev/null 2>&1; then
echo "published=true" >> "$GITHUB_OUTPUT"
echo "${NAME}@${VERSION} is already published."
else
echo "published=false" >> "$GITHUB_OUTPUT"
echo "${NAME}@${VERSION} is not published yet."
fi

- name: Install dependencies
if: steps.package.outputs.published == 'false'
run: npm ci

- name: Format check
if: steps.package.outputs.published == 'false'
run: npm run format:check

- name: Type check
if: steps.package.outputs.published == 'false'
run: npm run typecheck

- name: Test
if: steps.package.outputs.published == 'false'
run: npm test

- name: Publish
if: steps.package.outputs.published == 'false'
run: npx --yes --package npm@11 npm publish --access public --provenance
3 changes: 1 addition & 2 deletions .npmignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,8 @@ ANALYSIS.md
docs/
notes/

# Source files (since we're shipping dist/)
# Server source file (server runtime ships dist/)
index.ts
lib/

# Git
.git/
Expand Down
23 changes: 14 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,17 @@ opencode plugin @tarquinen/opencode-dcp@latest --global

This installs the package and adds it to your global OpenCode config.

## Project Status

Development on DCP has slowed because most new context-management work has moved to [Sleev](https://sleev.ai) and the `sleev` CLI. Sleev is a local proxy for Claude Code, Codex, and OpenCode that builds on DCP's core ideas with newer context-management features and will work with any harness/client.

DCP remains available for OpenCode plugin users, but new features are landing in Sleev first. If you are starting fresh, we recommend trying Sleev:

```bash
npm i -g sleev
sleev
```

## How It Works

DCP reduces context size through a compress tool and automatic cleanup. Your session history is never modified — DCP replaces pruned content with placeholders before sending requests to your LLM.
Expand Down Expand Up @@ -176,16 +187,10 @@ Each level overrides the previous, so project settings take priority over global

### Commands

DCP provides a `/dcp` slash command:
DCP provides a TUI panel and one prompt-producing slash command:

- `/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.
- `/dcp sweep` — Prunes all tools since the last user message. Accepts an optional count: `/dcp sweep 10` prunes the last 10 tools. Respects `commands.protectedTools`.
- `/dcp manual [on|off]` — Toggle manual mode or set explicit state. When on, the AI will not autonomously use context management tools.
- `/dcp compress [focus]` — Trigger a single compress tool execution. Optional focus text directs what content to compress, following the active `compress.mode`.
- `/dcp decompress <n>` — Restore a specific active compression by ID (for example `/dcp decompress 2`). Running without an argument shows available compression IDs, token sizes, and topics.
- `/dcp recompress <n>` — Re-apply a user-decompressed compression by ID (for example `/dcp recompress 2`). Running without an argument shows recompressible IDs, token sizes, and topics.
- `/dcp` — Opens the DCP panel with context, stats, and manual-mode controls.
- `/dcp-compress [focus]` — Asks the model to run one compression pass. Optional focus text directs what content to compress, following the active `compress.mode`.

### Prompt Overrides

Expand Down
4 changes: 2 additions & 2 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,9 +96,9 @@ const server: Plugin = (async (ctx) => {

if (config.commands.enabled && config.compress.permission !== "deny") {
opencodeConfig.command ??= {}
opencodeConfig.command["dcp"] = {
opencodeConfig.command["dcp-compress"] = {
template: "",
description: "Show available DCP commands",
description: "Trigger DCP manual compression with: /dcp-compress [focus]",
}
}

Expand Down
6 changes: 3 additions & 3 deletions lib/commands/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ interface TokenBreakdown {
total: number
}

function analyzeTokens(state: SessionState, messages: WithParts[]): TokenBreakdown {
export function analyzeContextTokens(state: SessionState, messages: WithParts[]): TokenBreakdown {
const breakdown: TokenBreakdown = {
system: 0,
user: 0,
Expand Down Expand Up @@ -235,7 +235,7 @@ function createBar(value: number, maxValue: number, width: number, char: string
return bar
}

function formatContextMessage(breakdown: TokenBreakdown): string {
export function formatContextMessage(breakdown: TokenBreakdown): string {
const lines: string[] = []
const barWidth = 30

Expand Down Expand Up @@ -296,7 +296,7 @@ function formatContextMessage(breakdown: TokenBreakdown): string {
export async function handleContextCommand(ctx: ContextCommandContext): Promise<void> {
const { client, state, logger, sessionId, messages } = ctx

const breakdown = analyzeTokens(state, messages)
const breakdown = analyzeContextTokens(state, messages)

const message = formatContextMessage(breakdown)

Expand Down
20 changes: 10 additions & 10 deletions lib/commands/help.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,32 +19,29 @@ export interface HelpCommandContext {
messages: WithParts[]
}

const BASE_COMMANDS: [string, string][] = [
["/dcp context", "Show token usage breakdown for current session"],
["/dcp stats", "Show DCP pruning statistics"],
["/dcp sweep [n]", "Prune tools since last user message, or last n tools"],
["/dcp manual [on|off]", "Toggle manual mode or set explicit state"],
const TUI_COMMANDS: [string, string][] = [
["DCP Context", "Show token usage breakdown for current session"],
["DCP Stats", "Show DCP pruning statistics"],
["DCP Help", "Show this help in a modal"],
]

const TOOL_COMMANDS: Record<string, [string, string]> = {
compress: ["/dcp compress [focus]", "Trigger manual compress tool execution"],
compress: ["/dcp-compress [focus]", "Trigger manual compress tool execution"],
decompress: ["/dcp decompress <n>", "Restore selected compression"],
recompress: ["/dcp recompress <n>", "Re-apply a user-decompressed compression"],
}

function getVisibleCommands(state: SessionState, config: PluginConfig): [string, string][] {
const commands = [...BASE_COMMANDS]
const commands = [...TUI_COMMANDS]

if (compressPermission(state, config) !== "deny") {
commands.push(TOOL_COMMANDS.compress)
commands.push(TOOL_COMMANDS.decompress)
commands.push(TOOL_COMMANDS.recompress)
}

return commands
}

function formatHelpMessage(state: SessionState, config: PluginConfig): string {
export function formatHelpMessage(state: SessionState, config: PluginConfig): string {
const commands = getVisibleCommands(state, config)
const colWidth = Math.max(...commands.map(([cmd]) => cmd.length)) + 4
const lines: string[] = []
Expand All @@ -55,6 +52,9 @@ function formatHelpMessage(state: SessionState, config: PluginConfig): string {
lines.push("")
lines.push(` ${"Manual mode:".padEnd(colWidth)}${state.manualMode ? "ON" : "OFF"}`)
lines.push("")
lines.push(" Open the command palette for DCP modal commands.")
lines.push(" Use /dcp-compress [focus] when you want DCP to ask the model to run compression.")
lines.push("")
for (const [cmd, desc] of commands) {
lines.push(` ${cmd.padEnd(colWidth)}${desc}`)
}
Expand Down
6 changes: 4 additions & 2 deletions lib/commands/manual.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,19 @@
*
* Usage:
* /dcp manual [on|off] - Toggle manual mode or set explicit state
* /dcp compress [focus] - Trigger manual compress execution
* /dcp-compress [focus] - Trigger manual compress execution
*/

import type { Logger } from "../logger"
import type { SessionState, WithParts } from "../state"
import type { PluginConfig } from "../config"
import { sendIgnoredMessage } from "../ui/notification"
import { saveManualModeSetting } from "../state/persistence"
import { getCurrentParams } from "../token-utils"
import { buildCompressedBlockGuidance } from "../prompts/extensions/nudge"
import { isIgnoredUserMessage } from "../messages/query"

const MANUAL_MODE_ON = "Manual mode is now ON. Use /dcp compress to trigger context tools manually."
const MANUAL_MODE_ON = "Manual mode is now ON. Use /dcp-compress to trigger context tools manually."

const MANUAL_MODE_OFF = "Manual mode is now OFF."

Expand Down Expand Up @@ -76,6 +77,7 @@ export async function handleManualToggleCommand(
params,
logger,
)
await saveManualModeSetting(sessionId, !!state.manualMode, logger)

logger.info("Manual mode toggled", { manualMode: state.manualMode })
}
Expand Down
47 changes: 29 additions & 18 deletions lib/commands/stats.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export interface StatsCommandContext {
messages: WithParts[]
}

function formatStatsMessage(
export function formatStatsMessage(
sessionTokens: number,
sessionSummaryTokens: number,
sessionTools: number,
Expand Down Expand Up @@ -92,7 +92,32 @@ function formatCompressionTime(ms: number): string {
export async function handleStatsCommand(ctx: StatsCommandContext): Promise<void> {
const { client, state, logger, sessionId, messages } = ctx

// Session stats from in-memory state
const report = await buildStatsReport(state, logger)
const message = formatStatsMessage(
report.sessionTokens,
report.sessionSummaryTokens,
report.sessionTools,
report.sessionMessages,
report.sessionDurationMs,
report.allTime,
)

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

logger.info("Stats command executed", {
sessionTokens: report.sessionTokens,
sessionSummaryTokens: report.sessionSummaryTokens,
sessionTools: report.sessionTools,
sessionMessages: report.sessionMessages,
sessionDurationMs: report.sessionDurationMs,
allTimeTokens: report.allTime.totalTokens,
allTimeTools: report.allTime.totalTools,
allTimeMessages: report.allTime.totalMessages,
})
}

export async function buildStatsReport(state: SessionState, logger: Logger) {
const sessionTokens = state.stats.totalPruneTokens
const sessionSummaryTokens = Array.from(state.prune.messages.blocksById.values()).reduce(
(total, block) => (block.active ? total + block.summaryTokens : total),
Expand Down Expand Up @@ -123,26 +148,12 @@ export async function handleStatsCommand(ctx: StatsCommandContext): Promise<void
// All-time stats from storage files
const allTime = await loadAllSessionStats(logger)

const message = formatStatsMessage(
return {
sessionTokens,
sessionSummaryTokens,
sessionTools,
sessionMessages,
sessionDurationMs,
allTime,
)

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

logger.info("Stats command executed", {
sessionTokens,
sessionSummaryTokens,
sessionTools,
sessionMessages,
sessionDurationMs,
allTimeTokens: allTime.totalTokens,
allTimeTools: allTime.totalTools,
allTimeMessages: allTime.totalMessages,
})
}
}
4 changes: 3 additions & 1 deletion lib/compress/pipeline.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { WithParts } from "../state"
import { ensureSessionInitialized } from "../state"
import { ensureSessionInitialized, refreshManualMode } from "../state"
import { saveSessionState } from "../state/persistence"
import { assignMessageRefs } from "../message-ids"
import { isIgnoredUserMessage } from "../messages/query"
Expand Down Expand Up @@ -39,6 +39,8 @@ export async function prepareSession(
toolCtx: RunContext,
title: string,
): Promise<PreparedSession> {
await refreshManualMode(ctx.state, toolCtx.sessionID, ctx.logger, ctx.config.manualMode.enabled)

if (ctx.state.manualMode && ctx.state.manualMode !== "compress-pending") {
throw new Error(
"Manual mode: compress blocked. Do not retry until `<compress triggered manually>` appears in user context.",
Expand Down
Loading
Loading