mirror of
https://github.com/bendtherules/opencode-plugin-compaction-prompt.git
synced 2026-08-18 13:42:55 +00:00
feat: add compaction prompt plugin
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
verify:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: latest
|
||||
- run: bun install --frozen-lockfile
|
||||
- run: bun test
|
||||
- run: bun run typecheck
|
||||
- run: bun run build
|
||||
- run: bun run pack:check
|
||||
@@ -0,0 +1,3 @@
|
||||
node_modules/
|
||||
dist/
|
||||
.DS_Store
|
||||
@@ -0,0 +1,5 @@
|
||||
# Changelog
|
||||
|
||||
## 0.1.0
|
||||
|
||||
- Initial standalone release.
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,56 @@
|
||||
# opencode-plugin-compaction-prompt
|
||||
|
||||
Customize OpenCode's compaction prompt to guide which context carries forward, which details are omitted, and what the next session should retain.
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
opencode plugin opencode-plugin-compaction-prompt
|
||||
```
|
||||
|
||||
Or add it to `opencode.json` manually:
|
||||
|
||||
```json
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"plugin": [
|
||||
[
|
||||
"opencode-plugin-compaction-prompt",
|
||||
{
|
||||
"memoryFile": ".opencode/compaction.md",
|
||||
"mode": "append",
|
||||
"completionMarker": "Custom compaction request honored."
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Create `.opencode/compaction.md` in the project when you have project-specific context to preserve. The file is optional.
|
||||
|
||||
## Options
|
||||
|
||||
| Option | Default | Description |
|
||||
| ------------------ | ------------------------------------ | ---------------------------------------------------------------------------------------------------------- |
|
||||
| `memoryFile` | `.opencode/compaction.md` | File resolved relative to the active worktree. |
|
||||
| `mode` | `append` | Append instructions to OpenCode's default prompt, or use `replace` to provide a complete prompt. |
|
||||
| `prompt` | Built-in preservation prompt | Additional instructions used together with `memoryFile`; both are included in the compaction instructions. |
|
||||
| `completionMarker` | `Custom compaction request honored.` | Exact text the model is asked to append at the end of the summary. |
|
||||
|
||||
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.
|
||||
|
||||
## Development
|
||||
|
||||
```bash
|
||||
bun install
|
||||
bun test
|
||||
bun run typecheck
|
||||
bun run build
|
||||
bun run pack:check
|
||||
```
|
||||
|
||||
The npm package exposes the compiled entrypoint at `dist/index.js` and TypeScript declarations at `dist/index.d.ts`.
|
||||
|
||||
## Compatibility
|
||||
|
||||
The plugin uses OpenCode's `experimental.session.compacting` hook. OpenCode may change experimental plugin APIs between releases; test the package against the OpenCode version you support.
|
||||
@@ -0,0 +1,93 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"configVersion": 1,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "opencode-plugin-compaction-prompt",
|
||||
"devDependencies": {
|
||||
"@opencode-ai/plugin": "^1.18.10",
|
||||
"@types/bun": "^1.3.14",
|
||||
"@types/node": "^25.6.0",
|
||||
"prettier": "^3.6.2",
|
||||
"typescript": "^6.0.3",
|
||||
},
|
||||
},
|
||||
},
|
||||
"packages": {
|
||||
"@ai-sdk/provider": ["@ai-sdk/provider@3.0.8", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-oGMAgGoQdBXbZqNG0Ze56CHjDZ1IDYOwGYxYjO5KLSlz5HiNQ9udIXsPZ61VWaHGZ5XW/jyjmr6t2xz2jGVwbQ=="],
|
||||
|
||||
"@msgpackr-extract/msgpackr-extract-darwin-arm64": ["@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-LCkGo6JDfaBhgST7UpPWgNgLINpcpabaHfyz5OBx75nUYxBsaEPxjnyNjWpeb/xBup/682QnBfRBy2/LvPutZQ=="],
|
||||
|
||||
"@msgpackr-extract/msgpackr-extract-darwin-x64": ["@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-zExlW9zUJKZH/tOtVMttwjKa4Xm/3KcNjnE3dPN92uCktwavMxpgCA3MoJK/DOnTWsQgo224OaST27/mPNAf+w=="],
|
||||
|
||||
"@msgpackr-extract/msgpackr-extract-linux-arm": ["@msgpackr-extract/msgpackr-extract-linux-arm@3.0.4", "", { "os": "linux", "cpu": "arm" }, "sha512-Tg3yX65f5GbtXLkrYEHE5oibZG9epyYWas7FogTTEJeDEF9JlXJzKgXaNhT3UXlTOeA+AfZpYZYZ0uPj7Cfquw=="],
|
||||
|
||||
"@msgpackr-extract/msgpackr-extract-linux-arm64": ["@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-dgX0P/9wGPJeHFBG+ZmhgE6bmtMt7NP5CRBGyyktpopdk/mW4POnrpQsSLtKI1dwpc+pPLuXHDh6vvskyQE/sw=="],
|
||||
|
||||
"@msgpackr-extract/msgpackr-extract-linux-x64": ["@msgpackr-extract/msgpackr-extract-linux-x64@3.0.4", "", { "os": "linux", "cpu": "x64" }, "sha512-8TNXMEjJc3QEy7R/x1INhgiU+XakDAFUzBhaz7+Rbrs8NH5UQeHQxxmzsSBJGyV6I1jW79undiQm8tOI+D+8FQ=="],
|
||||
|
||||
"@msgpackr-extract/msgpackr-extract-win32-x64": ["@msgpackr-extract/msgpackr-extract-win32-x64@3.0.4", "", { "os": "win32", "cpu": "x64" }, "sha512-CmCXPQrkbwExx3j946/PtHWHbYJiCRBRDl4BlkRQcJB/YOwQxJRTpoo7aTsortjgoJ1x7opzTSxn7C+ASSLVjQ=="],
|
||||
|
||||
"@opencode-ai/plugin": ["@opencode-ai/plugin@1.18.10", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@opencode-ai/sdk": "1.18.10", "effect": "4.0.0-beta.83", "zod": "4.1.8" }, "peerDependencies": { "@opentui/core": ">=0.4.5", "@opentui/keymap": ">=0.4.5", "@opentui/solid": ">=0.4.5" }, "optionalPeers": ["@opentui/core", "@opentui/keymap", "@opentui/solid"] }, "sha512-l4R1ulu5wtA0+cCinzdSPkEO8ZCdIgb/ZjltzstIWQqRqeH3H880OabNaeje/ZCkSoAT8c/o9oT3tdjyFP7o7g=="],
|
||||
|
||||
"@opencode-ai/sdk": ["@opencode-ai/sdk@1.18.10", "", { "dependencies": { "cross-spawn": "7.0.6" } }, "sha512-K+glWbBp5pfGepZ/6EHvuXY3IpVG2qgsEqF4z9mOiUQUrs6KWN72Vdp15Qsl2TCDbemgHWXKQKgbX3b8PSK2hg=="],
|
||||
|
||||
"@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
||||
|
||||
"@types/bun": ["@types/bun@1.3.14", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="],
|
||||
|
||||
"@types/node": ["@types/node@25.9.5", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-OScDchr2fwuUmWdf4kZ9h7PcJiYDVInhJizG/biAq3cAvqwYktuy/TYGGdZNMtNTFUP7rnb0NU4TUdm82kt4Rg=="],
|
||||
|
||||
"bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="],
|
||||
|
||||
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
|
||||
|
||||
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
|
||||
|
||||
"effect": ["effect@4.0.0-beta.83", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.8.0", "find-my-way-ts": "^0.1.6", "ini": "^7.0.0", "kubernetes-types": "^1.30.0", "msgpackr": "^2.0.1", "multipasta": "^0.2.7", "toml": "^4.1.1", "uuid": "^14.0.0", "yaml": "^2.9.0" } }, "sha512-0wsak8RtgGAr9UWSbVDgJHZcUqMSvicHcvaZv1MbMM7MCGgW4Rn/137J1MHQbwYPcwYGxT/IqehFd+UbYuj78w=="],
|
||||
|
||||
"fast-check": ["fast-check@4.9.0", "", { "dependencies": { "pure-rand": "^8.0.0" } }, "sha512-7ms6T7SybUev/PQITciI0yLM2pOSFy5zpG8Ty7tQofcVaQUvrMXp6CBwqF6fThLCLOrfBtuHAtwq6Yu4XPCllg=="],
|
||||
|
||||
"find-my-way-ts": ["find-my-way-ts@0.1.6", "", {}, "sha512-a85L9ZoXtNAey3Y6Z+eBWW658kO/MwR7zIafkIUPUMf3isZG0NCs2pjW2wtjxAKuJPxMAsHUIP4ZPGv0o5gyTA=="],
|
||||
|
||||
"ini": ["ini@7.0.0", "", {}, "sha512-ifK0CgjALofS5bkrcTy4RaQ9Vx2Knf/eLeIO+NaswQEpH1UblrtTSCIvN71qQDMq0PeQ/SSPojvEJp9vvvfr+w=="],
|
||||
|
||||
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
|
||||
|
||||
"json-schema": ["json-schema@0.4.0", "", {}, "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA=="],
|
||||
|
||||
"kubernetes-types": ["kubernetes-types@1.30.0", "", {}, "sha512-Dew1okvhM/SQcIa2rcgujNndZwU8VnSapDgdxlYoB84ZlpAD43U6KLAFqYo17ykSFGHNPrg0qry0bP+GJd9v7Q=="],
|
||||
|
||||
"msgpackr": ["msgpackr@2.0.5", "", { "optionalDependencies": { "msgpackr-extract": "^3.0.4" } }, "sha512-cef05H/dSYpLpqp3sj/qyZh5vhUYCalnaLO7j1yOmpsR0y/XwLVtK7r5gn+U/F7CTEfMowcGhlUQJDLcLf7jcA=="],
|
||||
|
||||
"msgpackr-extract": ["msgpackr-extract@3.0.4", "", { "dependencies": { "node-gyp-build-optional-packages": "5.2.2" }, "optionalDependencies": { "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.4", "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.4", "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.4", "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.4", "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.4", "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.4" }, "bin": { "download-msgpackr-prebuilds": "bin/download-prebuilds.js" } }, "sha512-4kmO/MdyUIkLIvTPr8VHLil4AtoKIoniWPIEk5+CDy0xnWC84azhSFmuJ7PxZdsYtiP5kEeQsORAVIeMgxT+Hw=="],
|
||||
|
||||
"multipasta": ["multipasta@0.2.8", "", {}, "sha512-ZPWuMKyv0cSO29f7hozp+k6+crZbQijV8ipMvxNxRf2SwtYGTX1ZX89Kd20VV4H9Znonx+EQn+iy1wGQsJ+b+Q=="],
|
||||
|
||||
"node-gyp-build-optional-packages": ["node-gyp-build-optional-packages@5.2.2", "", { "dependencies": { "detect-libc": "^2.0.1" }, "bin": { "node-gyp-build-optional-packages": "bin.js", "node-gyp-build-optional-packages-optional": "optional.js", "node-gyp-build-optional-packages-test": "build-test.js" } }, "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw=="],
|
||||
|
||||
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
|
||||
|
||||
"prettier": ["prettier@3.9.6", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g=="],
|
||||
|
||||
"pure-rand": ["pure-rand@8.4.2", "", {}, "sha512-vvuOGgcuPJAirlHvuQw1TrOiw7ptaIXXmIbNuiNOY6lNGJJH49PQ1Kj4nd783nPdQhQdicgOjVI2yI/9BD6/Ng=="],
|
||||
|
||||
"shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
|
||||
|
||||
"shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
|
||||
|
||||
"toml": ["toml@4.3.0", "", {}, "sha512-lVb8X9BsPVuH0M4BKeS91tXAmJvCjQ5UIyAbQFaxkKGyUFK2RPkhwaFSQH8vbpl1d23eu/IBH+dwVMHWaq9A5A=="],
|
||||
|
||||
"typescript": ["typescript@6.0.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw=="],
|
||||
|
||||
"undici-types": ["undici-types@7.24.6", "", {}, "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="],
|
||||
|
||||
"uuid": ["uuid@14.0.1", "", { "bin": { "uuid": "dist-node/bin/uuid" } }, "sha512-6ZxzVpzDXDa3bJWaHilVayA+BH/1zmxCJoVgvmqJnid/gPoKHxUrS/aC/T6LGQtNHT+XHG9fXPJB4d+IrU30Ew=="],
|
||||
|
||||
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
|
||||
|
||||
"yaml": ["yaml@2.9.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA=="],
|
||||
|
||||
"zod": ["zod@4.1.8", "", {}, "sha512-5R1P+WwQqmmMIEACyzSvo4JXHY5WiAFHRMg+zBZKgKS+Q1viRa0C1hmUKtHltoIFKtIdki3pRxkmpP74jnNYHQ=="],
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
There are two features in progress:
|
||||
|
||||
- Preserve everything related to session export feature.
|
||||
- Do not preserve discussions related to email notifications.
|
||||
|
||||
Keep the relevant file line numbers and short code snippets.
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"plugin": [
|
||||
[
|
||||
"opencode-plugin-compaction-prompt",
|
||||
{
|
||||
"memoryFile": ".opencode/compaction.md",
|
||||
"mode": "append",
|
||||
"prompt": "<Example prompt for compaction>",
|
||||
"completionMarker": "Custom compaction request honored."
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"name": "opencode-plugin-compaction-prompt",
|
||||
"version": "0.1.0",
|
||||
"description": "Customize OpenCode's compaction prompt to preserve important context and omit unnecessary details.",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"import": "./dist/index.js"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"README.md",
|
||||
"LICENSE",
|
||||
"CHANGELOG.md"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "bun build src/index.ts --outdir dist --target bun --format esm --sourcemap=external && tsc -p tsconfig.build.json",
|
||||
"clean": "rm -rf dist",
|
||||
"test": "bun test",
|
||||
"typecheck": "tsc --noEmit -p tsconfig.json",
|
||||
"pack:check": "npm pack --dry-run",
|
||||
"prepublishOnly": "bun run build",
|
||||
"format": "prettier --write src test *.json examples/*.json examples/*.md README.md",
|
||||
"format:check": "prettier --check src test *.json examples/*.json examples/*.md README.md"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@opencode-ai/plugin": "^1.18.10",
|
||||
"@types/bun": "^1.3.14",
|
||||
"@types/node": "^25.6.0",
|
||||
"prettier": "^3.6.2",
|
||||
"typescript": "^6.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"bun": ">=1.3.0"
|
||||
},
|
||||
"keywords": [
|
||||
"opencode",
|
||||
"opencode-plugin",
|
||||
"compaction",
|
||||
"context"
|
||||
],
|
||||
"license": "MIT"
|
||||
}
|
||||
+120
@@ -0,0 +1,120 @@
|
||||
import { readFile } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import type { Hooks, Plugin, PluginOptions } from "@opencode-ai/plugin";
|
||||
|
||||
type CompactionMode = "append" | "replace";
|
||||
|
||||
export type CompactionOptions = PluginOptions & {
|
||||
memoryFile?: unknown;
|
||||
mode?: unknown;
|
||||
prompt?: unknown;
|
||||
completionMarker?: unknown;
|
||||
};
|
||||
|
||||
const defaultMemoryFile = ".opencode/compaction.md";
|
||||
const defaultPrompt = "";
|
||||
const defaultCompletionMarker = "Custom compaction request honored.";
|
||||
|
||||
function asString(value: unknown): string | undefined {
|
||||
if (typeof value !== "string") {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const trimmedValue = value.trim();
|
||||
return trimmedValue || undefined;
|
||||
}
|
||||
|
||||
function buildInstructions(
|
||||
prompt: string,
|
||||
completionMarker: string,
|
||||
memory: string,
|
||||
): 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}**`,
|
||||
prompt,
|
||||
];
|
||||
|
||||
if (memory) {
|
||||
sections.push(memory);
|
||||
}
|
||||
|
||||
return sections.join("\n");
|
||||
}
|
||||
|
||||
async function readMemory(
|
||||
memoryPath: string,
|
||||
logError: (message: string) => Promise<void>,
|
||||
): Promise<string> {
|
||||
try {
|
||||
return (await readFile(memoryPath, "utf8")).trim();
|
||||
} catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
|
||||
await logError(`Unable to read compaction memory file: ${memoryPath}`);
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds configured instructions and optional project memory to OpenCode's
|
||||
* compaction prompt.
|
||||
*
|
||||
* Append mode preserves OpenCode's built-in compaction prompt and adds the
|
||||
* plugin instructions as context. Replace mode supplies a complete prompt
|
||||
* containing the plugin instructions instead. The memory file is resolved
|
||||
* relative to the active worktree and may be absent.
|
||||
*
|
||||
* @example
|
||||
* ```json
|
||||
* {
|
||||
* "plugin": [["opencode-plugin-compaction-prompt", {
|
||||
* "memoryFile": ".opencode/compaction.md",
|
||||
* "mode": "append"
|
||||
* }]]
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @param context OpenCode's plugin context, including the active worktree.
|
||||
* @param options User-provided plugin configuration.
|
||||
* @returns The hooks registered by the plugin.
|
||||
*/
|
||||
export const CompactionPromptPlugin: Plugin = async (
|
||||
context,
|
||||
options?: PluginOptions,
|
||||
): Promise<Hooks> => {
|
||||
const configured = (options ?? {}) as CompactionOptions;
|
||||
const memoryFile = asString(configured.memoryFile) ?? defaultMemoryFile;
|
||||
const prompt = asString(configured.prompt) ?? defaultPrompt;
|
||||
const completionMarker =
|
||||
asString(configured.completionMarker) ?? defaultCompletionMarker;
|
||||
const mode: CompactionMode =
|
||||
configured.mode === "replace" ? "replace" : "append";
|
||||
const memoryPath = path.resolve(context.worktree, memoryFile);
|
||||
const logError = async (message: string): Promise<void> => {
|
||||
await context.client.app.log({
|
||||
body: {
|
||||
level: "error",
|
||||
message,
|
||||
service: "opencode-plugin-compaction-prompt",
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
"experimental.session.compacting": async (
|
||||
_input,
|
||||
output,
|
||||
): Promise<void> => {
|
||||
const memory = await readMemory(memoryPath, logError);
|
||||
const instructions = buildInstructions(prompt, completionMarker, memory);
|
||||
|
||||
if (mode === "replace") {
|
||||
output.prompt = instructions;
|
||||
} else {
|
||||
output.context.push(instructions);
|
||||
}
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,136 @@
|
||||
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";
|
||||
|
||||
const temporaryDirectories: string[] = [];
|
||||
|
||||
afterEach(async (): Promise<void> => {
|
||||
await Promise.all(
|
||||
temporaryDirectories
|
||||
.splice(0)
|
||||
.map((directory) => rm(directory, { recursive: true })),
|
||||
);
|
||||
});
|
||||
|
||||
async function createContext(): Promise<{
|
||||
worktree: string;
|
||||
logs: Array<unknown>;
|
||||
client: { app: { log: (input: unknown) => Promise<void> } };
|
||||
}> {
|
||||
const worktree = await mkdtemp(
|
||||
path.join(os.tmpdir(), "opencode-compaction-"),
|
||||
);
|
||||
temporaryDirectories.push(worktree);
|
||||
const logs: Array<unknown> = [];
|
||||
|
||||
return {
|
||||
worktree,
|
||||
logs,
|
||||
client: {
|
||||
app: {
|
||||
log: async (input: unknown): Promise<void> => {
|
||||
logs.push(input);
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe("CompactionPromptPlugin", () => {
|
||||
test("appends instructions and memory by default", async () => {
|
||||
const context = await createContext();
|
||||
await mkdir(path.join(context.worktree, ".opencode"));
|
||||
await writeFile(
|
||||
path.join(context.worktree, ".opencode/compaction.md"),
|
||||
"Keep the active hypothesis.",
|
||||
);
|
||||
const hooks = await CompactionPromptPlugin(context as never);
|
||||
const output = { context: [] as string[] };
|
||||
|
||||
await hooks["experimental.session.compacting"]?.(
|
||||
{ sessionID: "test" },
|
||||
output,
|
||||
);
|
||||
|
||||
expect(output.context[0]).toContain("Keep the active hypothesis.");
|
||||
expect(output.context[0]).toContain("Custom compaction request honored.");
|
||||
});
|
||||
|
||||
test("combines custom instructions with a configured memory file", async () => {
|
||||
const context = await createContext();
|
||||
await writeFile(
|
||||
path.join(context.worktree, "project-memory.md"),
|
||||
"Preserve the active hypothesis.",
|
||||
);
|
||||
const hooks = await CompactionPromptPlugin(context as never, {
|
||||
memoryFile: "project-memory.md",
|
||||
prompt: "Keep exact file paths.",
|
||||
completionMarker: "Compaction complete.",
|
||||
});
|
||||
const output = { context: ["Existing context"] };
|
||||
|
||||
await hooks["experimental.session.compacting"]?.(
|
||||
{ sessionID: "test" },
|
||||
output,
|
||||
);
|
||||
|
||||
expect(output.context).toHaveLength(2);
|
||||
expect(output.context[0]).toBe("Existing context");
|
||||
expect(output.context[1]).toContain("Keep exact file paths.");
|
||||
expect(output.context[1]).toContain("Preserve the active hypothesis.");
|
||||
expect(output.context[1]).toContain("Compaction complete.");
|
||||
});
|
||||
|
||||
test("supports replacing the default prompt", async () => {
|
||||
const context = await createContext();
|
||||
const hooks = await CompactionPromptPlugin(context as never, {
|
||||
mode: "replace",
|
||||
prompt: "Preserve the current implementation plan.",
|
||||
});
|
||||
const output: { context: string[]; prompt?: string } = { context: [] };
|
||||
|
||||
await hooks["experimental.session.compacting"]?.(
|
||||
{ sessionID: "test" },
|
||||
output,
|
||||
);
|
||||
|
||||
expect(output.prompt).toContain("User Compaction Instructions");
|
||||
expect(output.prompt).toContain(
|
||||
"Preserve the current implementation plan.",
|
||||
);
|
||||
expect(output.context).toHaveLength(0);
|
||||
});
|
||||
|
||||
test("ignores a missing memory file", async () => {
|
||||
const context = await createContext();
|
||||
const hooks = await CompactionPromptPlugin(context as never);
|
||||
const output = { context: [] as string[] };
|
||||
|
||||
await hooks["experimental.session.compacting"]?.(
|
||||
{ sessionID: "test" },
|
||||
output,
|
||||
);
|
||||
|
||||
expect(context.logs).toHaveLength(0);
|
||||
expect(output.context).toHaveLength(1);
|
||||
});
|
||||
|
||||
test("logs non-missing memory file errors without failing compaction", async () => {
|
||||
const context = await createContext();
|
||||
await writeFile(path.join(context.worktree, "memory"), "not a directory");
|
||||
const hooks = await CompactionPromptPlugin(context as never, {
|
||||
memoryFile: "memory/file.md",
|
||||
});
|
||||
const output = { context: [] as string[] };
|
||||
|
||||
await hooks["experimental.session.compacting"]?.(
|
||||
{ sessionID: "test" },
|
||||
output,
|
||||
);
|
||||
|
||||
expect(context.logs).toHaveLength(1);
|
||||
expect(output.context).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"emitDeclarationOnly": true,
|
||||
"noEmit": false,
|
||||
"outDir": "dist",
|
||||
"rootDir": "src",
|
||||
"sourceMap": true
|
||||
},
|
||||
"include": ["src/**/*.ts"]
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"noEmit": true,
|
||||
"types": ["bun", "node"]
|
||||
},
|
||||
"include": ["src/**/*.ts", "test/**/*.ts"]
|
||||
}
|
||||
Reference in New Issue
Block a user