mirror of
https://github.com/bendtherules/opencode-plugin-compaction-prompt.git
synced 2026-08-18 21:52:46 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
658f30e38c | ||
|
|
9f549acfde | ||
|
|
387457ea94 | ||
|
|
e8c76ea922 | ||
|
|
edb5bb47f4 | ||
|
|
57c96fcd3b | ||
|
|
2fe522b99e | ||
|
|
3d26f61a2b | ||
|
|
55db9835a2 |
@@ -1,5 +1,21 @@
|
||||
# Changelog
|
||||
|
||||
## 0.3.1
|
||||
|
||||
- Fix the OpenCode `$ARGUMENTS` token in the `plugin-compaction-init` template so it is properly substituted instead of being written into the memory file.
|
||||
|
||||
## 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.
|
||||
|
||||
## 0.2.3
|
||||
|
||||
- Always ask the model to report whether custom compaction instructions were applied.
|
||||
|
||||
## 0.2.2
|
||||
|
||||
- Add explicit markers for applied and skipped custom compaction instructions.
|
||||
|
||||
@@ -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 <instructions>
|
||||
|
||||
- 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 |
|
||||
@@ -47,7 +52,7 @@ Create `.opencode/compaction.md` in the project when you have project-specific c
|
||||
|
||||
Append mode is the recommended default because it preserves OpenCode's built-in compaction behavior. Replace mode is available when the complete prompt needs to be controlled by this plugin.
|
||||
|
||||
When neither a prompt nor a memory file is available, the plugin adds `opencode-plugin-compaction-prompt: No custom compaction applied.` instead.
|
||||
When neither a prompt nor a memory file is available, the plugin asks the model to echo `opencode-plugin-compaction-prompt: No custom compaction applied.` instead.
|
||||
|
||||
## Development
|
||||
|
||||
|
||||
+2
-2
@@ -1,13 +1,13 @@
|
||||
{
|
||||
"name": "opencode-plugin-compaction-prompt",
|
||||
"version": "0.2.2",
|
||||
"version": "0.3.1",
|
||||
"description": "Customize OpenCode's compaction prompt to preserve important context and omit unnecessary details.",
|
||||
"type": "module",
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/bendtherules/opencode-plugin-compaction-prompt.git"
|
||||
"url": "git+https://github.com/bendtherules/opencode-plugin-compaction-prompt.git"
|
||||
},
|
||||
"homepage": "https://github.com/bendtherules/opencode-plugin-compaction-prompt#readme",
|
||||
"bugs": {
|
||||
|
||||
+54
-16
@@ -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") {
|
||||
@@ -31,11 +63,7 @@ function buildInstructions(
|
||||
prompt: string,
|
||||
completionMarker: string,
|
||||
memory: string,
|
||||
): string | undefined {
|
||||
if (!prompt && !memory) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
): string {
|
||||
const sections = [
|
||||
"## User Compaction Instructions",
|
||||
`These instructions take precedence over previous instructions if there is conflict. At the very end of the summary, echo exactly: **${completionMarker}**`,
|
||||
@@ -110,22 +138,32 @@ export const CompactionPromptPlugin: Plugin = async (
|
||||
};
|
||||
|
||||
return {
|
||||
config: async (cfg): Promise<void> => {
|
||||
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,
|
||||
): Promise<void> => {
|
||||
const memory = await readMemory(memoryPath, logError);
|
||||
const instructions = buildInstructions(prompt, completionMarker, memory);
|
||||
|
||||
if (!instructions) {
|
||||
if (mode === "replace") {
|
||||
output.prompt = skippedCompactionMarker;
|
||||
} else {
|
||||
output.context.push(skippedCompactionMarker);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
const marker =
|
||||
prompt || memory ? completionMarker : skippedCompactionMarker;
|
||||
const instructions = buildInstructions(prompt, marker, memory);
|
||||
|
||||
if (mode === "replace") {
|
||||
output.prompt = instructions;
|
||||
|
||||
+116
-11
@@ -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[] = [];
|
||||
|
||||
@@ -116,9 +119,9 @@ describe("CompactionPromptPlugin", () => {
|
||||
);
|
||||
|
||||
expect(context.logs).toHaveLength(0);
|
||||
expect(output.context).toEqual([
|
||||
"opencode-plugin-compaction-prompt: No custom compaction applied.",
|
||||
]);
|
||||
expect(output.context[0]).toContain(
|
||||
"echo exactly: **opencode-plugin-compaction-prompt: No custom compaction applied.**",
|
||||
);
|
||||
});
|
||||
|
||||
test("adds a skipped marker when no instructions are provided", async () => {
|
||||
@@ -133,10 +136,10 @@ describe("CompactionPromptPlugin", () => {
|
||||
output,
|
||||
);
|
||||
|
||||
expect(output.context).toEqual([
|
||||
"Existing context",
|
||||
"opencode-plugin-compaction-prompt: No custom compaction applied.",
|
||||
]);
|
||||
expect(output.context[0]).toBe("Existing context");
|
||||
expect(output.context[1]).toContain(
|
||||
"echo exactly: **opencode-plugin-compaction-prompt: No custom compaction applied.**",
|
||||
);
|
||||
expect(output.prompt).toBeUndefined();
|
||||
});
|
||||
|
||||
@@ -154,8 +157,110 @@ describe("CompactionPromptPlugin", () => {
|
||||
);
|
||||
|
||||
expect(context.logs).toHaveLength(1);
|
||||
expect(output.context).toEqual([
|
||||
"opencode-plugin-compaction-prompt: No custom compaction applied.",
|
||||
]);
|
||||
expect(output.context[0]).toContain(
|
||||
"echo exactly: **opencode-plugin-compaction-prompt: No custom compaction applied.**",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
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<string, { description: string; template: string }>;
|
||||
} = {};
|
||||
|
||||
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<string, { description: string; template: string }>;
|
||||
} = {};
|
||||
|
||||
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<string, { description: string; template: string }>;
|
||||
} = {
|
||||
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");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user