@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 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.
@@ -0,0 +1,4 @@
1
+ import type { Plugin } from "@opencode-ai/plugin";
2
+ declare const server: Plugin;
3
+ export default server;
4
+ //# sourceMappingURL=index.d.ts.map
@@ -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
+ }