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