opencode-context-dropper-plugin 0.1.6 → 0.2.1
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 +5 -5
- package/dist/index.js +378 -405
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -58,11 +58,11 @@ Once installed, start OpenCode:
|
|
|
58
58
|
opencode
|
|
59
59
|
```
|
|
60
60
|
|
|
61
|
-
You can invoke the context dropper loop inside chat simply by using the
|
|
62
|
-
slash command:
|
|
61
|
+
You can invoke the context dropper loop inside chat simply by using the
|
|
62
|
+
`:context-dropper` slash command:
|
|
63
63
|
|
|
64
64
|
```text
|
|
65
|
-
|
|
65
|
+
:context-dropper <filesetName> <instructions>
|
|
66
66
|
```
|
|
67
67
|
|
|
68
68
|
- `<filesetName>` is the name of a pre-existing fileset in your project.
|
|
@@ -72,7 +72,7 @@ slash command:
|
|
|
72
72
|
**Example:**
|
|
73
73
|
|
|
74
74
|
```text
|
|
75
|
-
|
|
75
|
+
:context-dropper backend-routes Please add try/catch blocks and proper logging to all async functions in this file.
|
|
76
76
|
```
|
|
77
77
|
|
|
78
78
|
### The Automation Loop
|
|
@@ -82,7 +82,7 @@ Once invoked, the plugin completely takes over the context management:
|
|
|
82
82
|
1. It automatically fetches the first file in the fileset and provides it to the
|
|
83
83
|
agent along with your instructions.
|
|
84
84
|
2. The agent performs the instructions and automatically calls the
|
|
85
|
-
`context-
|
|
85
|
+
`context-dropper_next` tool.
|
|
86
86
|
3. **Context Pruning**: When the tool is called, the file is tagged as
|
|
87
87
|
processed. The plugin drops the previous file's context from the chat history
|
|
88
88
|
(saving tokens), and feeds the next file to the agent.
|
package/dist/index.js
CHANGED
|
@@ -12333,46 +12333,13 @@ function tool(input) {
|
|
|
12333
12333
|
return input;
|
|
12334
12334
|
}
|
|
12335
12335
|
tool.schema = exports_external;
|
|
12336
|
-
// ../package.json
|
|
12337
|
-
var package_default = {
|
|
12338
|
-
name: "context-dropper",
|
|
12339
|
-
version: "0.1.6",
|
|
12340
|
-
description: "CLI for iterating through a fixed list of files, tracking position, and tagging progress within an AI agent's session.",
|
|
12341
|
-
author: {
|
|
12342
|
-
name: "Fardjad Davari",
|
|
12343
|
-
email: "public@fardjad.com"
|
|
12344
|
-
},
|
|
12345
|
-
license: "MIT",
|
|
12346
|
-
repository: {
|
|
12347
|
-
type: "git",
|
|
12348
|
-
url: "https://github.com/fardjad/context-dropper"
|
|
12349
|
-
},
|
|
12350
|
-
keywords: ["cli", "context", "llm", "ai", "opencode", "automation"],
|
|
12351
|
-
bin: {
|
|
12352
|
-
"context-dropper": "./dist/index.js"
|
|
12353
|
-
},
|
|
12354
|
-
main: "dist/index.js",
|
|
12355
|
-
module: "dist/index.js",
|
|
12356
|
-
type: "module",
|
|
12357
|
-
files: ["dist/index.js"],
|
|
12358
|
-
scripts: {
|
|
12359
|
-
build: "bun build ./src/index.ts --outdir ./dist --target node",
|
|
12360
|
-
prepack: "npm run build"
|
|
12361
|
-
},
|
|
12362
|
-
devDependencies: {
|
|
12363
|
-
"@types/bun": "latest",
|
|
12364
|
-
typescript: "^5"
|
|
12365
|
-
},
|
|
12366
|
-
dependencies: {
|
|
12367
|
-
"@types/yargs": "^17.0.35",
|
|
12368
|
-
yargs: "^18.0.0"
|
|
12369
|
-
}
|
|
12370
|
-
};
|
|
12371
|
-
|
|
12372
|
-
// ../src/version/version.ts
|
|
12373
|
-
var getPackageVersion = () => package_default.version;
|
|
12374
|
-
|
|
12375
12336
|
// src/logger.ts
|
|
12337
|
+
var toastVariant = {
|
|
12338
|
+
debug: "info",
|
|
12339
|
+
info: "success",
|
|
12340
|
+
warn: "warning",
|
|
12341
|
+
error: "error"
|
|
12342
|
+
};
|
|
12376
12343
|
function createLogger(service, client) {
|
|
12377
12344
|
return (msg, extra, level = "info") => {
|
|
12378
12345
|
client.app.log({
|
|
@@ -12385,40 +12352,286 @@ function createLogger(service, client) {
|
|
|
12385
12352
|
}).catch((e) => {
|
|
12386
12353
|
console.error(`[${service}] Failed to send log: ${e}`);
|
|
12387
12354
|
});
|
|
12355
|
+
if (process.env.CONTEXT_DROPPER_TOAST_LOGS) {
|
|
12356
|
+
client.tui.showToast({
|
|
12357
|
+
body: {
|
|
12358
|
+
title: `[${service}] ${level.toUpperCase()}`,
|
|
12359
|
+
message: msg,
|
|
12360
|
+
variant: toastVariant[level],
|
|
12361
|
+
duration: 4000
|
|
12362
|
+
}
|
|
12363
|
+
}).catch((e) => {
|
|
12364
|
+
console.error(`[${service}] Failed to show toast: ${e}`);
|
|
12365
|
+
});
|
|
12366
|
+
}
|
|
12367
|
+
};
|
|
12368
|
+
}
|
|
12369
|
+
|
|
12370
|
+
// src/message-handler.ts
|
|
12371
|
+
function compilePattern(pattern) {
|
|
12372
|
+
const paramNames = [];
|
|
12373
|
+
const paramTypes = [];
|
|
12374
|
+
const regexSource = pattern.replace(/<(string|number):([^>]+)>/g, (_, type, name) => {
|
|
12375
|
+
paramNames.push(name);
|
|
12376
|
+
paramTypes.push(type);
|
|
12377
|
+
return type === "number" ? "(-?\\d+(?:\\.\\d+)?)" : "(.+?)";
|
|
12378
|
+
});
|
|
12379
|
+
return {
|
|
12380
|
+
regex: new RegExp(`^${regexSource}$`, "is"),
|
|
12381
|
+
paramNames,
|
|
12382
|
+
paramTypes
|
|
12388
12383
|
};
|
|
12389
12384
|
}
|
|
12390
12385
|
|
|
12386
|
+
class MessageHandler {
|
|
12387
|
+
routes = [];
|
|
12388
|
+
use(pattern, handler) {
|
|
12389
|
+
const { regex, paramNames, paramTypes } = compilePattern(pattern);
|
|
12390
|
+
this.routes.push({ regex, paramNames, paramTypes, handler });
|
|
12391
|
+
return this;
|
|
12392
|
+
}
|
|
12393
|
+
get handle() {
|
|
12394
|
+
return async (input, output) => {
|
|
12395
|
+
const sessionId = input.sessionID;
|
|
12396
|
+
const messageId = output.message?.id;
|
|
12397
|
+
for (const part of output.parts) {
|
|
12398
|
+
if (part.type !== "text")
|
|
12399
|
+
continue;
|
|
12400
|
+
const text = part.text.trim();
|
|
12401
|
+
for (const route of this.routes) {
|
|
12402
|
+
const match = text.match(route.regex);
|
|
12403
|
+
if (!match)
|
|
12404
|
+
continue;
|
|
12405
|
+
const params = {};
|
|
12406
|
+
route.paramNames.forEach((name, i) => {
|
|
12407
|
+
const raw = match[i + 1];
|
|
12408
|
+
params[name] = route.paramTypes[i] === "number" ? Number(raw) : raw;
|
|
12409
|
+
});
|
|
12410
|
+
const result = await route.handler(params, {
|
|
12411
|
+
sessionId,
|
|
12412
|
+
messageId,
|
|
12413
|
+
input,
|
|
12414
|
+
output
|
|
12415
|
+
});
|
|
12416
|
+
if (typeof result === "string") {
|
|
12417
|
+
part.text = result;
|
|
12418
|
+
}
|
|
12419
|
+
break;
|
|
12420
|
+
}
|
|
12421
|
+
}
|
|
12422
|
+
};
|
|
12423
|
+
}
|
|
12424
|
+
}
|
|
12425
|
+
|
|
12426
|
+
// src/dropper.ts
|
|
12427
|
+
import path from "node:path";
|
|
12428
|
+
|
|
12429
|
+
class Dropper {
|
|
12430
|
+
filesetName;
|
|
12431
|
+
dropperName;
|
|
12432
|
+
log;
|
|
12433
|
+
dropperService;
|
|
12434
|
+
dataDir;
|
|
12435
|
+
constructor(cwd, filesetName, dropperName, log, dropperService) {
|
|
12436
|
+
this.filesetName = filesetName;
|
|
12437
|
+
this.dropperName = dropperName;
|
|
12438
|
+
this.log = log;
|
|
12439
|
+
this.dropperService = dropperService;
|
|
12440
|
+
this.dataDir = path.resolve(cwd, ".context-dropper");
|
|
12441
|
+
}
|
|
12442
|
+
async create() {
|
|
12443
|
+
this.log(`Creating dropper`, {
|
|
12444
|
+
dropperName: this.dropperName,
|
|
12445
|
+
filesetName: this.filesetName
|
|
12446
|
+
});
|
|
12447
|
+
try {
|
|
12448
|
+
await this.dropperService.remove({
|
|
12449
|
+
dataDir: this.dataDir,
|
|
12450
|
+
dropperName: this.dropperName
|
|
12451
|
+
});
|
|
12452
|
+
} catch (e) {
|
|
12453
|
+
if (e.message && e.message.includes("not found")) {} else {
|
|
12454
|
+
throw e;
|
|
12455
|
+
}
|
|
12456
|
+
}
|
|
12457
|
+
await this.dropperService.create({
|
|
12458
|
+
dataDir: this.dataDir,
|
|
12459
|
+
filesetName: this.filesetName,
|
|
12460
|
+
dropperName: this.dropperName
|
|
12461
|
+
});
|
|
12462
|
+
}
|
|
12463
|
+
async tagProcessed() {
|
|
12464
|
+
this.log(`Tagging current file as 'processed'`, {
|
|
12465
|
+
dropperName: this.dropperName
|
|
12466
|
+
});
|
|
12467
|
+
await this.dropperService.tag({
|
|
12468
|
+
dataDir: this.dataDir,
|
|
12469
|
+
dropperName: this.dropperName,
|
|
12470
|
+
tags: ["processed"]
|
|
12471
|
+
});
|
|
12472
|
+
}
|
|
12473
|
+
async isDone() {
|
|
12474
|
+
this.log(`Checking if done`, { dropperName: this.dropperName });
|
|
12475
|
+
try {
|
|
12476
|
+
await this.dropperService.isDone({
|
|
12477
|
+
dataDir: this.dataDir,
|
|
12478
|
+
dropperName: this.dropperName
|
|
12479
|
+
});
|
|
12480
|
+
return true;
|
|
12481
|
+
} catch (e) {
|
|
12482
|
+
return false;
|
|
12483
|
+
}
|
|
12484
|
+
}
|
|
12485
|
+
async nextFile() {
|
|
12486
|
+
this.log(`Advancing to next file`, { dropperName: this.dropperName });
|
|
12487
|
+
await this.dropperService.next({
|
|
12488
|
+
dataDir: this.dataDir,
|
|
12489
|
+
dropperName: this.dropperName
|
|
12490
|
+
});
|
|
12491
|
+
}
|
|
12492
|
+
async getCurrentFile() {
|
|
12493
|
+
const dump = await this.dropperService.dump({
|
|
12494
|
+
dataDir: this.dataDir,
|
|
12495
|
+
dropperName: this.dropperName
|
|
12496
|
+
});
|
|
12497
|
+
const index = dump.pointer.currentIndex;
|
|
12498
|
+
if (index === null) {
|
|
12499
|
+
throw new Error("No current file found");
|
|
12500
|
+
}
|
|
12501
|
+
const filePath = dump.entries[index]?.path;
|
|
12502
|
+
if (!filePath) {
|
|
12503
|
+
throw new Error("No current file found");
|
|
12504
|
+
}
|
|
12505
|
+
let fileContent = "";
|
|
12506
|
+
try {
|
|
12507
|
+
fileContent = await this.dropperService.show({
|
|
12508
|
+
dataDir: this.dataDir,
|
|
12509
|
+
dropperName: this.dropperName
|
|
12510
|
+
});
|
|
12511
|
+
} catch (e) {
|
|
12512
|
+
fileContent = `Error reading file: ${e.message}`;
|
|
12513
|
+
}
|
|
12514
|
+
return { path: filePath, content: fileContent };
|
|
12515
|
+
}
|
|
12516
|
+
}
|
|
12517
|
+
|
|
12391
12518
|
// src/session.ts
|
|
12519
|
+
class Session {
|
|
12520
|
+
options;
|
|
12521
|
+
log;
|
|
12522
|
+
dropperService;
|
|
12523
|
+
dropper;
|
|
12524
|
+
#pruneMessageId;
|
|
12525
|
+
constructor(options, log, dropperService) {
|
|
12526
|
+
this.options = options;
|
|
12527
|
+
this.log = log;
|
|
12528
|
+
this.dropperService = dropperService;
|
|
12529
|
+
this.dropper = new Dropper(options.cwd, options.filesetName, `opencode-${options.filesetName}-${options.sessionId}`, log, dropperService);
|
|
12530
|
+
}
|
|
12531
|
+
set pruneMessageId(messageId) {
|
|
12532
|
+
this.log(`Prune anchor set`, {
|
|
12533
|
+
sessionId: this.options.sessionId,
|
|
12534
|
+
messageId
|
|
12535
|
+
});
|
|
12536
|
+
this.#pruneMessageId = messageId;
|
|
12537
|
+
}
|
|
12538
|
+
get pruneMessageId() {
|
|
12539
|
+
return this.#pruneMessageId;
|
|
12540
|
+
}
|
|
12541
|
+
pruneMessages(messages) {
|
|
12542
|
+
if (!messages || messages.length === 0)
|
|
12543
|
+
return 0;
|
|
12544
|
+
if (!this.pruneMessageId)
|
|
12545
|
+
return 0;
|
|
12546
|
+
const totalBefore = messages.length;
|
|
12547
|
+
const index = messages.findIndex((m) => m.info && m.info.id === this.pruneMessageId);
|
|
12548
|
+
if (index !== -1) {
|
|
12549
|
+
const assistantMessage = messages[index];
|
|
12550
|
+
if (assistantMessage) {
|
|
12551
|
+
if (assistantMessage.role === "assistant" || assistantMessage.info?.role === "assistant") {
|
|
12552
|
+
if (typeof assistantMessage.content === "string") {
|
|
12553
|
+
assistantMessage.content = "";
|
|
12554
|
+
}
|
|
12555
|
+
if (Array.isArray(assistantMessage.parts)) {
|
|
12556
|
+
assistantMessage.parts = assistantMessage.parts.filter((p) => p.type !== "text" && p.type !== "reasoning");
|
|
12557
|
+
}
|
|
12558
|
+
if (Array.isArray(assistantMessage.content)) {
|
|
12559
|
+
assistantMessage.content = assistantMessage.content.filter((p) => p.type !== "text" && p.type !== "reasoning");
|
|
12560
|
+
}
|
|
12561
|
+
}
|
|
12562
|
+
}
|
|
12563
|
+
messages.splice(0, index);
|
|
12564
|
+
this.log(`Deep context prune completed`, {
|
|
12565
|
+
sessionId: this.options.sessionId,
|
|
12566
|
+
removed: index,
|
|
12567
|
+
totalBefore,
|
|
12568
|
+
remaining: messages.length
|
|
12569
|
+
});
|
|
12570
|
+
return index;
|
|
12571
|
+
}
|
|
12572
|
+
return 0;
|
|
12573
|
+
}
|
|
12574
|
+
async initDropper() {
|
|
12575
|
+
await this.dropper.create();
|
|
12576
|
+
}
|
|
12577
|
+
async getCurrentFile() {
|
|
12578
|
+
return this.dropper.getCurrentFile();
|
|
12579
|
+
}
|
|
12580
|
+
async getPrompt() {
|
|
12581
|
+
const file2 = await this.getCurrentFile();
|
|
12582
|
+
return `<context_dropper_session id="${this.dropperName}">
|
|
12583
|
+
` + `You are currently processing a file injected by Context-Dropper. ` + `**DO NOT use any tools to read this file again.** The complete file content is already provided below.
|
|
12584
|
+
|
|
12585
|
+
` + `<instructions>
|
|
12586
|
+
` + `${this.options.instructions}
|
|
12587
|
+
` + `</instructions>
|
|
12588
|
+
|
|
12589
|
+
` + `<file path="${file2.path}">
|
|
12590
|
+
` + `${file2.content}
|
|
12591
|
+
` + `</file>
|
|
12592
|
+
|
|
12593
|
+
` + `**IMPORTANT:** When you are completely finished fulfilling the instructions for this specific file, ` + `you MUST call the \`context-dropper_next\` tool to get the next file. Do not stop until all files are processed.
|
|
12594
|
+
` + `</context_dropper_session>`;
|
|
12595
|
+
}
|
|
12596
|
+
async tagProcessed() {
|
|
12597
|
+
return this.dropper.tagProcessed();
|
|
12598
|
+
}
|
|
12599
|
+
async isDone() {
|
|
12600
|
+
return this.dropper.isDone();
|
|
12601
|
+
}
|
|
12602
|
+
async nextFile() {
|
|
12603
|
+
return this.dropper.nextFile();
|
|
12604
|
+
}
|
|
12605
|
+
get dropperName() {
|
|
12606
|
+
return this.dropper.dropperName;
|
|
12607
|
+
}
|
|
12608
|
+
}
|
|
12609
|
+
|
|
12392
12610
|
class SessionManager {
|
|
12393
|
-
|
|
12394
|
-
|
|
12611
|
+
dropperService;
|
|
12612
|
+
sessions = new Map;
|
|
12395
12613
|
log;
|
|
12396
|
-
|
|
12614
|
+
cwd;
|
|
12615
|
+
constructor(cwd, log, dropperService) {
|
|
12616
|
+
this.dropperService = dropperService;
|
|
12617
|
+
this.cwd = cwd;
|
|
12397
12618
|
this.log = log;
|
|
12398
12619
|
}
|
|
12399
|
-
|
|
12400
|
-
this.
|
|
12620
|
+
async createSession(sessionId, filesetName, instructions) {
|
|
12621
|
+
const session = new Session({ sessionId, filesetName, instructions, cwd: this.cwd }, this.log, this.dropperService);
|
|
12622
|
+
this.sessions.set(sessionId, session);
|
|
12623
|
+
await session.initDropper();
|
|
12624
|
+
return session;
|
|
12401
12625
|
}
|
|
12402
12626
|
getSession(sessionId) {
|
|
12403
|
-
return this.
|
|
12627
|
+
return this.sessions.get(sessionId);
|
|
12404
12628
|
}
|
|
12405
12629
|
deleteSession(sessionId) {
|
|
12406
12630
|
this.log(`Deleting session ${sessionId}`);
|
|
12407
|
-
this.
|
|
12408
|
-
this.sessionPruneMap.delete(sessionId);
|
|
12409
|
-
}
|
|
12410
|
-
setPruneMessageId(sessionId, messageId) {
|
|
12411
|
-
this.log(`Prune anchor set`, { sessionId, messageId });
|
|
12412
|
-
this.sessionPruneMap.set(sessionId, messageId);
|
|
12413
|
-
}
|
|
12414
|
-
getPruneMessageId(sessionId) {
|
|
12415
|
-
return this.sessionPruneMap.get(sessionId);
|
|
12631
|
+
this.sessions.delete(sessionId);
|
|
12416
12632
|
}
|
|
12417
12633
|
}
|
|
12418
12634
|
|
|
12419
|
-
// src/toolkit.ts
|
|
12420
|
-
import path3 from "node:path";
|
|
12421
|
-
|
|
12422
12635
|
// ../src/dropper/service.ts
|
|
12423
12636
|
import {
|
|
12424
12637
|
mkdir,
|
|
@@ -12429,7 +12642,7 @@ import {
|
|
|
12429
12642
|
writeFile,
|
|
12430
12643
|
access
|
|
12431
12644
|
} from "node:fs/promises";
|
|
12432
|
-
import
|
|
12645
|
+
import path2 from "node:path";
|
|
12433
12646
|
|
|
12434
12647
|
// ../src/file-utils/errors.ts
|
|
12435
12648
|
class AppError extends Error {
|
|
@@ -12499,16 +12712,16 @@ var defaultDropperServiceDeps = {
|
|
|
12499
12712
|
}
|
|
12500
12713
|
};
|
|
12501
12714
|
function getFilesetsDirectory(dataDir) {
|
|
12502
|
-
return
|
|
12715
|
+
return path2.join(dataDir, "filesets");
|
|
12503
12716
|
}
|
|
12504
12717
|
function getDroppersDirectory(dataDir) {
|
|
12505
|
-
return
|
|
12718
|
+
return path2.join(dataDir, "droppers");
|
|
12506
12719
|
}
|
|
12507
12720
|
function getFilesetFilePath(dataDir, filesetName) {
|
|
12508
|
-
return
|
|
12721
|
+
return path2.join(getFilesetsDirectory(dataDir), `${filesetName}.txt`);
|
|
12509
12722
|
}
|
|
12510
12723
|
function getDropperFilePath(dataDir, dropperName) {
|
|
12511
|
-
return
|
|
12724
|
+
return path2.join(getDroppersDirectory(dataDir), `${dropperName}.json`);
|
|
12512
12725
|
}
|
|
12513
12726
|
function parseFilesetContent(content) {
|
|
12514
12727
|
return content.split(/\r?\n/g).map((line) => line.trim()).filter((line) => line.length > 0);
|
|
@@ -12768,392 +12981,152 @@ ${untaggedPaths.join(`
|
|
|
12768
12981
|
return true;
|
|
12769
12982
|
}
|
|
12770
12983
|
}
|
|
12771
|
-
|
|
12772
|
-
|
|
12773
|
-
|
|
12774
|
-
|
|
12775
|
-
|
|
12776
|
-
|
|
12777
|
-
|
|
12778
|
-
|
|
12779
|
-
writeFile as writeFile2,
|
|
12780
|
-
access as access2
|
|
12781
|
-
} from "node:fs/promises";
|
|
12782
|
-
import path2 from "node:path";
|
|
12783
|
-
function isNotFoundError2(error45) {
|
|
12784
|
-
return typeof error45 === "object" && error45 !== null && "code" in error45 && error45.code === "ENOENT";
|
|
12785
|
-
}
|
|
12786
|
-
var defaultFilesetServiceDeps = {
|
|
12787
|
-
ensureDirFn: async (directoryPath) => {
|
|
12788
|
-
await mkdir2(directoryPath, { recursive: true });
|
|
12789
|
-
},
|
|
12790
|
-
fileExistsFn: async (filePath) => {
|
|
12791
|
-
try {
|
|
12792
|
-
await access2(filePath);
|
|
12793
|
-
return true;
|
|
12794
|
-
} catch {
|
|
12795
|
-
return false;
|
|
12796
|
-
}
|
|
12797
|
-
},
|
|
12798
|
-
writeTextFileFn: async (filePath, content) => {
|
|
12799
|
-
await writeFile2(filePath, content, "utf-8");
|
|
12984
|
+
// package.json
|
|
12985
|
+
var package_default = {
|
|
12986
|
+
name: "opencode-context-dropper-plugin",
|
|
12987
|
+
version: "0.2.1",
|
|
12988
|
+
description: "A Context Dropper plugin for OpenCode that automates file iteration context management.",
|
|
12989
|
+
author: {
|
|
12990
|
+
name: "Fardjad Davari",
|
|
12991
|
+
email: "public@fardjad.com"
|
|
12800
12992
|
},
|
|
12801
|
-
|
|
12802
|
-
|
|
12993
|
+
license: "MIT",
|
|
12994
|
+
repository: {
|
|
12995
|
+
type: "git",
|
|
12996
|
+
url: "https://github.com/fardjad/context-dropper.git",
|
|
12997
|
+
directory: "opencode-plugin"
|
|
12803
12998
|
},
|
|
12804
|
-
|
|
12805
|
-
|
|
12806
|
-
|
|
12807
|
-
|
|
12808
|
-
|
|
12809
|
-
|
|
12810
|
-
|
|
12811
|
-
}
|
|
12812
|
-
throw error45;
|
|
12999
|
+
keywords: ["opencode", "plugin", "context-dropper", "llm", "ai"],
|
|
13000
|
+
main: "dist/index.js",
|
|
13001
|
+
module: "dist/index.js",
|
|
13002
|
+
type: "module",
|
|
13003
|
+
exports: {
|
|
13004
|
+
".": {
|
|
13005
|
+
default: "./dist/index.js"
|
|
12813
13006
|
}
|
|
12814
13007
|
},
|
|
12815
|
-
|
|
12816
|
-
|
|
13008
|
+
files: ["dist/index.js"],
|
|
13009
|
+
scripts: {
|
|
13010
|
+
build: "bun build ./src/index.ts --outdir ./dist --target node",
|
|
13011
|
+
prepack: "npm run build"
|
|
12817
13012
|
},
|
|
12818
|
-
|
|
12819
|
-
|
|
12820
|
-
|
|
12821
|
-
|
|
12822
|
-
|
|
12823
|
-
|
|
13013
|
+
devDependencies: {
|
|
13014
|
+
"@types/bun": "^1.3.11",
|
|
13015
|
+
"@types/node": "^25.5.0",
|
|
13016
|
+
typescript: "^6.0.2"
|
|
13017
|
+
},
|
|
13018
|
+
dependencies: {
|
|
13019
|
+
"@opencode-ai/plugin": "^1.3.13",
|
|
13020
|
+
zod: "^4.3.6"
|
|
12824
13021
|
}
|
|
12825
13022
|
};
|
|
12826
|
-
function getFilesetsDirectory2(dataDir) {
|
|
12827
|
-
return path2.join(dataDir, "filesets");
|
|
12828
|
-
}
|
|
12829
|
-
function getDroppersDirectory2(dataDir) {
|
|
12830
|
-
return path2.join(dataDir, "droppers");
|
|
12831
|
-
}
|
|
12832
|
-
function getFilesetFilePath2(dataDir, filesetName) {
|
|
12833
|
-
return path2.join(getFilesetsDirectory2(dataDir), `${filesetName}.txt`);
|
|
12834
|
-
}
|
|
12835
|
-
function parseFilesetContent2(content) {
|
|
12836
|
-
return content.split(/\r?\n/g).map((line) => line.trim()).filter((line) => line.length > 0);
|
|
12837
|
-
}
|
|
12838
|
-
function parseDropperReference(rawJson, dropperPath) {
|
|
12839
|
-
let parsed;
|
|
12840
|
-
try {
|
|
12841
|
-
parsed = JSON.parse(rawJson);
|
|
12842
|
-
} catch {
|
|
12843
|
-
throw new AppError(`Invalid dropper metadata: ${dropperPath}`);
|
|
12844
|
-
}
|
|
12845
|
-
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed) || typeof parsed.fileset !== "string") {
|
|
12846
|
-
throw new AppError(`Invalid dropper metadata: ${dropperPath}`);
|
|
12847
|
-
}
|
|
12848
|
-
return {
|
|
12849
|
-
fileset: parsed.fileset
|
|
12850
|
-
};
|
|
12851
|
-
}
|
|
12852
13023
|
|
|
12853
|
-
|
|
12854
|
-
|
|
12855
|
-
constructor(deps = defaultFilesetServiceDeps) {
|
|
12856
|
-
this.deps = deps;
|
|
12857
|
-
}
|
|
12858
|
-
async importFromList(input) {
|
|
12859
|
-
const filesetsDirectory = getFilesetsDirectory2(input.dataDir);
|
|
12860
|
-
const filesetFilePath = getFilesetFilePath2(input.dataDir, input.name);
|
|
12861
|
-
await this.deps.ensureDirFn(filesetsDirectory);
|
|
12862
|
-
if (await this.deps.fileExistsFn(filesetFilePath)) {
|
|
12863
|
-
throw new AppError(`Fileset already exists: ${input.name}`);
|
|
12864
|
-
}
|
|
12865
|
-
const content = input.normalizedFilePaths.length === 0 ? "" : `${input.normalizedFilePaths.join(`
|
|
12866
|
-
`)}
|
|
12867
|
-
`;
|
|
12868
|
-
await this.deps.writeTextFileFn(filesetFilePath, content);
|
|
12869
|
-
}
|
|
12870
|
-
async list(input) {
|
|
12871
|
-
const filesetPaths = (await this.deps.listFilesFn(getFilesetsDirectory2(input.dataDir))).filter((filePath) => filePath.endsWith(".txt")).sort((a, b) => path2.basename(a).localeCompare(path2.basename(b)));
|
|
12872
|
-
const records = [];
|
|
12873
|
-
for (const filesetPath of filesetPaths) {
|
|
12874
|
-
const name = path2.basename(filesetPath, ".txt");
|
|
12875
|
-
records.push(await this.show({
|
|
12876
|
-
dataDir: input.dataDir,
|
|
12877
|
-
name
|
|
12878
|
-
}));
|
|
12879
|
-
}
|
|
12880
|
-
return records;
|
|
12881
|
-
}
|
|
12882
|
-
async show(input) {
|
|
12883
|
-
const filesetFilePath = getFilesetFilePath2(input.dataDir, input.name);
|
|
12884
|
-
if (!await this.deps.fileExistsFn(filesetFilePath)) {
|
|
12885
|
-
throw new AppError(`Fileset not found: ${input.name}`);
|
|
12886
|
-
}
|
|
12887
|
-
const [content, fileStat] = await Promise.all([
|
|
12888
|
-
this.deps.readTextFileFn(filesetFilePath),
|
|
12889
|
-
this.deps.statFileFn(filesetFilePath)
|
|
12890
|
-
]);
|
|
12891
|
-
return {
|
|
12892
|
-
name: input.name,
|
|
12893
|
-
files: parseFilesetContent2(content),
|
|
12894
|
-
createdAt: fileStat.createdAt,
|
|
12895
|
-
updatedAt: fileStat.updatedAt
|
|
12896
|
-
};
|
|
12897
|
-
}
|
|
12898
|
-
async remove(input) {
|
|
12899
|
-
const filesetFilePath = getFilesetFilePath2(input.dataDir, input.name);
|
|
12900
|
-
if (!await this.deps.fileExistsFn(filesetFilePath)) {
|
|
12901
|
-
throw new AppError(`Fileset not found: ${input.name}`);
|
|
12902
|
-
}
|
|
12903
|
-
const dependentDroppers = [];
|
|
12904
|
-
const dropperPaths = (await this.deps.listFilesFn(getDroppersDirectory2(input.dataDir))).filter((dropperPath) => dropperPath.endsWith(".json"));
|
|
12905
|
-
for (const dropperPath of dropperPaths) {
|
|
12906
|
-
const dropperContent = await this.deps.readTextFileFn(dropperPath);
|
|
12907
|
-
const reference = parseDropperReference(dropperContent, dropperPath);
|
|
12908
|
-
if (reference.fileset === input.name) {
|
|
12909
|
-
dependentDroppers.push(path2.basename(dropperPath, ".json"));
|
|
12910
|
-
}
|
|
12911
|
-
}
|
|
12912
|
-
if (dependentDroppers.length > 0) {
|
|
12913
|
-
throw new AppError(`Cannot remove fileset ${input.name}: referenced by droppers: ${dependentDroppers.join(", ")}`);
|
|
12914
|
-
}
|
|
12915
|
-
await this.deps.deleteFileFn(filesetFilePath);
|
|
12916
|
-
}
|
|
12917
|
-
}
|
|
13024
|
+
// src/version.ts
|
|
13025
|
+
var getPackageVersion = () => package_default.version;
|
|
12918
13026
|
|
|
12919
|
-
// src/
|
|
12920
|
-
class
|
|
12921
|
-
|
|
12922
|
-
filesetService;
|
|
12923
|
-
dataDir;
|
|
13027
|
+
// src/index.ts
|
|
13028
|
+
class Program {
|
|
13029
|
+
sessionManager;
|
|
12924
13030
|
log;
|
|
12925
|
-
|
|
13031
|
+
messageHandler;
|
|
13032
|
+
constructor(sessionManager, log) {
|
|
13033
|
+
this.sessionManager = sessionManager;
|
|
12926
13034
|
this.log = log;
|
|
12927
|
-
this.
|
|
12928
|
-
this.
|
|
12929
|
-
|
|
12930
|
-
|
|
12931
|
-
|
|
12932
|
-
|
|
12933
|
-
|
|
12934
|
-
|
|
12935
|
-
|
|
12936
|
-
|
|
12937
|
-
|
|
12938
|
-
|
|
12939
|
-
async removeDropper(dropperName) {
|
|
12940
|
-
this.log(`Removing dropper`, { dropperName });
|
|
12941
|
-
try {
|
|
12942
|
-
await this.dropperService.remove({
|
|
12943
|
-
dataDir: this.dataDir,
|
|
12944
|
-
dropperName
|
|
12945
|
-
});
|
|
12946
|
-
} catch (e) {
|
|
12947
|
-
if (e.message && e.message.includes("not found")) {
|
|
12948
|
-
return;
|
|
13035
|
+
this.messageHandler = new MessageHandler;
|
|
13036
|
+
this.messageHandler.use(":context-dropper <string:filesetName> <string:instructions>", async ({ filesetName, instructions }, { sessionId, messageId, input }) => {
|
|
13037
|
+
this.log(`Processing :context-dropper command`, { sessionId });
|
|
13038
|
+
const session = await this.sessionManager.createSession(sessionId, String(filesetName), String(instructions));
|
|
13039
|
+
try {
|
|
13040
|
+
const prompt = await session.getPrompt();
|
|
13041
|
+
if (messageId)
|
|
13042
|
+
session.pruneMessageId = messageId;
|
|
13043
|
+
return prompt;
|
|
13044
|
+
} catch (error45) {
|
|
13045
|
+
this.log(`Error handling :context-dropper command`, { error: error45.message }, "error");
|
|
13046
|
+
return `Error handling :context-dropper: ${error45.message}`;
|
|
12949
13047
|
}
|
|
12950
|
-
throw e;
|
|
12951
|
-
}
|
|
12952
|
-
}
|
|
12953
|
-
async tagProcessed(dropperName) {
|
|
12954
|
-
this.log(`Tagging current file as 'processed'`, { dropperName });
|
|
12955
|
-
await this.dropperService.tag({
|
|
12956
|
-
dataDir: this.dataDir,
|
|
12957
|
-
dropperName,
|
|
12958
|
-
tags: ["processed"]
|
|
12959
|
-
});
|
|
12960
|
-
}
|
|
12961
|
-
async isDone(dropperName) {
|
|
12962
|
-
this.log(`Checking if done`, { dropperName });
|
|
12963
|
-
try {
|
|
12964
|
-
await this.dropperService.isDone({
|
|
12965
|
-
dataDir: this.dataDir,
|
|
12966
|
-
dropperName
|
|
12967
|
-
});
|
|
12968
|
-
return true;
|
|
12969
|
-
} catch (e) {
|
|
12970
|
-
return false;
|
|
12971
|
-
}
|
|
12972
|
-
}
|
|
12973
|
-
async nextFile(dropperName) {
|
|
12974
|
-
this.log(`Advancing to next file`, { dropperName });
|
|
12975
|
-
await this.dropperService.next({
|
|
12976
|
-
dataDir: this.dataDir,
|
|
12977
|
-
dropperName
|
|
12978
13048
|
});
|
|
12979
13049
|
}
|
|
12980
|
-
|
|
12981
|
-
|
|
12982
|
-
|
|
12983
|
-
|
|
12984
|
-
|
|
12985
|
-
|
|
12986
|
-
const
|
|
12987
|
-
|
|
12988
|
-
|
|
12989
|
-
|
|
12990
|
-
dataDir: this.dataDir,
|
|
12991
|
-
dropperName
|
|
12992
|
-
});
|
|
12993
|
-
} catch (e) {
|
|
12994
|
-
fileContent = `Error reading file: ${e.message}`;
|
|
12995
|
-
}
|
|
12996
|
-
const header = isNext ? `[Context-Dropper: Advanced to next file]` : `Context-dropper task initialized for session '${dropperName}'.`;
|
|
12997
|
-
return `${header}
|
|
12998
|
-
|
|
12999
|
-
` + `Instructions for this file:
|
|
13000
|
-
${instructions}
|
|
13001
|
-
|
|
13002
|
-
` + `File: ${filePath}
|
|
13003
|
-
|
|
13004
|
-
` + `File Content:
|
|
13005
|
-
${fileContent}
|
|
13006
|
-
|
|
13007
|
-
` + `When you are done with this file, DO NOT just say "DONE". You MUST call the 'context-dropper.next' tool to automatically fetch the next file.`;
|
|
13050
|
+
getActiveSession(messages) {
|
|
13051
|
+
if (!messages || messages.length === 0)
|
|
13052
|
+
return;
|
|
13053
|
+
const firstMessage = messages[0];
|
|
13054
|
+
if (!firstMessage || !firstMessage.info)
|
|
13055
|
+
return;
|
|
13056
|
+
const sessionId = firstMessage.info.sessionID;
|
|
13057
|
+
if (!sessionId)
|
|
13058
|
+
return;
|
|
13059
|
+
return this.sessionManager.getSession(sessionId);
|
|
13008
13060
|
}
|
|
13009
|
-
|
|
13010
|
-
|
|
13011
|
-
|
|
13012
|
-
var ContextDropperPlugin = async (ctx) => {
|
|
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);
|
|
13017
|
-
log(`Plugin initializing! Version: ${version2}`);
|
|
13018
|
-
setTimeout(() => {
|
|
13019
|
-
ctx.client.tui.showToast({
|
|
13020
|
-
body: {
|
|
13021
|
-
title: `Context Dropper v${version2}`,
|
|
13022
|
-
message: "Plugin is active! Type '/drop <filesetName> <instructions>' to start.",
|
|
13023
|
-
variant: "success",
|
|
13024
|
-
duration: 5000
|
|
13025
|
-
}
|
|
13026
|
-
}).catch((e) => log("Failed to show toast", { error: String(e) }, "warn"));
|
|
13027
|
-
log("Initialization complete", { worktree: ctx.worktree, version: version2 });
|
|
13028
|
-
}, 1000);
|
|
13029
|
-
return {
|
|
13030
|
-
tool: {
|
|
13031
|
-
"context-dropper": tool({
|
|
13061
|
+
get tools() {
|
|
13062
|
+
return {
|
|
13063
|
+
"context-dropper_init": tool({
|
|
13032
13064
|
description: "Initializes the context-dropper task.",
|
|
13033
13065
|
args: {
|
|
13034
13066
|
filesetName: tool.schema.string().describe("The name of the fileset to process"),
|
|
13035
13067
|
instructions: tool.schema.string().describe("Instructions on what to do with the files")
|
|
13036
13068
|
},
|
|
13037
13069
|
execute: async (args, context) => {
|
|
13038
|
-
const
|
|
13039
|
-
|
|
13040
|
-
|
|
13041
|
-
instructions: args.instructions
|
|
13042
|
-
});
|
|
13070
|
+
const { filesetName, instructions } = args;
|
|
13071
|
+
const sessionId = context.sessionID;
|
|
13072
|
+
const session = await this.sessionManager.createSession(sessionId, filesetName, instructions);
|
|
13043
13073
|
try {
|
|
13044
|
-
await
|
|
13045
|
-
await toolkit.createDropper(args.filesetName, dropperName);
|
|
13046
|
-
return await toolkit.getFilePrompt(dropperName, args.instructions, false);
|
|
13074
|
+
return await session.getPrompt();
|
|
13047
13075
|
} catch (error45) {
|
|
13048
|
-
log(`Error in
|
|
13049
|
-
return `Error
|
|
13076
|
+
this.log(`Error in context-dropper_init`, { error: error45.message }, "error");
|
|
13077
|
+
return `Error in context-dropper_init: ${error45.message}`;
|
|
13050
13078
|
}
|
|
13051
13079
|
}
|
|
13052
13080
|
}),
|
|
13053
|
-
"context-
|
|
13081
|
+
"context-dropper_next": tool({
|
|
13054
13082
|
description: "Call this tool when you have finished processing the current file to save state, prune context, and fetch the next file.",
|
|
13055
13083
|
args: {},
|
|
13056
13084
|
execute: async (args, context) => {
|
|
13057
13085
|
const sessionId = context.sessionID;
|
|
13058
|
-
const
|
|
13059
|
-
if (!
|
|
13086
|
+
const session = this.sessionManager.getSession(sessionId);
|
|
13087
|
+
if (!session) {
|
|
13060
13088
|
return "No active context-dropper session found. Please initialize one first.";
|
|
13061
13089
|
}
|
|
13062
13090
|
try {
|
|
13063
|
-
await
|
|
13064
|
-
const isDone = await
|
|
13091
|
+
await session.tagProcessed();
|
|
13092
|
+
const isDone = await session.isDone();
|
|
13065
13093
|
if (isDone) {
|
|
13066
|
-
log(`Session completed`, {
|
|
13067
|
-
|
|
13094
|
+
this.log(`Session completed`, {
|
|
13095
|
+
dropperName: session.dropperName
|
|
13096
|
+
});
|
|
13097
|
+
this.sessionManager.deleteSession(sessionId);
|
|
13068
13098
|
return `[Context-Dropper: All files have been processed. Task complete.]`;
|
|
13069
13099
|
}
|
|
13070
|
-
await
|
|
13071
|
-
const prompt = await
|
|
13072
|
-
|
|
13100
|
+
await session.nextFile();
|
|
13101
|
+
const prompt = await session.getPrompt();
|
|
13102
|
+
session.pruneMessageId = context.messageID;
|
|
13073
13103
|
return prompt;
|
|
13074
13104
|
} catch (error45) {
|
|
13075
|
-
log(`Error
|
|
13076
|
-
return `[
|
|
13105
|
+
this.log(`Error in context-dropper_next`, { error: error45.message }, "error");
|
|
13106
|
+
return `[context-dropper_next error: ${error45.message}]`;
|
|
13077
13107
|
}
|
|
13078
13108
|
}
|
|
13079
13109
|
})
|
|
13080
|
-
}
|
|
13081
|
-
|
|
13082
|
-
|
|
13083
|
-
|
|
13084
|
-
|
|
13085
|
-
|
|
13086
|
-
|
|
13087
|
-
|
|
13088
|
-
|
|
13089
|
-
});
|
|
13090
|
-
if (text.startsWith("/drop ")) {
|
|
13091
|
-
const originalText = part.text.trim();
|
|
13092
|
-
const match = originalText.match(/^\/drop\s+([^\s]+)\s+(.+)$/is);
|
|
13093
|
-
if (match) {
|
|
13094
|
-
const filesetName = match[1] || "";
|
|
13095
|
-
const instructions = match[2] || "";
|
|
13096
|
-
const dropperName = `session-${sessionId}`;
|
|
13097
|
-
sessionManager.setSession(sessionId, {
|
|
13098
|
-
dropperName,
|
|
13099
|
-
instructions
|
|
13100
|
-
});
|
|
13101
|
-
try {
|
|
13102
|
-
await toolkit.removeDropper(dropperName);
|
|
13103
|
-
await toolkit.createDropper(filesetName, dropperName);
|
|
13104
|
-
const prompt = await toolkit.getFilePrompt(dropperName, instructions, false);
|
|
13105
|
-
part.text = prompt;
|
|
13106
|
-
if (output.message?.id) {
|
|
13107
|
-
sessionManager.setPruneMessageId(sessionId, output.message.id);
|
|
13108
|
-
}
|
|
13109
|
-
} catch (error45) {
|
|
13110
|
-
log(`Error starting context-dropper via /drop`, {
|
|
13111
|
-
error: error45.message
|
|
13112
|
-
}, "error");
|
|
13113
|
-
part.text = `Error starting context-dropper: ${error45.message}`;
|
|
13114
|
-
}
|
|
13115
|
-
} else {
|
|
13116
|
-
part.text = "Invalid command format. Please use: `/drop <filesetName> <instructions>`";
|
|
13117
|
-
}
|
|
13118
|
-
continue;
|
|
13119
|
-
}
|
|
13120
|
-
if (text.includes("stop context-dropper") || text.includes("stop dropping")) {
|
|
13121
|
-
sessionManager.deleteSession(sessionId);
|
|
13122
|
-
part.text += `
|
|
13123
|
-
|
|
13124
|
-
[Context-Dropper: Process stopped manually by user. State cleared.]`;
|
|
13125
|
-
continue;
|
|
13126
|
-
}
|
|
13127
|
-
}
|
|
13128
|
-
}
|
|
13129
|
-
},
|
|
13130
|
-
"experimental.chat.messages.transform": async (input, output) => {
|
|
13131
|
-
if (!output.messages || output.messages.length === 0)
|
|
13132
|
-
return;
|
|
13133
|
-
const firstMessage = output.messages[0];
|
|
13134
|
-
if (!firstMessage || !firstMessage.info)
|
|
13135
|
-
return;
|
|
13136
|
-
const sessionId = firstMessage.info.sessionID;
|
|
13137
|
-
if (!sessionId)
|
|
13138
|
-
return;
|
|
13139
|
-
const pruneStartId = sessionManager.getPruneMessageId(sessionId);
|
|
13140
|
-
if (pruneStartId) {
|
|
13141
|
-
const totalBefore = output.messages.length;
|
|
13142
|
-
const index = output.messages.findIndex((m) => m.info && m.info.id === pruneStartId);
|
|
13143
|
-
if (index !== -1) {
|
|
13144
|
-
output.messages.splice(0, index);
|
|
13145
|
-
log(`Context pruned`, {
|
|
13146
|
-
sessionId,
|
|
13147
|
-
removed: index,
|
|
13148
|
-
totalBefore,
|
|
13149
|
-
remaining: output.messages.length
|
|
13150
|
-
});
|
|
13151
|
-
}
|
|
13110
|
+
};
|
|
13111
|
+
}
|
|
13112
|
+
get plugin() {
|
|
13113
|
+
return {
|
|
13114
|
+
tool: this.tools,
|
|
13115
|
+
"chat.message": this.messageHandler.handle,
|
|
13116
|
+
"experimental.chat.messages.transform": async (_input, output) => {
|
|
13117
|
+
const activeSession = this.getActiveSession(output.messages);
|
|
13118
|
+
activeSession?.pruneMessages(output.messages);
|
|
13152
13119
|
}
|
|
13153
|
-
}
|
|
13154
|
-
}
|
|
13120
|
+
};
|
|
13121
|
+
}
|
|
13122
|
+
}
|
|
13123
|
+
var src_default = async (ctx) => {
|
|
13124
|
+
const log = createLogger("context-dropper", ctx.client);
|
|
13125
|
+
log(`Plugin initializing! Version: ${getPackageVersion()}`);
|
|
13126
|
+
const dropperService = new DefaultDropperService;
|
|
13127
|
+
const sessionManager = new SessionManager(ctx.worktree, log, dropperService);
|
|
13128
|
+
return new Program(sessionManager, log).plugin;
|
|
13155
13129
|
};
|
|
13156
|
-
var src_default = ContextDropperPlugin;
|
|
13157
13130
|
export {
|
|
13158
13131
|
src_default as default
|
|
13159
13132
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-context-dropper-plugin",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "A Context Dropper plugin for OpenCode that automates file iteration context management.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Fardjad Davari",
|
|
@@ -27,12 +27,12 @@
|
|
|
27
27
|
"prepack": "npm run build"
|
|
28
28
|
},
|
|
29
29
|
"devDependencies": {
|
|
30
|
-
"@types/bun": "
|
|
31
|
-
"@types/node": "^25.
|
|
32
|
-
"typescript": "^
|
|
30
|
+
"@types/bun": "^1.3.11",
|
|
31
|
+
"@types/node": "^25.5.0",
|
|
32
|
+
"typescript": "^6.0.2"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@opencode-ai/plugin": "^1.
|
|
35
|
+
"@opencode-ai/plugin": "^1.3.13",
|
|
36
36
|
"zod": "^4.3.6"
|
|
37
37
|
}
|
|
38
38
|
}
|