opencode-context-dropper-plugin 0.1.4 → 0.1.6
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/README.md +51 -32
- package/dist/index.js +53 -52
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
# OpenCode Context Dropper Plugin
|
|
2
2
|
|
|
3
|
-
A Context Dropper plugin for OpenCode, built with Bun and TypeScript. It
|
|
3
|
+
A Context Dropper plugin for OpenCode, built with Bun and TypeScript. It
|
|
4
|
+
leverages the internal `context-dropper` APIs to efficiently process a large set
|
|
5
|
+
of files one-by-one entirely within your OpenCode sessions. It automatically
|
|
6
|
+
tracks context and handles token pruning to allow continuous agent iteration.
|
|
4
7
|
|
|
5
8
|
## Prerequisites
|
|
6
9
|
|
|
@@ -9,24 +12,44 @@ A Context Dropper plugin for OpenCode, built with Bun and TypeScript. It leverag
|
|
|
9
12
|
|
|
10
13
|
## Installation
|
|
11
14
|
|
|
12
|
-
|
|
15
|
+
You can install the `context-dropper` plugin using one of the following methods.
|
|
13
16
|
|
|
14
|
-
1.
|
|
17
|
+
### 1. Configure OpenCode (Recommended)
|
|
15
18
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
+
You do not need to manually install the package. OpenCode will automatically
|
|
20
|
+
resolve and install it from the NPM registry when you add it to your
|
|
21
|
+
configuration.
|
|
22
|
+
|
|
23
|
+
You can configure the plugin either globally for all projects, or locally for a
|
|
24
|
+
single project:
|
|
25
|
+
|
|
26
|
+
**Option A: Project-Level (Local)**
|
|
27
|
+
|
|
28
|
+
1. Create or edit the `opencode.json` file in the root of your project.
|
|
29
|
+
2. Add the package name to the `plugin` array:
|
|
30
|
+
|
|
31
|
+
```json
|
|
32
|
+
{
|
|
33
|
+
"plugin": ["opencode-context-dropper-plugin"]
|
|
34
|
+
}
|
|
19
35
|
```
|
|
20
36
|
|
|
21
|
-
|
|
22
|
-
|
|
37
|
+
**Option B: Global-Level**
|
|
38
|
+
|
|
39
|
+
1. Create or edit your global OpenCode config file at
|
|
40
|
+
`~/.config/opencode/opencode.json`.
|
|
41
|
+
2. Add the package name to the `plugin` array:
|
|
23
42
|
|
|
24
43
|
```json
|
|
25
44
|
{
|
|
26
|
-
"
|
|
45
|
+
"plugin": ["opencode-context-dropper-plugin"]
|
|
27
46
|
}
|
|
28
47
|
```
|
|
29
48
|
|
|
49
|
+
(Optional) If you prefer to manage the installation yourself, you can install
|
|
50
|
+
the plugin globally using Bun (`bun install -g opencode-context-dropper-plugin`)
|
|
51
|
+
or NPM (`npm install -g opencode-context-dropper-plugin`).
|
|
52
|
+
|
|
30
53
|
## Usage
|
|
31
54
|
|
|
32
55
|
Once installed, start OpenCode:
|
|
@@ -35,14 +58,16 @@ Once installed, start OpenCode:
|
|
|
35
58
|
opencode
|
|
36
59
|
```
|
|
37
60
|
|
|
38
|
-
You can invoke the context dropper loop inside chat simply by using the `/drop`
|
|
61
|
+
You can invoke the context dropper loop inside chat simply by using the `/drop`
|
|
62
|
+
slash command:
|
|
39
63
|
|
|
40
64
|
```text
|
|
41
65
|
/drop <filesetName> <instructions>
|
|
42
66
|
```
|
|
43
67
|
|
|
44
68
|
- `<filesetName>` is the name of a pre-existing fileset in your project.
|
|
45
|
-
- `<instructions>` is the prompt you want the AI to perform on each file
|
|
69
|
+
- `<instructions>` is the prompt you want the AI to perform on each file
|
|
70
|
+
sequentially.
|
|
46
71
|
|
|
47
72
|
**Example:**
|
|
48
73
|
|
|
@@ -54,33 +79,27 @@ You can invoke the context dropper loop inside chat simply by using the `/drop`
|
|
|
54
79
|
|
|
55
80
|
Once invoked, the plugin completely takes over the context management:
|
|
56
81
|
|
|
57
|
-
1. It automatically fetches the first file in the fileset and provides it to the
|
|
58
|
-
|
|
59
|
-
|
|
82
|
+
1. It automatically fetches the first file in the fileset and provides it to the
|
|
83
|
+
agent along with your instructions.
|
|
84
|
+
2. The agent performs the instructions and automatically calls the
|
|
85
|
+
`context-dropper.next` tool.
|
|
86
|
+
3. **Context Pruning**: When the tool is called, the file is tagged as
|
|
87
|
+
processed. The plugin drops the previous file's context from the chat history
|
|
88
|
+
(saving tokens), and feeds the next file to the agent.
|
|
60
89
|
4. This loop continues until all files are processed.
|
|
61
90
|
|
|
62
91
|
To forcefully stop the loop before it finishes, type **"stop context-dropper"**.
|
|
63
92
|
|
|
64
|
-
##
|
|
93
|
+
## Logging
|
|
65
94
|
|
|
66
|
-
|
|
95
|
+
Plugin activity is written to OpenCode's native log system via the
|
|
96
|
+
`context-dropper` service. To view logs, run OpenCode with debug-level output
|
|
97
|
+
enabled:
|
|
67
98
|
|
|
68
99
|
```bash
|
|
69
|
-
|
|
70
|
-
# rebuild the bundle after changes
|
|
71
|
-
bun run build
|
|
100
|
+
opencode --log-level debug
|
|
72
101
|
```
|
|
73
102
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
```bash
|
|
79
|
-
~/.opencode/context-dropper.log
|
|
80
|
-
```
|
|
81
|
-
|
|
82
|
-
You can monitor these logs in real-time by running the following command in your terminal:
|
|
83
|
-
|
|
84
|
-
```bash
|
|
85
|
-
tail -f ~/.opencode/context-dropper.log
|
|
86
|
-
```
|
|
103
|
+
Unexpected errors are logged at `error` level and are visible at any log level.
|
|
104
|
+
Operational events (dropper lifecycle, context pruning) are logged at `info`
|
|
105
|
+
level.
|
package/dist/index.js
CHANGED
|
@@ -12333,14 +12333,10 @@ function tool(input) {
|
|
|
12333
12333
|
return input;
|
|
12334
12334
|
}
|
|
12335
12335
|
tool.schema = exports_external;
|
|
12336
|
-
// src/index.ts
|
|
12337
|
-
import * as fs from "node:fs";
|
|
12338
|
-
import * as os from "node:os";
|
|
12339
|
-
import * as path4 from "node:path";
|
|
12340
12336
|
// ../package.json
|
|
12341
12337
|
var package_default = {
|
|
12342
12338
|
name: "context-dropper",
|
|
12343
|
-
version: "0.1.
|
|
12339
|
+
version: "0.1.6",
|
|
12344
12340
|
description: "CLI for iterating through a fixed list of files, tracking position, and tagging progress within an AI agent's session.",
|
|
12345
12341
|
author: {
|
|
12346
12342
|
name: "Fardjad Davari",
|
|
@@ -12376,10 +12372,30 @@ var package_default = {
|
|
|
12376
12372
|
// ../src/version/version.ts
|
|
12377
12373
|
var getPackageVersion = () => package_default.version;
|
|
12378
12374
|
|
|
12375
|
+
// src/logger.ts
|
|
12376
|
+
function createLogger(service, client) {
|
|
12377
|
+
return (msg, extra, level = "info") => {
|
|
12378
|
+
client.app.log({
|
|
12379
|
+
body: {
|
|
12380
|
+
service,
|
|
12381
|
+
level,
|
|
12382
|
+
message: msg,
|
|
12383
|
+
...extra !== undefined ? { extra } : {}
|
|
12384
|
+
}
|
|
12385
|
+
}).catch((e) => {
|
|
12386
|
+
console.error(`[${service}] Failed to send log: ${e}`);
|
|
12387
|
+
});
|
|
12388
|
+
};
|
|
12389
|
+
}
|
|
12390
|
+
|
|
12379
12391
|
// src/session.ts
|
|
12380
12392
|
class SessionManager {
|
|
12381
12393
|
sessionStates = new Map;
|
|
12382
12394
|
sessionPruneMap = new Map;
|
|
12395
|
+
log;
|
|
12396
|
+
constructor(log) {
|
|
12397
|
+
this.log = log;
|
|
12398
|
+
}
|
|
12383
12399
|
setSession(sessionId, state) {
|
|
12384
12400
|
this.sessionStates.set(sessionId, state);
|
|
12385
12401
|
}
|
|
@@ -12387,10 +12403,12 @@ class SessionManager {
|
|
|
12387
12403
|
return this.sessionStates.get(sessionId);
|
|
12388
12404
|
}
|
|
12389
12405
|
deleteSession(sessionId) {
|
|
12406
|
+
this.log(`Deleting session ${sessionId}`);
|
|
12390
12407
|
this.sessionStates.delete(sessionId);
|
|
12391
12408
|
this.sessionPruneMap.delete(sessionId);
|
|
12392
12409
|
}
|
|
12393
12410
|
setPruneMessageId(sessionId, messageId) {
|
|
12411
|
+
this.log(`Prune anchor set`, { sessionId, messageId });
|
|
12394
12412
|
this.sessionPruneMap.set(sessionId, messageId);
|
|
12395
12413
|
}
|
|
12396
12414
|
getPruneMessageId(sessionId) {
|
|
@@ -12903,12 +12921,15 @@ class Toolkit {
|
|
|
12903
12921
|
dropperService;
|
|
12904
12922
|
filesetService;
|
|
12905
12923
|
dataDir;
|
|
12906
|
-
|
|
12924
|
+
log;
|
|
12925
|
+
constructor(cwd, log, dropperService, filesetService) {
|
|
12926
|
+
this.log = log;
|
|
12907
12927
|
this.dropperService = dropperService ?? new DefaultDropperService;
|
|
12908
12928
|
this.filesetService = filesetService ?? new DefaultFilesetService;
|
|
12909
12929
|
this.dataDir = path3.resolve(cwd, ".context-dropper");
|
|
12910
12930
|
}
|
|
12911
12931
|
async createDropper(filesetName, dropperName) {
|
|
12932
|
+
this.log(`Creating dropper`, { dropperName, filesetName });
|
|
12912
12933
|
await this.dropperService.create({
|
|
12913
12934
|
dataDir: this.dataDir,
|
|
12914
12935
|
filesetName,
|
|
@@ -12916,6 +12937,7 @@ class Toolkit {
|
|
|
12916
12937
|
});
|
|
12917
12938
|
}
|
|
12918
12939
|
async removeDropper(dropperName) {
|
|
12940
|
+
this.log(`Removing dropper`, { dropperName });
|
|
12919
12941
|
try {
|
|
12920
12942
|
await this.dropperService.remove({
|
|
12921
12943
|
dataDir: this.dataDir,
|
|
@@ -12929,6 +12951,7 @@ class Toolkit {
|
|
|
12929
12951
|
}
|
|
12930
12952
|
}
|
|
12931
12953
|
async tagProcessed(dropperName) {
|
|
12954
|
+
this.log(`Tagging current file as 'processed'`, { dropperName });
|
|
12932
12955
|
await this.dropperService.tag({
|
|
12933
12956
|
dataDir: this.dataDir,
|
|
12934
12957
|
dropperName,
|
|
@@ -12936,6 +12959,7 @@ class Toolkit {
|
|
|
12936
12959
|
});
|
|
12937
12960
|
}
|
|
12938
12961
|
async isDone(dropperName) {
|
|
12962
|
+
this.log(`Checking if done`, { dropperName });
|
|
12939
12963
|
try {
|
|
12940
12964
|
await this.dropperService.isDone({
|
|
12941
12965
|
dataDir: this.dataDir,
|
|
@@ -12947,6 +12971,7 @@ class Toolkit {
|
|
|
12947
12971
|
}
|
|
12948
12972
|
}
|
|
12949
12973
|
async nextFile(dropperName) {
|
|
12974
|
+
this.log(`Advancing to next file`, { dropperName });
|
|
12950
12975
|
await this.dropperService.next({
|
|
12951
12976
|
dataDir: this.dataDir,
|
|
12952
12977
|
dropperName
|
|
@@ -12984,40 +13009,11 @@ ${fileContent}
|
|
|
12984
13009
|
}
|
|
12985
13010
|
|
|
12986
13011
|
// src/index.ts
|
|
12987
|
-
var opencodeDir = path4.join(os.homedir(), ".opencode");
|
|
12988
|
-
if (!fs.existsSync(opencodeDir)) {
|
|
12989
|
-
fs.mkdirSync(opencodeDir, { recursive: true });
|
|
12990
|
-
}
|
|
12991
|
-
var logFile = path4.join(opencodeDir, "context-dropper.log");
|
|
12992
|
-
var MAX_LOG_SIZE_BYTES = 10 * 1024 * 1024;
|
|
12993
|
-
var log = (msg, ...args) => {
|
|
12994
|
-
const timestamp = new Date().toISOString();
|
|
12995
|
-
const formattedArgs = args.length > 0 ? " " + args.map((a) => typeof a === "object" ? JSON.stringify(a) : a).join(" ") : "";
|
|
12996
|
-
const logMessage = `[${timestamp}] [ContextDropper] ${msg}${formattedArgs}
|
|
12997
|
-
`;
|
|
12998
|
-
console.error(`[ContextDropper] ${msg}`, ...args);
|
|
12999
|
-
try {
|
|
13000
|
-
if (fs.existsSync(logFile)) {
|
|
13001
|
-
const stats = fs.statSync(logFile);
|
|
13002
|
-
if (stats.size >= MAX_LOG_SIZE_BYTES) {
|
|
13003
|
-
const content = fs.readFileSync(logFile, "utf-8");
|
|
13004
|
-
const keepLength = Math.floor(content.length / 2);
|
|
13005
|
-
const rotatedContent = content.slice(-keepLength);
|
|
13006
|
-
const firstNewlineIndex = rotatedContent.indexOf(`
|
|
13007
|
-
`);
|
|
13008
|
-
const cleanContent = firstNewlineIndex !== -1 ? rotatedContent.slice(firstNewlineIndex + 1) : rotatedContent;
|
|
13009
|
-
fs.writeFileSync(logFile, cleanContent);
|
|
13010
|
-
}
|
|
13011
|
-
}
|
|
13012
|
-
fs.appendFileSync(logFile, logMessage);
|
|
13013
|
-
} catch (e) {
|
|
13014
|
-
console.error("Failed to write/rotate plugin log file", e);
|
|
13015
|
-
}
|
|
13016
|
-
};
|
|
13017
|
-
var sessionManager = new SessionManager;
|
|
13018
|
-
var toolkit = new Toolkit(process.cwd());
|
|
13019
13012
|
var ContextDropperPlugin = async (ctx) => {
|
|
13020
13013
|
const version2 = getPackageVersion();
|
|
13014
|
+
const log = createLogger("context-dropper", ctx.client);
|
|
13015
|
+
const toolkit = new Toolkit(ctx.worktree, log);
|
|
13016
|
+
const sessionManager = new SessionManager(log);
|
|
13021
13017
|
log(`Plugin initializing! Version: ${version2}`);
|
|
13022
13018
|
setTimeout(() => {
|
|
13023
13019
|
ctx.client.tui.showToast({
|
|
@@ -13027,8 +13023,8 @@ var ContextDropperPlugin = async (ctx) => {
|
|
|
13027
13023
|
variant: "success",
|
|
13028
13024
|
duration: 5000
|
|
13029
13025
|
}
|
|
13030
|
-
}).catch((e) => log("Failed to show toast", e));
|
|
13031
|
-
log("Initialization
|
|
13026
|
+
}).catch((e) => log("Failed to show toast", { error: String(e) }, "warn"));
|
|
13027
|
+
log("Initialization complete", { worktree: ctx.worktree, version: version2 });
|
|
13032
13028
|
}, 1000);
|
|
13033
13029
|
return {
|
|
13034
13030
|
tool: {
|
|
@@ -13045,13 +13041,11 @@ var ContextDropperPlugin = async (ctx) => {
|
|
|
13045
13041
|
instructions: args.instructions
|
|
13046
13042
|
});
|
|
13047
13043
|
try {
|
|
13048
|
-
log(`Removing existing dropper ${dropperName}`);
|
|
13049
13044
|
await toolkit.removeDropper(dropperName);
|
|
13050
|
-
log(`Creating dropper ${dropperName} from fileset ${args.filesetName}`);
|
|
13051
13045
|
await toolkit.createDropper(args.filesetName, dropperName);
|
|
13052
13046
|
return await toolkit.getFilePrompt(dropperName, args.instructions, false);
|
|
13053
13047
|
} catch (error45) {
|
|
13054
|
-
log(`Error in tool execution:
|
|
13048
|
+
log(`Error in tool execution`, { error: error45.message }, "error");
|
|
13055
13049
|
return `Error initializing context-dropper: ${error45.message}`;
|
|
13056
13050
|
}
|
|
13057
13051
|
}
|
|
@@ -13066,22 +13060,19 @@ var ContextDropperPlugin = async (ctx) => {
|
|
|
13066
13060
|
return "No active context-dropper session found. Please initialize one first.";
|
|
13067
13061
|
}
|
|
13068
13062
|
try {
|
|
13069
|
-
log(`Tagging ${state.dropperName} current file as 'processed'`);
|
|
13070
13063
|
await toolkit.tagProcessed(state.dropperName);
|
|
13071
|
-
log(`Checking if ${state.dropperName} is done`);
|
|
13072
13064
|
const isDone = await toolkit.isDone(state.dropperName);
|
|
13073
13065
|
if (isDone) {
|
|
13074
|
-
log(`Session
|
|
13066
|
+
log(`Session completed`, { dropperName: state.dropperName });
|
|
13075
13067
|
sessionManager.deleteSession(sessionId);
|
|
13076
13068
|
return `[Context-Dropper: All files have been processed. Task complete.]`;
|
|
13077
13069
|
}
|
|
13078
|
-
log(`Advancing to next file in ${state.dropperName}`);
|
|
13079
13070
|
await toolkit.nextFile(state.dropperName);
|
|
13080
13071
|
const prompt = await toolkit.getFilePrompt(state.dropperName, state.instructions, true);
|
|
13081
13072
|
sessionManager.setPruneMessageId(sessionId, context.messageID);
|
|
13082
13073
|
return prompt;
|
|
13083
13074
|
} catch (error45) {
|
|
13084
|
-
log(`Error during 'next' processing:
|
|
13075
|
+
log(`Error during 'next' processing`, { error: error45.message }, "error");
|
|
13085
13076
|
return `[Context-Dropper Error: ${error45.message}]`;
|
|
13086
13077
|
}
|
|
13087
13078
|
}
|
|
@@ -13092,7 +13083,10 @@ var ContextDropperPlugin = async (ctx) => {
|
|
|
13092
13083
|
for (const part of output.parts) {
|
|
13093
13084
|
if (part.type === "text") {
|
|
13094
13085
|
const text = part.text.trim().toLowerCase();
|
|
13095
|
-
log(`Processing message chunk
|
|
13086
|
+
log(`Processing message chunk`, {
|
|
13087
|
+
sessionId,
|
|
13088
|
+
startsWithDrop: text.startsWith("/drop")
|
|
13089
|
+
});
|
|
13096
13090
|
if (text.startsWith("/drop ")) {
|
|
13097
13091
|
const originalText = part.text.trim();
|
|
13098
13092
|
const match = originalText.match(/^\/drop\s+([^\s]+)\s+(.+)$/is);
|
|
@@ -13105,9 +13099,7 @@ var ContextDropperPlugin = async (ctx) => {
|
|
|
13105
13099
|
instructions
|
|
13106
13100
|
});
|
|
13107
13101
|
try {
|
|
13108
|
-
log(`Removing existing dropper ${dropperName}`);
|
|
13109
13102
|
await toolkit.removeDropper(dropperName);
|
|
13110
|
-
log(`Creating dropper ${dropperName} from fileset ${filesetName}`);
|
|
13111
13103
|
await toolkit.createDropper(filesetName, dropperName);
|
|
13112
13104
|
const prompt = await toolkit.getFilePrompt(dropperName, instructions, false);
|
|
13113
13105
|
part.text = prompt;
|
|
@@ -13115,7 +13107,9 @@ var ContextDropperPlugin = async (ctx) => {
|
|
|
13115
13107
|
sessionManager.setPruneMessageId(sessionId, output.message.id);
|
|
13116
13108
|
}
|
|
13117
13109
|
} catch (error45) {
|
|
13118
|
-
log(`Error starting context-dropper via /drop
|
|
13110
|
+
log(`Error starting context-dropper via /drop`, {
|
|
13111
|
+
error: error45.message
|
|
13112
|
+
}, "error");
|
|
13119
13113
|
part.text = `Error starting context-dropper: ${error45.message}`;
|
|
13120
13114
|
}
|
|
13121
13115
|
} else {
|
|
@@ -13144,9 +13138,16 @@ var ContextDropperPlugin = async (ctx) => {
|
|
|
13144
13138
|
return;
|
|
13145
13139
|
const pruneStartId = sessionManager.getPruneMessageId(sessionId);
|
|
13146
13140
|
if (pruneStartId) {
|
|
13141
|
+
const totalBefore = output.messages.length;
|
|
13147
13142
|
const index = output.messages.findIndex((m) => m.info && m.info.id === pruneStartId);
|
|
13148
13143
|
if (index !== -1) {
|
|
13149
13144
|
output.messages.splice(0, index);
|
|
13145
|
+
log(`Context pruned`, {
|
|
13146
|
+
sessionId,
|
|
13147
|
+
removed: index,
|
|
13148
|
+
totalBefore,
|
|
13149
|
+
remaining: output.messages.length
|
|
13150
|
+
});
|
|
13150
13151
|
}
|
|
13151
13152
|
}
|
|
13152
13153
|
}
|
package/package.json
CHANGED