@vikrant82/opencode-context-truncate 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +214 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +414 -0
- package/dist/index.js.map +1 -0
- package/package.json +60 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 vikrant82
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
# opencode-context-truncate
|
|
2
|
+
|
|
3
|
+
An OpenCode plugin for **hiding old conversation context from future model
|
|
4
|
+
requests** without deleting your visible OpenCode history.
|
|
5
|
+
|
|
6
|
+
Use it when a recent part of a session has gone down the wrong path and you want
|
|
7
|
+
future prompts to behave as if that part of the conversation never happened —
|
|
8
|
+
without starting a new session and without paying for an LLM summarization step.
|
|
9
|
+
|
|
10
|
+
## What it does
|
|
11
|
+
|
|
12
|
+
`opencode-context-truncate` adds three slash commands:
|
|
13
|
+
|
|
14
|
+
```text
|
|
15
|
+
/truncate_user_turns 5
|
|
16
|
+
/truncate_status
|
|
17
|
+
/truncate_clear
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
When you run `/truncate_user_turns N`, the plugin:
|
|
21
|
+
|
|
22
|
+
1. Looks at the current session history.
|
|
23
|
+
2. Counts backward by real user messages.
|
|
24
|
+
3. Stores a fixed hidden range from the Nth most recent user message through the
|
|
25
|
+
current latest message.
|
|
26
|
+
4. Removes that range from future model prompts.
|
|
27
|
+
|
|
28
|
+
Your OpenCode UI and session history remain unchanged. Only future LLM requests
|
|
29
|
+
are filtered.
|
|
30
|
+
|
|
31
|
+
## When to use it
|
|
32
|
+
|
|
33
|
+
Good fits:
|
|
34
|
+
|
|
35
|
+
- You tried an approach and want the model to forget it.
|
|
36
|
+
- A long debugging branch is no longer relevant.
|
|
37
|
+
- You pasted large context that should not be sent again.
|
|
38
|
+
- You want a cheap alternative to summarization/compression.
|
|
39
|
+
|
|
40
|
+
Not a fit:
|
|
41
|
+
|
|
42
|
+
- You want a summary of the hidden content.
|
|
43
|
+
- You want to physically delete messages from OpenCode history.
|
|
44
|
+
- You want token accounting or automatic pruning.
|
|
45
|
+
|
|
46
|
+
## Installation
|
|
47
|
+
|
|
48
|
+
### Local development install
|
|
49
|
+
|
|
50
|
+
Clone or keep this project somewhere on disk, then install and build it:
|
|
51
|
+
|
|
52
|
+
```sh
|
|
53
|
+
npm install
|
|
54
|
+
npm run build
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Add the plugin path to your OpenCode config:
|
|
58
|
+
|
|
59
|
+
```jsonc
|
|
60
|
+
{
|
|
61
|
+
"$schema": "https://opencode.ai/config.json",
|
|
62
|
+
"plugin": ["/Users/chauv/vibe-tools/opencode-context-truncate"],
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Then fully quit and restart OpenCode. OpenCode loads plugins at startup and does
|
|
67
|
+
not hot-reload plugin config.
|
|
68
|
+
|
|
69
|
+
### npm package install
|
|
70
|
+
|
|
71
|
+
If this package is published to npm, configure it by package name instead of a
|
|
72
|
+
local path:
|
|
73
|
+
|
|
74
|
+
```jsonc
|
|
75
|
+
{
|
|
76
|
+
"$schema": "https://opencode.ai/config.json",
|
|
77
|
+
"plugin": ["@vikrant82/opencode-context-truncate"],
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Restart OpenCode after changing the config.
|
|
82
|
+
|
|
83
|
+
## Commands
|
|
84
|
+
|
|
85
|
+
### `/truncate_user_turns N`
|
|
86
|
+
|
|
87
|
+
Hide the last `N` real user-turn windows from future model prompts.
|
|
88
|
+
|
|
89
|
+
Example:
|
|
90
|
+
|
|
91
|
+
```text
|
|
92
|
+
/truncate_user_turns 3
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Important details:
|
|
96
|
+
|
|
97
|
+
- `N` counts user messages, not assistant replies, tool calls, or ignored plugin
|
|
98
|
+
notifications.
|
|
99
|
+
- The hidden range is fixed at command time.
|
|
100
|
+
- New messages sent after the command remain visible to the model.
|
|
101
|
+
- If fewer than `N` user messages exist, the plugin hides from the oldest real
|
|
102
|
+
user message it can find and reports the actual count.
|
|
103
|
+
- The command itself does not call the model.
|
|
104
|
+
|
|
105
|
+
### `/truncate_status`
|
|
106
|
+
|
|
107
|
+
Show active hidden ranges for the current session.
|
|
108
|
+
|
|
109
|
+
```text
|
|
110
|
+
/truncate_status
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### `/truncate_clear`
|
|
114
|
+
|
|
115
|
+
Clear active hidden ranges for the current session.
|
|
116
|
+
|
|
117
|
+
```text
|
|
118
|
+
/truncate_clear
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
After clearing, future model prompts can see the full session context again.
|
|
122
|
+
|
|
123
|
+
## Persistence
|
|
124
|
+
|
|
125
|
+
Hidden ranges are persisted and survive OpenCode restarts until you clear them.
|
|
126
|
+
|
|
127
|
+
State is stored as JSON under OpenCode's data directory:
|
|
128
|
+
|
|
129
|
+
```text
|
|
130
|
+
$XDG_DATA_HOME/opencode/storage/plugin/context-truncate/state.json
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
If `XDG_DATA_HOME` is unset, the plugin uses:
|
|
134
|
+
|
|
135
|
+
```text
|
|
136
|
+
~/.local/share/opencode/storage/plugin/context-truncate/state.json
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Writes are atomic. If the state file is corrupt, the plugin renames it aside and
|
|
140
|
+
starts with empty state.
|
|
141
|
+
|
|
142
|
+
## Example workflow
|
|
143
|
+
|
|
144
|
+
1. Work normally in an OpenCode session.
|
|
145
|
+
2. Realize the last few user turns led the model in the wrong direction.
|
|
146
|
+
3. Run:
|
|
147
|
+
|
|
148
|
+
```text
|
|
149
|
+
/truncate_user_turns 4
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
4. Optionally check what is hidden:
|
|
153
|
+
|
|
154
|
+
```text
|
|
155
|
+
/truncate_status
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
5. Continue with a new prompt. The model will not see the hidden historical
|
|
159
|
+
range, but your OpenCode history remains visible.
|
|
160
|
+
6. Undo the filtering if needed:
|
|
161
|
+
|
|
162
|
+
```text
|
|
163
|
+
/truncate_clear
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
## Development
|
|
167
|
+
|
|
168
|
+
Useful commands:
|
|
169
|
+
|
|
170
|
+
```sh
|
|
171
|
+
npm install
|
|
172
|
+
npm run typecheck
|
|
173
|
+
npm run build
|
|
174
|
+
npm run format:check
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Format files with:
|
|
178
|
+
|
|
179
|
+
```sh
|
|
180
|
+
npm run format
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
## Current limitations
|
|
184
|
+
|
|
185
|
+
- State is per OpenCode session ID.
|
|
186
|
+
- There is no automatic cleanup for stale sessions yet.
|
|
187
|
+
- There is no summarization or token accounting.
|
|
188
|
+
- The plugin depends on OpenCode's experimental chat message transform hook.
|
|
189
|
+
|
|
190
|
+
## Troubleshooting
|
|
191
|
+
|
|
192
|
+
### The commands are not available
|
|
193
|
+
|
|
194
|
+
- Confirm the plugin is listed in your OpenCode config.
|
|
195
|
+
- Confirm the local path is correct, or that the npm package is installed and
|
|
196
|
+
resolvable.
|
|
197
|
+
- Restart OpenCode after changing config.
|
|
198
|
+
|
|
199
|
+
### Hidden context still appears to be used
|
|
200
|
+
|
|
201
|
+
- Run `/truncate_status` in the same session.
|
|
202
|
+
- Make sure you cleared or truncated the intended session.
|
|
203
|
+
- Remember that only future model prompts are filtered; existing visible history
|
|
204
|
+
is intentionally unchanged.
|
|
205
|
+
|
|
206
|
+
### I want to reset all persisted state manually
|
|
207
|
+
|
|
208
|
+
Delete the state file:
|
|
209
|
+
|
|
210
|
+
```sh
|
|
211
|
+
rm ~/.local/share/opencode/storage/plugin/context-truncate/state.json
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
If you use `XDG_DATA_HOME`, delete the file under that directory instead.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAigBlD,QAAA,MAAM,MAAM,EAAE,MAqDb,CAAC;AAEF,eAAe,MAAM,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
// index.ts
|
|
2
|
+
import { mkdir, readFile, rename, writeFile } from "fs/promises";
|
|
3
|
+
import { homedir } from "os";
|
|
4
|
+
import { dirname, join } from "path";
|
|
5
|
+
var HANDLED = "__OPENCODE_CONTEXT_TRUNCATE_HANDLED__";
|
|
6
|
+
var sessionStates = /* @__PURE__ */ new Map();
|
|
7
|
+
var persistenceLoaded = false;
|
|
8
|
+
var saveQueue = Promise.resolve();
|
|
9
|
+
function stateFilePath() {
|
|
10
|
+
return join(
|
|
11
|
+
process.env.XDG_DATA_HOME ?? join(homedir(), ".local", "share"),
|
|
12
|
+
"opencode",
|
|
13
|
+
"storage",
|
|
14
|
+
"plugin",
|
|
15
|
+
"context-truncate",
|
|
16
|
+
"state.json"
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
function isRecord(value) {
|
|
20
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
21
|
+
}
|
|
22
|
+
function asString(value) {
|
|
23
|
+
return typeof value === "string" && value.length > 0 ? value : null;
|
|
24
|
+
}
|
|
25
|
+
function asNonNegativeInteger(value) {
|
|
26
|
+
return Number.isSafeInteger(value) && Number(value) >= 0 ? Number(value) : null;
|
|
27
|
+
}
|
|
28
|
+
function asPositiveInteger(value) {
|
|
29
|
+
return Number.isSafeInteger(value) && Number(value) > 0 ? Number(value) : null;
|
|
30
|
+
}
|
|
31
|
+
function parsePersistedRange(value) {
|
|
32
|
+
if (!isRecord(value)) {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
const id = asPositiveInteger(value.id);
|
|
36
|
+
const startMessageId = asString(value.startMessageId);
|
|
37
|
+
const endMessageId = asString(value.endMessageId);
|
|
38
|
+
const createdAt = asNonNegativeInteger(value.createdAt);
|
|
39
|
+
const command = asString(value.command);
|
|
40
|
+
const requestedUserTurns = asPositiveInteger(value.requestedUserTurns);
|
|
41
|
+
const actualUserTurns = asNonNegativeInteger(value.actualUserTurns);
|
|
42
|
+
const originalMessageCount = asPositiveInteger(value.originalMessageCount);
|
|
43
|
+
if (id === null || startMessageId === null || endMessageId === null || createdAt === null || command === null || requestedUserTurns === null || actualUserTurns === null || originalMessageCount === null) {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
return {
|
|
47
|
+
id,
|
|
48
|
+
startMessageId,
|
|
49
|
+
endMessageId,
|
|
50
|
+
createdAt,
|
|
51
|
+
command,
|
|
52
|
+
requestedUserTurns,
|
|
53
|
+
actualUserTurns,
|
|
54
|
+
originalMessageCount
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
function parsePersistedSessionState(value) {
|
|
58
|
+
if (!isRecord(value)) {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
const ranges = Array.isArray(value.ranges) ? value.ranges.flatMap((range) => {
|
|
62
|
+
const parsed = parsePersistedRange(range);
|
|
63
|
+
return parsed ? [parsed] : [];
|
|
64
|
+
}) : [];
|
|
65
|
+
const maxRangeId = ranges.reduce((max, range) => Math.max(max, range.id), 0);
|
|
66
|
+
const nextRangeId = Math.max(
|
|
67
|
+
asPositiveInteger(value.nextRangeId) ?? 1,
|
|
68
|
+
maxRangeId + 1
|
|
69
|
+
);
|
|
70
|
+
return { nextRangeId, ranges };
|
|
71
|
+
}
|
|
72
|
+
function serializeState() {
|
|
73
|
+
const sessions = {};
|
|
74
|
+
for (const [sessionId, state] of sessionStates) {
|
|
75
|
+
if (state.ranges.length > 0) {
|
|
76
|
+
sessions[sessionId] = state;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
version: 1,
|
|
81
|
+
updatedAt: Date.now(),
|
|
82
|
+
sessions
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
async function loadPersistedState() {
|
|
86
|
+
if (persistenceLoaded) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
persistenceLoaded = true;
|
|
90
|
+
const filePath = stateFilePath();
|
|
91
|
+
let raw;
|
|
92
|
+
try {
|
|
93
|
+
raw = await readFile(filePath, "utf8");
|
|
94
|
+
} catch (error) {
|
|
95
|
+
if (error.code === "ENOENT") {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
throw error;
|
|
99
|
+
}
|
|
100
|
+
let parsed;
|
|
101
|
+
try {
|
|
102
|
+
parsed = JSON.parse(raw);
|
|
103
|
+
} catch {
|
|
104
|
+
await rename(filePath, `${filePath}.corrupt-${Date.now()}`).catch(() => {
|
|
105
|
+
});
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
if (!isRecord(parsed) || parsed.version !== 1 || !isRecord(parsed.sessions)) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
sessionStates.clear();
|
|
112
|
+
for (const [sessionId, state] of Object.entries(parsed.sessions)) {
|
|
113
|
+
const parsedState = parsePersistedSessionState(state);
|
|
114
|
+
if (parsedState) {
|
|
115
|
+
sessionStates.set(sessionId, parsedState);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
async function writePersistedState() {
|
|
120
|
+
const filePath = stateFilePath();
|
|
121
|
+
const temporaryPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;
|
|
122
|
+
await mkdir(dirname(filePath), { recursive: true });
|
|
123
|
+
await writeFile(
|
|
124
|
+
temporaryPath,
|
|
125
|
+
`${JSON.stringify(serializeState(), null, 2)}
|
|
126
|
+
`,
|
|
127
|
+
"utf8"
|
|
128
|
+
);
|
|
129
|
+
await rename(temporaryPath, filePath);
|
|
130
|
+
}
|
|
131
|
+
async function savePersistedState() {
|
|
132
|
+
saveQueue = saveQueue.then(writePersistedState, writePersistedState);
|
|
133
|
+
await saveQueue;
|
|
134
|
+
}
|
|
135
|
+
function getSessionState(sessionId) {
|
|
136
|
+
let state = sessionStates.get(sessionId);
|
|
137
|
+
if (!state) {
|
|
138
|
+
state = { nextRangeId: 1, ranges: [] };
|
|
139
|
+
sessionStates.set(sessionId, state);
|
|
140
|
+
}
|
|
141
|
+
return state;
|
|
142
|
+
}
|
|
143
|
+
function messageId(message) {
|
|
144
|
+
return typeof message.info?.id === "string" ? message.info.id : void 0;
|
|
145
|
+
}
|
|
146
|
+
function messageSessionId(message) {
|
|
147
|
+
return typeof message.info?.sessionID === "string" ? message.info.sessionID : void 0;
|
|
148
|
+
}
|
|
149
|
+
function isIgnoredUserMessage(message) {
|
|
150
|
+
if (message.info?.role !== "user") {
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
const parts = Array.isArray(message.parts) ? message.parts : [];
|
|
154
|
+
if (parts.length === 0) {
|
|
155
|
+
return true;
|
|
156
|
+
}
|
|
157
|
+
return parts.every((part) => part.ignored === true);
|
|
158
|
+
}
|
|
159
|
+
function isRealUserMessage(message) {
|
|
160
|
+
return message.info?.role === "user" && !isIgnoredUserMessage(message);
|
|
161
|
+
}
|
|
162
|
+
function getSessionIdFromMessages(messages) {
|
|
163
|
+
for (const message of messages) {
|
|
164
|
+
const sessionId = messageSessionId(message);
|
|
165
|
+
if (sessionId) {
|
|
166
|
+
return sessionId;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return void 0;
|
|
170
|
+
}
|
|
171
|
+
function parsePositiveInteger(raw) {
|
|
172
|
+
const trimmed = raw.trim();
|
|
173
|
+
if (!/^\d+$/.test(trimmed)) {
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
const value = Number.parseInt(trimmed, 10);
|
|
177
|
+
return Number.isSafeInteger(value) && value > 0 ? value : null;
|
|
178
|
+
}
|
|
179
|
+
async function fetchSessionMessages(client, sessionId) {
|
|
180
|
+
const response = await client.session.messages({
|
|
181
|
+
path: { id: sessionId }
|
|
182
|
+
});
|
|
183
|
+
const data = response?.data ?? response;
|
|
184
|
+
return Array.isArray(data) ? data : [];
|
|
185
|
+
}
|
|
186
|
+
async function sendIgnoredMessage(client, sessionId, text) {
|
|
187
|
+
await client.session.prompt({
|
|
188
|
+
path: { id: sessionId },
|
|
189
|
+
body: {
|
|
190
|
+
noReply: true,
|
|
191
|
+
parts: [
|
|
192
|
+
{
|
|
193
|
+
type: "text",
|
|
194
|
+
text,
|
|
195
|
+
ignored: true
|
|
196
|
+
}
|
|
197
|
+
]
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
function findTruncationStart(messages, requestedUserTurns) {
|
|
202
|
+
let seen = 0;
|
|
203
|
+
let oldestRealUserIndex = -1;
|
|
204
|
+
for (let index = messages.length - 1; index >= 0; index--) {
|
|
205
|
+
if (!isRealUserMessage(messages[index])) {
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
oldestRealUserIndex = index;
|
|
209
|
+
seen++;
|
|
210
|
+
if (seen === requestedUserTurns) {
|
|
211
|
+
return { index, actualUserTurns: seen, partial: false };
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
if (oldestRealUserIndex >= 0) {
|
|
215
|
+
return { index: oldestRealUserIndex, actualUserTurns: seen, partial: true };
|
|
216
|
+
}
|
|
217
|
+
return { index: -1, actualUserTurns: 0, partial: false };
|
|
218
|
+
}
|
|
219
|
+
function findLastMessageIndex(messages) {
|
|
220
|
+
for (let index = messages.length - 1; index >= 0; index--) {
|
|
221
|
+
if (messageId(messages[index])) {
|
|
222
|
+
return index;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return -1;
|
|
226
|
+
}
|
|
227
|
+
function countRealUsers(messages, startIndex, endIndex) {
|
|
228
|
+
let count = 0;
|
|
229
|
+
for (let index = startIndex; index <= endIndex; index++) {
|
|
230
|
+
if (isRealUserMessage(messages[index])) {
|
|
231
|
+
count++;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return count;
|
|
235
|
+
}
|
|
236
|
+
function createHiddenRange(state, messages, requestedUserTurns) {
|
|
237
|
+
const start = findTruncationStart(messages, requestedUserTurns);
|
|
238
|
+
const endIndex = findLastMessageIndex(messages);
|
|
239
|
+
if (start.index < 0 || endIndex < 0 || start.index > endIndex) {
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
const startMessageId = messageId(messages[start.index]);
|
|
243
|
+
const endMessageId = messageId(messages[endIndex]);
|
|
244
|
+
if (!startMessageId || !endMessageId) {
|
|
245
|
+
return null;
|
|
246
|
+
}
|
|
247
|
+
return {
|
|
248
|
+
id: state.nextRangeId++,
|
|
249
|
+
startMessageId,
|
|
250
|
+
endMessageId,
|
|
251
|
+
createdAt: Date.now(),
|
|
252
|
+
command: `truncate_user_turns ${requestedUserTurns}`,
|
|
253
|
+
requestedUserTurns,
|
|
254
|
+
actualUserTurns: countRealUsers(messages, start.index, endIndex),
|
|
255
|
+
originalMessageCount: endIndex - start.index + 1
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
function hiddenIndexesForRange(messages, range) {
|
|
259
|
+
const startIndex = messages.findIndex(
|
|
260
|
+
(message) => messageId(message) === range.startMessageId
|
|
261
|
+
);
|
|
262
|
+
const endIndex = messages.findIndex(
|
|
263
|
+
(message) => messageId(message) === range.endMessageId
|
|
264
|
+
);
|
|
265
|
+
const indexes = /* @__PURE__ */ new Set();
|
|
266
|
+
if (startIndex < 0 || endIndex < 0 || startIndex > endIndex) {
|
|
267
|
+
return indexes;
|
|
268
|
+
}
|
|
269
|
+
for (let index = startIndex; index <= endIndex; index++) {
|
|
270
|
+
indexes.add(index);
|
|
271
|
+
}
|
|
272
|
+
return indexes;
|
|
273
|
+
}
|
|
274
|
+
function applyHiddenRanges(messages, ranges) {
|
|
275
|
+
if (ranges.length === 0 || messages.length === 0) {
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
const hidden = /* @__PURE__ */ new Set();
|
|
279
|
+
for (const range of ranges) {
|
|
280
|
+
for (const index of hiddenIndexesForRange(messages, range)) {
|
|
281
|
+
hidden.add(index);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
if (hidden.size === 0) {
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
const filtered = messages.filter((_, index) => !hidden.has(index));
|
|
288
|
+
messages.length = 0;
|
|
289
|
+
messages.push(...filtered);
|
|
290
|
+
}
|
|
291
|
+
function formatRange(range) {
|
|
292
|
+
return [
|
|
293
|
+
`#${range.id}: ${range.command}`,
|
|
294
|
+
` hides ${range.originalMessageCount} message(s), ${range.actualUserTurns} user turn(s)`,
|
|
295
|
+
` ${range.startMessageId} \u2192 ${range.endMessageId}`
|
|
296
|
+
].join("\n");
|
|
297
|
+
}
|
|
298
|
+
function formatUsage() {
|
|
299
|
+
return [
|
|
300
|
+
"Context Truncate",
|
|
301
|
+
"",
|
|
302
|
+
"Usage:",
|
|
303
|
+
" /truncate_user_turns N Hide the last N user-turn windows from future LLM prompts",
|
|
304
|
+
" /truncate_status Show active hidden ranges for this session",
|
|
305
|
+
" /truncate_clear Clear active hidden ranges for this session",
|
|
306
|
+
"",
|
|
307
|
+
"Example:",
|
|
308
|
+
" /truncate_user_turns 5"
|
|
309
|
+
].join("\n");
|
|
310
|
+
}
|
|
311
|
+
async function handleTruncateUserTurns(client, sessionId, args) {
|
|
312
|
+
const requestedUserTurns = parsePositiveInteger(args);
|
|
313
|
+
if (requestedUserTurns === null) {
|
|
314
|
+
await sendIgnoredMessage(client, sessionId, formatUsage());
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
const messages = await fetchSessionMessages(client, sessionId);
|
|
318
|
+
const state = getSessionState(sessionId);
|
|
319
|
+
const range = createHiddenRange(state, messages, requestedUserTurns);
|
|
320
|
+
if (!range) {
|
|
321
|
+
await sendIgnoredMessage(
|
|
322
|
+
client,
|
|
323
|
+
sessionId,
|
|
324
|
+
"Nothing truncated: no real user messages found."
|
|
325
|
+
);
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
state.ranges.push(range);
|
|
329
|
+
await savePersistedState();
|
|
330
|
+
await sendIgnoredMessage(
|
|
331
|
+
client,
|
|
332
|
+
sessionId,
|
|
333
|
+
[
|
|
334
|
+
"Context Truncate: active",
|
|
335
|
+
"",
|
|
336
|
+
`Requested: last ${requestedUserTurns} user turn(s)`,
|
|
337
|
+
`Hidden now: ${range.actualUserTurns} user turn(s), ${range.originalMessageCount} message(s)`,
|
|
338
|
+
`Range: ${range.startMessageId} \u2192 ${range.endMessageId}`,
|
|
339
|
+
"",
|
|
340
|
+
"This is prompt-only. OpenCode history is unchanged; future LLM requests will not see this range."
|
|
341
|
+
].join("\n")
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
async function handleStatus(client, sessionId) {
|
|
345
|
+
const state = getSessionState(sessionId);
|
|
346
|
+
const message = state.ranges.length === 0 ? "Context Truncate: no active hidden ranges for this session." : [
|
|
347
|
+
"Context Truncate: active hidden ranges",
|
|
348
|
+
"",
|
|
349
|
+
...state.ranges.map(formatRange)
|
|
350
|
+
].join("\n");
|
|
351
|
+
await sendIgnoredMessage(client, sessionId, message);
|
|
352
|
+
}
|
|
353
|
+
async function handleClear(client, sessionId) {
|
|
354
|
+
const state = getSessionState(sessionId);
|
|
355
|
+
const cleared = state.ranges.length;
|
|
356
|
+
state.ranges = [];
|
|
357
|
+
await savePersistedState();
|
|
358
|
+
await sendIgnoredMessage(
|
|
359
|
+
client,
|
|
360
|
+
sessionId,
|
|
361
|
+
`Context Truncate: cleared ${cleared} hidden range(s) for this session.`
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
var server = async ({ client }) => {
|
|
365
|
+
await loadPersistedState();
|
|
366
|
+
return {
|
|
367
|
+
async config(config) {
|
|
368
|
+
config.command ??= {};
|
|
369
|
+
config.command.truncate_user_turns ??= {
|
|
370
|
+
template: "",
|
|
371
|
+
description: "Hide the last N user-turn windows from future LLM prompts"
|
|
372
|
+
};
|
|
373
|
+
config.command.truncate_status ??= {
|
|
374
|
+
template: "",
|
|
375
|
+
description: "Show active context-truncation ranges for this session"
|
|
376
|
+
};
|
|
377
|
+
config.command.truncate_clear ??= {
|
|
378
|
+
template: "",
|
|
379
|
+
description: "Clear active context-truncation ranges for this session"
|
|
380
|
+
};
|
|
381
|
+
},
|
|
382
|
+
async "command.execute.before"(input) {
|
|
383
|
+
if (input.command === "truncate_user_turns") {
|
|
384
|
+
await handleTruncateUserTurns(client, input.sessionID, input.arguments);
|
|
385
|
+
throw new Error(HANDLED);
|
|
386
|
+
}
|
|
387
|
+
if (input.command === "truncate_status") {
|
|
388
|
+
await handleStatus(client, input.sessionID);
|
|
389
|
+
throw new Error(HANDLED);
|
|
390
|
+
}
|
|
391
|
+
if (input.command === "truncate_clear") {
|
|
392
|
+
await handleClear(client, input.sessionID);
|
|
393
|
+
throw new Error(HANDLED);
|
|
394
|
+
}
|
|
395
|
+
},
|
|
396
|
+
async "experimental.chat.messages.transform"(_input, output) {
|
|
397
|
+
const messages = output.messages;
|
|
398
|
+
const sessionId = getSessionIdFromMessages(messages);
|
|
399
|
+
if (!sessionId) {
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
const state = sessionStates.get(sessionId);
|
|
403
|
+
if (!state) {
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
applyHiddenRanges(messages, state.ranges);
|
|
407
|
+
}
|
|
408
|
+
};
|
|
409
|
+
};
|
|
410
|
+
var index_default = server;
|
|
411
|
+
export {
|
|
412
|
+
index_default as default
|
|
413
|
+
};
|
|
414
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../index.ts"],"sourcesContent":["import type { Plugin } from \"@opencode-ai/plugin\";\nimport { mkdir, readFile, rename, writeFile } from \"node:fs/promises\";\nimport { homedir } from \"node:os\";\nimport { dirname, join } from \"node:path\";\n\ntype MessageInfo = {\n id?: string;\n role?: string;\n sessionID?: string;\n};\n\ntype MessageWithParts = {\n info?: MessageInfo;\n parts?: Array<Record<string, any>>;\n};\n\ntype HiddenRange = {\n id: number;\n startMessageId: string;\n endMessageId: string;\n createdAt: number;\n command: string;\n requestedUserTurns: number;\n actualUserTurns: number;\n originalMessageCount: number;\n};\n\ntype SessionState = {\n nextRangeId: number;\n ranges: HiddenRange[];\n};\n\ntype PersistedState = {\n version: 1;\n updatedAt: number;\n sessions: Record<string, SessionState>;\n};\n\nconst HANDLED = \"__OPENCODE_CONTEXT_TRUNCATE_HANDLED__\";\nconst sessionStates = new Map<string, SessionState>();\nlet persistenceLoaded = false;\nlet saveQueue: Promise<void> = Promise.resolve();\n\nfunction stateFilePath(): string {\n return join(\n process.env.XDG_DATA_HOME ?? join(homedir(), \".local\", \"share\"),\n \"opencode\",\n \"storage\",\n \"plugin\",\n \"context-truncate\",\n \"state.json\",\n );\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction asString(value: unknown): string | null {\n return typeof value === \"string\" && value.length > 0 ? value : null;\n}\n\nfunction asNonNegativeInteger(value: unknown): number | null {\n return Number.isSafeInteger(value) && Number(value) >= 0\n ? Number(value)\n : null;\n}\n\nfunction asPositiveInteger(value: unknown): number | null {\n return Number.isSafeInteger(value) && Number(value) > 0\n ? Number(value)\n : null;\n}\n\nfunction parsePersistedRange(value: unknown): HiddenRange | null {\n if (!isRecord(value)) {\n return null;\n }\n\n const id = asPositiveInteger(value.id);\n const startMessageId = asString(value.startMessageId);\n const endMessageId = asString(value.endMessageId);\n const createdAt = asNonNegativeInteger(value.createdAt);\n const command = asString(value.command);\n const requestedUserTurns = asPositiveInteger(value.requestedUserTurns);\n const actualUserTurns = asNonNegativeInteger(value.actualUserTurns);\n const originalMessageCount = asPositiveInteger(value.originalMessageCount);\n\n if (\n id === null ||\n startMessageId === null ||\n endMessageId === null ||\n createdAt === null ||\n command === null ||\n requestedUserTurns === null ||\n actualUserTurns === null ||\n originalMessageCount === null\n ) {\n return null;\n }\n\n return {\n id,\n startMessageId,\n endMessageId,\n createdAt,\n command,\n requestedUserTurns,\n actualUserTurns,\n originalMessageCount,\n };\n}\n\nfunction parsePersistedSessionState(value: unknown): SessionState | null {\n if (!isRecord(value)) {\n return null;\n }\n\n const ranges = Array.isArray(value.ranges)\n ? value.ranges.flatMap((range) => {\n const parsed = parsePersistedRange(range);\n return parsed ? [parsed] : [];\n })\n : [];\n\n const maxRangeId = ranges.reduce((max, range) => Math.max(max, range.id), 0);\n const nextRangeId = Math.max(\n asPositiveInteger(value.nextRangeId) ?? 1,\n maxRangeId + 1,\n );\n\n return { nextRangeId, ranges };\n}\n\nfunction serializeState(): PersistedState {\n const sessions: Record<string, SessionState> = {};\n for (const [sessionId, state] of sessionStates) {\n if (state.ranges.length > 0) {\n sessions[sessionId] = state;\n }\n }\n\n return {\n version: 1,\n updatedAt: Date.now(),\n sessions,\n };\n}\n\nasync function loadPersistedState(): Promise<void> {\n if (persistenceLoaded) {\n return;\n }\n\n persistenceLoaded = true;\n const filePath = stateFilePath();\n let raw: string;\n\n try {\n raw = await readFile(filePath, \"utf8\");\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code === \"ENOENT\") {\n return;\n }\n throw error;\n }\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch {\n await rename(filePath, `${filePath}.corrupt-${Date.now()}`).catch(() => {});\n return;\n }\n\n if (!isRecord(parsed) || parsed.version !== 1 || !isRecord(parsed.sessions)) {\n return;\n }\n\n sessionStates.clear();\n for (const [sessionId, state] of Object.entries(parsed.sessions)) {\n const parsedState = parsePersistedSessionState(state);\n if (parsedState) {\n sessionStates.set(sessionId, parsedState);\n }\n }\n}\n\nasync function writePersistedState(): Promise<void> {\n const filePath = stateFilePath();\n const temporaryPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;\n await mkdir(dirname(filePath), { recursive: true });\n await writeFile(\n temporaryPath,\n `${JSON.stringify(serializeState(), null, 2)}\\n`,\n \"utf8\",\n );\n await rename(temporaryPath, filePath);\n}\n\nasync function savePersistedState(): Promise<void> {\n saveQueue = saveQueue.then(writePersistedState, writePersistedState);\n await saveQueue;\n}\n\nfunction getSessionState(sessionId: string): SessionState {\n let state = sessionStates.get(sessionId);\n if (!state) {\n state = { nextRangeId: 1, ranges: [] };\n sessionStates.set(sessionId, state);\n }\n return state;\n}\n\nfunction messageId(message: MessageWithParts): string | undefined {\n return typeof message.info?.id === \"string\" ? message.info.id : undefined;\n}\n\nfunction messageSessionId(message: MessageWithParts): string | undefined {\n return typeof message.info?.sessionID === \"string\"\n ? message.info.sessionID\n : undefined;\n}\n\nfunction isIgnoredUserMessage(message: MessageWithParts): boolean {\n if (message.info?.role !== \"user\") {\n return false;\n }\n\n const parts = Array.isArray(message.parts) ? message.parts : [];\n if (parts.length === 0) {\n return true;\n }\n\n return parts.every((part) => part.ignored === true);\n}\n\nfunction isRealUserMessage(message: MessageWithParts): boolean {\n return message.info?.role === \"user\" && !isIgnoredUserMessage(message);\n}\n\nfunction getSessionIdFromMessages(\n messages: MessageWithParts[],\n): string | undefined {\n for (const message of messages) {\n const sessionId = messageSessionId(message);\n if (sessionId) {\n return sessionId;\n }\n }\n return undefined;\n}\n\nfunction parsePositiveInteger(raw: string): number | null {\n const trimmed = raw.trim();\n if (!/^\\d+$/.test(trimmed)) {\n return null;\n }\n\n const value = Number.parseInt(trimmed, 10);\n return Number.isSafeInteger(value) && value > 0 ? value : null;\n}\n\nasync function fetchSessionMessages(\n client: any,\n sessionId: string,\n): Promise<MessageWithParts[]> {\n const response = await client.session.messages({\n path: { id: sessionId },\n });\n\n const data = response?.data ?? response;\n return Array.isArray(data) ? data : [];\n}\n\nasync function sendIgnoredMessage(\n client: any,\n sessionId: string,\n text: string,\n): Promise<void> {\n await client.session.prompt({\n path: { id: sessionId },\n body: {\n noReply: true,\n parts: [\n {\n type: \"text\",\n text,\n ignored: true,\n },\n ],\n },\n });\n}\n\nfunction findTruncationStart(\n messages: MessageWithParts[],\n requestedUserTurns: number,\n) {\n let seen = 0;\n let oldestRealUserIndex = -1;\n\n for (let index = messages.length - 1; index >= 0; index--) {\n if (!isRealUserMessage(messages[index])) {\n continue;\n }\n\n oldestRealUserIndex = index;\n seen++;\n\n if (seen === requestedUserTurns) {\n return { index, actualUserTurns: seen, partial: false };\n }\n }\n\n if (oldestRealUserIndex >= 0) {\n return { index: oldestRealUserIndex, actualUserTurns: seen, partial: true };\n }\n\n return { index: -1, actualUserTurns: 0, partial: false };\n}\n\nfunction findLastMessageIndex(messages: MessageWithParts[]): number {\n for (let index = messages.length - 1; index >= 0; index--) {\n if (messageId(messages[index])) {\n return index;\n }\n }\n return -1;\n}\n\nfunction countRealUsers(\n messages: MessageWithParts[],\n startIndex: number,\n endIndex: number,\n): number {\n let count = 0;\n for (let index = startIndex; index <= endIndex; index++) {\n if (isRealUserMessage(messages[index])) {\n count++;\n }\n }\n return count;\n}\n\nfunction createHiddenRange(\n state: SessionState,\n messages: MessageWithParts[],\n requestedUserTurns: number,\n): HiddenRange | null {\n const start = findTruncationStart(messages, requestedUserTurns);\n const endIndex = findLastMessageIndex(messages);\n\n if (start.index < 0 || endIndex < 0 || start.index > endIndex) {\n return null;\n }\n\n const startMessageId = messageId(messages[start.index]);\n const endMessageId = messageId(messages[endIndex]);\n if (!startMessageId || !endMessageId) {\n return null;\n }\n\n return {\n id: state.nextRangeId++,\n startMessageId,\n endMessageId,\n createdAt: Date.now(),\n command: `truncate_user_turns ${requestedUserTurns}`,\n requestedUserTurns,\n actualUserTurns: countRealUsers(messages, start.index, endIndex),\n originalMessageCount: endIndex - start.index + 1,\n };\n}\n\nfunction hiddenIndexesForRange(\n messages: MessageWithParts[],\n range: HiddenRange,\n): Set<number> {\n const startIndex = messages.findIndex(\n (message) => messageId(message) === range.startMessageId,\n );\n const endIndex = messages.findIndex(\n (message) => messageId(message) === range.endMessageId,\n );\n const indexes = new Set<number>();\n\n if (startIndex < 0 || endIndex < 0 || startIndex > endIndex) {\n return indexes;\n }\n\n for (let index = startIndex; index <= endIndex; index++) {\n indexes.add(index);\n }\n\n return indexes;\n}\n\nfunction applyHiddenRanges(\n messages: MessageWithParts[],\n ranges: HiddenRange[],\n): void {\n if (ranges.length === 0 || messages.length === 0) {\n return;\n }\n\n const hidden = new Set<number>();\n for (const range of ranges) {\n for (const index of hiddenIndexesForRange(messages, range)) {\n hidden.add(index);\n }\n }\n\n if (hidden.size === 0) {\n return;\n }\n\n const filtered = messages.filter((_, index) => !hidden.has(index));\n messages.length = 0;\n messages.push(...filtered);\n}\n\nfunction formatRange(range: HiddenRange): string {\n return [\n `#${range.id}: ${range.command}`,\n ` hides ${range.originalMessageCount} message(s), ${range.actualUserTurns} user turn(s)`,\n ` ${range.startMessageId} → ${range.endMessageId}`,\n ].join(\"\\n\");\n}\n\nfunction formatUsage(): string {\n return [\n \"Context Truncate\",\n \"\",\n \"Usage:\",\n \" /truncate_user_turns N Hide the last N user-turn windows from future LLM prompts\",\n \" /truncate_status Show active hidden ranges for this session\",\n \" /truncate_clear Clear active hidden ranges for this session\",\n \"\",\n \"Example:\",\n \" /truncate_user_turns 5\",\n ].join(\"\\n\");\n}\n\nasync function handleTruncateUserTurns(\n client: any,\n sessionId: string,\n args: string,\n): Promise<void> {\n const requestedUserTurns = parsePositiveInteger(args);\n if (requestedUserTurns === null) {\n await sendIgnoredMessage(client, sessionId, formatUsage());\n return;\n }\n\n const messages = await fetchSessionMessages(client, sessionId);\n const state = getSessionState(sessionId);\n const range = createHiddenRange(state, messages, requestedUserTurns);\n\n if (!range) {\n await sendIgnoredMessage(\n client,\n sessionId,\n \"Nothing truncated: no real user messages found.\",\n );\n return;\n }\n\n state.ranges.push(range);\n await savePersistedState();\n\n await sendIgnoredMessage(\n client,\n sessionId,\n [\n \"Context Truncate: active\",\n \"\",\n `Requested: last ${requestedUserTurns} user turn(s)`,\n `Hidden now: ${range.actualUserTurns} user turn(s), ${range.originalMessageCount} message(s)`,\n `Range: ${range.startMessageId} → ${range.endMessageId}`,\n \"\",\n \"This is prompt-only. OpenCode history is unchanged; future LLM requests will not see this range.\",\n ].join(\"\\n\"),\n );\n}\n\nasync function handleStatus(client: any, sessionId: string): Promise<void> {\n const state = getSessionState(sessionId);\n const message =\n state.ranges.length === 0\n ? \"Context Truncate: no active hidden ranges for this session.\"\n : [\n \"Context Truncate: active hidden ranges\",\n \"\",\n ...state.ranges.map(formatRange),\n ].join(\"\\n\");\n\n await sendIgnoredMessage(client, sessionId, message);\n}\n\nasync function handleClear(client: any, sessionId: string): Promise<void> {\n const state = getSessionState(sessionId);\n const cleared = state.ranges.length;\n state.ranges = [];\n await savePersistedState();\n\n await sendIgnoredMessage(\n client,\n sessionId,\n `Context Truncate: cleared ${cleared} hidden range(s) for this session.`,\n );\n}\n\nconst server: Plugin = async ({ client }) => {\n await loadPersistedState();\n\n return {\n async config(config) {\n config.command ??= {};\n config.command.truncate_user_turns ??= {\n template: \"\",\n description:\n \"Hide the last N user-turn windows from future LLM prompts\",\n };\n config.command.truncate_status ??= {\n template: \"\",\n description: \"Show active context-truncation ranges for this session\",\n };\n config.command.truncate_clear ??= {\n template: \"\",\n description: \"Clear active context-truncation ranges for this session\",\n };\n },\n\n async \"command.execute.before\"(input) {\n if (input.command === \"truncate_user_turns\") {\n await handleTruncateUserTurns(client, input.sessionID, input.arguments);\n throw new Error(HANDLED);\n }\n\n if (input.command === \"truncate_status\") {\n await handleStatus(client, input.sessionID);\n throw new Error(HANDLED);\n }\n\n if (input.command === \"truncate_clear\") {\n await handleClear(client, input.sessionID);\n throw new Error(HANDLED);\n }\n },\n\n async \"experimental.chat.messages.transform\"(_input, output) {\n const messages = output.messages as MessageWithParts[];\n const sessionId = getSessionIdFromMessages(messages);\n if (!sessionId) {\n return;\n }\n\n const state = sessionStates.get(sessionId);\n if (!state) {\n return;\n }\n\n applyHiddenRanges(messages, state.ranges);\n },\n };\n};\n\nexport default server;\n"],"mappings":"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","names":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json.schemastore.org/package.json",
|
|
3
|
+
"name": "@vikrant82/opencode-context-truncate",
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"description": "Lightweight OpenCode plugin for prompt-only truncation of recent user-turn windows",
|
|
7
|
+
"main": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js"
|
|
13
|
+
},
|
|
14
|
+
"./server": {
|
|
15
|
+
"types": "./dist/index.d.ts",
|
|
16
|
+
"import": "./dist/index.js"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"scripts": {
|
|
20
|
+
"clean": "rm -rf dist",
|
|
21
|
+
"build": "npm run clean && tsup && tsc --emitDeclarationOnly",
|
|
22
|
+
"typecheck": "tsc --noEmit",
|
|
23
|
+
"format": "prettier --write .",
|
|
24
|
+
"format:check": "prettier --check ."
|
|
25
|
+
},
|
|
26
|
+
"keywords": [
|
|
27
|
+
"opencode",
|
|
28
|
+
"opencode-plugin",
|
|
29
|
+
"context",
|
|
30
|
+
"truncate",
|
|
31
|
+
"tokens"
|
|
32
|
+
],
|
|
33
|
+
"author": "vikrant82",
|
|
34
|
+
"license": "MIT",
|
|
35
|
+
"repository": {
|
|
36
|
+
"type": "git",
|
|
37
|
+
"url": "git+https://github.com/vikrant82/opencode-context-truncate.git"
|
|
38
|
+
},
|
|
39
|
+
"homepage": "https://github.com/vikrant82/opencode-context-truncate#readme",
|
|
40
|
+
"bugs": {
|
|
41
|
+
"url": "https://github.com/vikrant82/opencode-context-truncate/issues"
|
|
42
|
+
},
|
|
43
|
+
"publishConfig": {
|
|
44
|
+
"access": "public"
|
|
45
|
+
},
|
|
46
|
+
"peerDependencies": {
|
|
47
|
+
"@opencode-ai/plugin": ">=1.4.3"
|
|
48
|
+
},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"@opencode-ai/plugin": "^1.4.3",
|
|
51
|
+
"@types/node": "^25.5.0",
|
|
52
|
+
"prettier": "^3.8.1",
|
|
53
|
+
"tsup": "^8.5.1",
|
|
54
|
+
"typescript": "^6.0.2"
|
|
55
|
+
},
|
|
56
|
+
"files": [
|
|
57
|
+
"dist/",
|
|
58
|
+
"README.md"
|
|
59
|
+
]
|
|
60
|
+
}
|