From e8c76ea922debe56f715670f431fc5968230b63f Mon Sep 17 00:00:00 2001 From: bendtherules Date: Tue, 11 Aug 2026 12:48:11 +0530 Subject: [PATCH] feat: auto-register plugin-compaction-init slash command Adds a custom OpenCode command that scaffolds the compaction memory file with ## Keep and ## Discard sections of classification rules. The configured memoryFile path is substituted into both the command description and the prompt template, and any arguments passed after the command name are appended as user instructions. --- CHANGELOG.md | 4 ++ README.md | 5 +++ src/index.ts | 50 +++++++++++++++++++++ test/index.test.ts | 107 ++++++++++++++++++++++++++++++++++++++++++++- 4 files changed, 165 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d0b258b..e1ebd5d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 0.3.0 + +- Auto-register a `/plugin-compaction-init` slash command that scaffolds the compaction memory file with `## Keep` and `## Discard` sections of classification rules. User-supplied arguments are appended to extend the defaults. + ## 0.2.4 - Normalize npm package repository metadata. diff --git a/README.md b/README.md index 7fa92ad..0755a9b 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,11 @@ You can also add it manually to your global `opencode.jsonc`: Create `.opencode/compaction.md` in the project when you have project-specific context to preserve. The file is optional. +### /plugin-compaction-init + +- Writes the memory file at the configured `memoryFile` path with a top instruction line and `## Keep` and `## Discard` sections containing classification rules for which messages to preserve or drop. +- **Recommended**: pass user instructions to extend the default rules with concrete topics — specific areas of discussion you want kept or dropped. + ## Options | Option | Default | Description | diff --git a/src/index.ts b/src/index.ts index 2b06710..b3fd736 100644 --- a/src/index.ts +++ b/src/index.ts @@ -17,6 +17,38 @@ const defaultCompletionMarker = "opencode-plugin-compaction-prompt: Custom compaction done."; const skippedCompactionMarker = "opencode-plugin-compaction-prompt: No custom compaction applied."; +const initCommandName = "plugin-compaction-init"; + +/** + * Builds the prompt the auto-registered `/plugin-compaction-init` slash + * command sends to the model. `$ARGUMENTS$` is left unsubstituted so + * OpenCode substitutes it at invocation time. + * + * @param memoryFile Literal path to the memory file, inserted verbatim. + * @returns The full command-body template. + */ +export function buildInitCommandTemplate(memoryFile: string): string { + return `You are creating the file \`${memoryFile}\` (relative to the current project root). It is a meta-classification of which messages to preserve and which to discard during session compaction, not a summary of details. + +Follow this format: + +\`\`\` +Keep all details related to the keep topics below, and discard everything related to the discard topics below. + +## Keep + +## Discard +\`\`\` + +\`## Keep\` contains one bullet per active in-progress topic. Each bullet is a **classification rule** describing how to recognize messages that discuss that topic, including its requirements, design decisions, dependency information, and ongoing discussions needed to continue the work. + +\`## Discard\` contains one bullet per older or already-finished topic. Each bullet is a **classification rule** describing how to recognize messages to drop — features/tasks already shipped, abandoned, or otherwise not relevant to the next session, including any debug loops or repeated fix attempts that have already concluded. Add a final catch-all bullet: + +- Any other older discussions not mentioned above. + +$ARGUMENTS$ +`; +} function asString(value: unknown): string | undefined { if (typeof value !== "string") { @@ -106,6 +138,24 @@ export const CompactionPromptPlugin: Plugin = async ( }; return { + config: async (cfg): Promise => { + if (cfg.command?.[initCommandName]) { + await context.client.app.log({ + body: { + level: "warn", + message: `Skipping registration of "${initCommandName}": already defined in config.`, + service: "opencode-plugin-compaction-prompt", + }, + }); + return; + } + + cfg.command ??= {}; + cfg.command[initCommandName] = { + description: `Create ${memoryFile} [compaction file]`, + template: buildInitCommandTemplate(memoryFile), + }; + }, "experimental.session.compacting": async ( _input, output, diff --git a/test/index.test.ts b/test/index.test.ts index 7b59e05..52a0c4e 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -2,7 +2,10 @@ import { afterEach, describe, expect, test } from "bun:test"; import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"; import os from "node:os"; import path from "node:path"; -import { CompactionPromptPlugin } from "../src/index.js"; +import { + buildInitCommandTemplate, + CompactionPromptPlugin, +} from "../src/index.js"; const temporaryDirectories: string[] = []; @@ -159,3 +162,105 @@ describe("CompactionPromptPlugin", () => { ); }); }); + +describe("buildInitCommandTemplate", () => { + test("substitutes the configured memory file path verbatim", () => { + const template = buildInitCommandTemplate("docs/agent-memory.md"); + + expect(template).toContain("`docs/agent-memory.md`"); + expect(template).not.toContain(".opencode/compaction.md"); + }); + + test("contains the file skeleton and both section headings", () => { + const template = buildInitCommandTemplate(".opencode/compaction.md"); + + expect(template).toContain("```\nKeep all details related to"); + expect(template).toContain("## Keep\n"); + expect(template).toContain("## Discard\n"); + }); + + test("leaves $ARGUMENTS$ unsubstituted for OpenCode to fill in", () => { + const template = buildInitCommandTemplate(".opencode/compaction.md"); + + expect(template.trimEnd().endsWith("$ARGUMENTS$")).toBe(true); + }); +}); + +describe("CompactionPromptPlugin config hook", () => { + test("registers the plugin-compaction-init command by default", async () => { + const context = await createContext(); + const hooks = await CompactionPromptPlugin(context as never); + const cfg: { + command?: Record; + } = {}; + + await hooks.config?.(cfg as never); + + expect(cfg.command?.["plugin-compaction-init"]).toBeDefined(); + expect( + cfg.command?.["plugin-compaction-init"].description.length, + ).toBeGreaterThan(0); + expect(cfg.command?.["plugin-compaction-init"].template).toContain( + ".opencode/compaction.md", + ); + expect(cfg.command?.["plugin-compaction-init"].template).toContain( + "## Keep", + ); + expect(cfg.command?.["plugin-compaction-init"].template).toContain( + "## Discard", + ); + expect(cfg.command?.["plugin-compaction-init"].template).toContain( + "$ARGUMENTS$", + ); + expect(context.logs).toHaveLength(0); + }); + + test("uses the configured memoryFile in the registered template", async () => { + const context = await createContext(); + const hooks = await CompactionPromptPlugin(context as never, { + memoryFile: "notes/agent.md", + }); + const cfg: { + command?: Record; + } = {}; + + await hooks.config?.(cfg as never); + + expect(cfg.command?.["plugin-compaction-init"].template).toContain( + "`notes/agent.md`", + ); + expect(cfg.command?.["plugin-compaction-init"].template).not.toContain( + ".opencode/compaction.md", + ); + }); + + test("skips registration and logs a warning when the command is already defined", async () => { + const context = await createContext(); + const hooks = await CompactionPromptPlugin(context as never); + const userTemplate = "user-provided template body"; + const cfg: { + command?: Record; + } = { + command: { + "plugin-compaction-init": { + description: "user description", + template: userTemplate, + }, + }, + }; + + await hooks.config?.(cfg as never); + + expect(cfg.command?.["plugin-compaction-init"].template).toBe(userTemplate); + expect(context.logs).toHaveLength(1); + expect(context.logs[0]).toMatchObject({ + body: { + level: "warn", + service: "opencode-plugin-compaction-prompt", + }, + }); + expect( + (context.logs[0] as { body?: { message?: string } }).body?.message, + ).toContain("plugin-compaction-init"); + }); +});