opencode-context-dropper-plugin 0.1.6 → 0.2.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.
Files changed (2) hide show
  1. package/dist/index.js +345 -375
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -12336,7 +12336,7 @@ tool.schema = exports_external;
12336
12336
  // ../package.json
12337
12337
  var package_default = {
12338
12338
  name: "context-dropper",
12339
- version: "0.1.6",
12339
+ version: "0.2.0",
12340
12340
  description: "CLI for iterating through a fixed list of files, tracking position, and tagging progress within an AI agent's session.",
12341
12341
  author: {
12342
12342
  name: "Fardjad Davari",
@@ -12373,6 +12373,12 @@ var package_default = {
12373
12373
  var getPackageVersion = () => package_default.version;
12374
12374
 
12375
12375
  // src/logger.ts
12376
+ var toastVariant = {
12377
+ debug: "info",
12378
+ info: "success",
12379
+ warn: "warning",
12380
+ error: "error"
12381
+ };
12376
12382
  function createLogger(service, client) {
12377
12383
  return (msg, extra, level = "info") => {
12378
12384
  client.app.log({
@@ -12385,40 +12391,286 @@ function createLogger(service, client) {
12385
12391
  }).catch((e) => {
12386
12392
  console.error(`[${service}] Failed to send log: ${e}`);
12387
12393
  });
12394
+ if (process.env.CONTEXT_DROPPER_TOAST_LOGS) {
12395
+ client.tui.showToast({
12396
+ body: {
12397
+ title: `[${service}] ${level.toUpperCase()}`,
12398
+ message: msg,
12399
+ variant: toastVariant[level],
12400
+ duration: 4000
12401
+ }
12402
+ }).catch((e) => {
12403
+ console.error(`[${service}] Failed to show toast: ${e}`);
12404
+ });
12405
+ }
12388
12406
  };
12389
12407
  }
12390
12408
 
12409
+ // src/message-handler.ts
12410
+ function compilePattern(pattern) {
12411
+ const paramNames = [];
12412
+ const paramTypes = [];
12413
+ const regexSource = pattern.replace(/<(string|number):([^>]+)>/g, (_, type, name) => {
12414
+ paramNames.push(name);
12415
+ paramTypes.push(type);
12416
+ return type === "number" ? "(-?\\d+(?:\\.\\d+)?)" : "(.+?)";
12417
+ });
12418
+ return {
12419
+ regex: new RegExp(`^${regexSource}$`, "is"),
12420
+ paramNames,
12421
+ paramTypes
12422
+ };
12423
+ }
12424
+
12425
+ class MessageHandler {
12426
+ routes = [];
12427
+ use(pattern, handler) {
12428
+ const { regex, paramNames, paramTypes } = compilePattern(pattern);
12429
+ this.routes.push({ regex, paramNames, paramTypes, handler });
12430
+ return this;
12431
+ }
12432
+ get handle() {
12433
+ return async (input, output) => {
12434
+ const sessionId = input.sessionID;
12435
+ const messageId = output.message?.id;
12436
+ for (const part of output.parts) {
12437
+ if (part.type !== "text")
12438
+ continue;
12439
+ const text = part.text.trim();
12440
+ for (const route of this.routes) {
12441
+ const match = text.match(route.regex);
12442
+ if (!match)
12443
+ continue;
12444
+ const params = {};
12445
+ route.paramNames.forEach((name, i) => {
12446
+ const raw = match[i + 1];
12447
+ params[name] = route.paramTypes[i] === "number" ? Number(raw) : raw;
12448
+ });
12449
+ const result = await route.handler(params, {
12450
+ sessionId,
12451
+ messageId,
12452
+ input,
12453
+ output
12454
+ });
12455
+ if (typeof result === "string") {
12456
+ part.text = result;
12457
+ }
12458
+ break;
12459
+ }
12460
+ }
12461
+ };
12462
+ }
12463
+ }
12464
+
12465
+ // src/dropper.ts
12466
+ import path from "node:path";
12467
+
12468
+ class Dropper {
12469
+ filesetName;
12470
+ dropperName;
12471
+ log;
12472
+ dropperService;
12473
+ dataDir;
12474
+ constructor(cwd, filesetName, dropperName, log, dropperService) {
12475
+ this.filesetName = filesetName;
12476
+ this.dropperName = dropperName;
12477
+ this.log = log;
12478
+ this.dropperService = dropperService;
12479
+ this.dataDir = path.resolve(cwd, ".context-dropper");
12480
+ }
12481
+ async create() {
12482
+ this.log(`Creating dropper`, {
12483
+ dropperName: this.dropperName,
12484
+ filesetName: this.filesetName
12485
+ });
12486
+ try {
12487
+ await this.dropperService.remove({
12488
+ dataDir: this.dataDir,
12489
+ dropperName: this.dropperName
12490
+ });
12491
+ } catch (e) {
12492
+ if (e.message && e.message.includes("not found")) {} else {
12493
+ throw e;
12494
+ }
12495
+ }
12496
+ await this.dropperService.create({
12497
+ dataDir: this.dataDir,
12498
+ filesetName: this.filesetName,
12499
+ dropperName: this.dropperName
12500
+ });
12501
+ }
12502
+ async tagProcessed() {
12503
+ this.log(`Tagging current file as 'processed'`, {
12504
+ dropperName: this.dropperName
12505
+ });
12506
+ await this.dropperService.tag({
12507
+ dataDir: this.dataDir,
12508
+ dropperName: this.dropperName,
12509
+ tags: ["processed"]
12510
+ });
12511
+ }
12512
+ async isDone() {
12513
+ this.log(`Checking if done`, { dropperName: this.dropperName });
12514
+ try {
12515
+ await this.dropperService.isDone({
12516
+ dataDir: this.dataDir,
12517
+ dropperName: this.dropperName
12518
+ });
12519
+ return true;
12520
+ } catch (e) {
12521
+ return false;
12522
+ }
12523
+ }
12524
+ async nextFile() {
12525
+ this.log(`Advancing to next file`, { dropperName: this.dropperName });
12526
+ await this.dropperService.next({
12527
+ dataDir: this.dataDir,
12528
+ dropperName: this.dropperName
12529
+ });
12530
+ }
12531
+ async getCurrentFile() {
12532
+ const dump = await this.dropperService.dump({
12533
+ dataDir: this.dataDir,
12534
+ dropperName: this.dropperName
12535
+ });
12536
+ const index = dump.pointer.currentIndex;
12537
+ if (index === null) {
12538
+ throw new Error("No current file found");
12539
+ }
12540
+ const filePath = dump.entries[index]?.path;
12541
+ if (!filePath) {
12542
+ throw new Error("No current file found");
12543
+ }
12544
+ let fileContent = "";
12545
+ try {
12546
+ fileContent = await this.dropperService.show({
12547
+ dataDir: this.dataDir,
12548
+ dropperName: this.dropperName
12549
+ });
12550
+ } catch (e) {
12551
+ fileContent = `Error reading file: ${e.message}`;
12552
+ }
12553
+ return { path: filePath, content: fileContent };
12554
+ }
12555
+ }
12556
+
12391
12557
  // src/session.ts
12558
+ class Session {
12559
+ options;
12560
+ log;
12561
+ dropperService;
12562
+ dropper;
12563
+ #pruneMessageId;
12564
+ constructor(options, log, dropperService) {
12565
+ this.options = options;
12566
+ this.log = log;
12567
+ this.dropperService = dropperService;
12568
+ this.dropper = new Dropper(options.cwd, options.filesetName, `opencode-${options.filesetName}-${options.sessionId}`, log, dropperService);
12569
+ }
12570
+ set pruneMessageId(messageId) {
12571
+ this.log(`Prune anchor set`, {
12572
+ sessionId: this.options.sessionId,
12573
+ messageId
12574
+ });
12575
+ this.#pruneMessageId = messageId;
12576
+ }
12577
+ get pruneMessageId() {
12578
+ return this.#pruneMessageId;
12579
+ }
12580
+ pruneMessages(messages) {
12581
+ if (!messages || messages.length === 0)
12582
+ return 0;
12583
+ if (!this.pruneMessageId)
12584
+ return 0;
12585
+ const totalBefore = messages.length;
12586
+ const index = messages.findIndex((m) => m.info && m.info.id === this.pruneMessageId);
12587
+ if (index !== -1) {
12588
+ const assistantMessage = messages[index];
12589
+ if (assistantMessage) {
12590
+ if (assistantMessage.role === "assistant" || assistantMessage.info?.role === "assistant") {
12591
+ if (typeof assistantMessage.content === "string") {
12592
+ assistantMessage.content = "";
12593
+ }
12594
+ if (Array.isArray(assistantMessage.parts)) {
12595
+ assistantMessage.parts = assistantMessage.parts.filter((p) => p.type !== "text" && p.type !== "reasoning");
12596
+ }
12597
+ if (Array.isArray(assistantMessage.content)) {
12598
+ assistantMessage.content = assistantMessage.content.filter((p) => p.type !== "text" && p.type !== "reasoning");
12599
+ }
12600
+ }
12601
+ }
12602
+ messages.splice(0, index);
12603
+ this.log(`Deep context prune completed`, {
12604
+ sessionId: this.options.sessionId,
12605
+ removed: index,
12606
+ totalBefore,
12607
+ remaining: messages.length
12608
+ });
12609
+ return index;
12610
+ }
12611
+ return 0;
12612
+ }
12613
+ async initDropper() {
12614
+ await this.dropper.create();
12615
+ }
12616
+ async getCurrentFile() {
12617
+ return this.dropper.getCurrentFile();
12618
+ }
12619
+ async getPrompt() {
12620
+ const file2 = await this.getCurrentFile();
12621
+ return `<context_dropper_session id="${this.dropperName}">
12622
+ ` + `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.
12623
+
12624
+ ` + `<instructions>
12625
+ ` + `${this.options.instructions}
12626
+ ` + `</instructions>
12627
+
12628
+ ` + `<file path="${file2.path}">
12629
+ ` + `${file2.content}
12630
+ ` + `</file>
12631
+
12632
+ ` + `**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.
12633
+ ` + `</context_dropper_session>`;
12634
+ }
12635
+ async tagProcessed() {
12636
+ return this.dropper.tagProcessed();
12637
+ }
12638
+ async isDone() {
12639
+ return this.dropper.isDone();
12640
+ }
12641
+ async nextFile() {
12642
+ return this.dropper.nextFile();
12643
+ }
12644
+ get dropperName() {
12645
+ return this.dropper.dropperName;
12646
+ }
12647
+ }
12648
+
12392
12649
  class SessionManager {
12393
- sessionStates = new Map;
12394
- sessionPruneMap = new Map;
12650
+ dropperService;
12651
+ sessions = new Map;
12395
12652
  log;
12396
- constructor(log) {
12653
+ cwd;
12654
+ constructor(cwd, log, dropperService) {
12655
+ this.dropperService = dropperService;
12656
+ this.cwd = cwd;
12397
12657
  this.log = log;
12398
12658
  }
12399
- setSession(sessionId, state) {
12400
- this.sessionStates.set(sessionId, state);
12659
+ async createSession(sessionId, filesetName, instructions) {
12660
+ const session = new Session({ sessionId, filesetName, instructions, cwd: this.cwd }, this.log, this.dropperService);
12661
+ this.sessions.set(sessionId, session);
12662
+ await session.initDropper();
12663
+ return session;
12401
12664
  }
12402
12665
  getSession(sessionId) {
12403
- return this.sessionStates.get(sessionId);
12666
+ return this.sessions.get(sessionId);
12404
12667
  }
12405
12668
  deleteSession(sessionId) {
12406
12669
  this.log(`Deleting session ${sessionId}`);
12407
- this.sessionStates.delete(sessionId);
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);
12670
+ this.sessions.delete(sessionId);
12416
12671
  }
12417
12672
  }
12418
12673
 
12419
- // src/toolkit.ts
12420
- import path3 from "node:path";
12421
-
12422
12674
  // ../src/dropper/service.ts
12423
12675
  import {
12424
12676
  mkdir,
@@ -12429,7 +12681,7 @@ import {
12429
12681
  writeFile,
12430
12682
  access
12431
12683
  } from "node:fs/promises";
12432
- import path from "node:path";
12684
+ import path2 from "node:path";
12433
12685
 
12434
12686
  // ../src/file-utils/errors.ts
12435
12687
  class AppError extends Error {
@@ -12499,16 +12751,16 @@ var defaultDropperServiceDeps = {
12499
12751
  }
12500
12752
  };
12501
12753
  function getFilesetsDirectory(dataDir) {
12502
- return path.join(dataDir, "filesets");
12754
+ return path2.join(dataDir, "filesets");
12503
12755
  }
12504
12756
  function getDroppersDirectory(dataDir) {
12505
- return path.join(dataDir, "droppers");
12757
+ return path2.join(dataDir, "droppers");
12506
12758
  }
12507
12759
  function getFilesetFilePath(dataDir, filesetName) {
12508
- return path.join(getFilesetsDirectory(dataDir), `${filesetName}.txt`);
12760
+ return path2.join(getFilesetsDirectory(dataDir), `${filesetName}.txt`);
12509
12761
  }
12510
12762
  function getDropperFilePath(dataDir, dropperName) {
12511
- return path.join(getDroppersDirectory(dataDir), `${dropperName}.json`);
12763
+ return path2.join(getDroppersDirectory(dataDir), `${dropperName}.json`);
12512
12764
  }
12513
12765
  function parseFilesetContent(content) {
12514
12766
  return content.split(/\r?\n/g).map((line) => line.trim()).filter((line) => line.length > 0);
@@ -12769,391 +13021,109 @@ ${untaggedPaths.join(`
12769
13021
  }
12770
13022
  }
12771
13023
 
12772
- // ../src/fileset/service.ts
12773
- import {
12774
- mkdir as mkdir2,
12775
- readdir as readdir2,
12776
- rm as rm2,
12777
- stat as stat2,
12778
- readFile as readFile2,
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");
12800
- },
12801
- readTextFileFn: async (filePath) => {
12802
- return await readFile2(filePath, "utf-8");
12803
- },
12804
- listFilesFn: async (directoryPath) => {
12805
- try {
12806
- const names = await readdir2(directoryPath);
12807
- return names.map((name) => path2.join(directoryPath, name));
12808
- } catch (error45) {
12809
- if (isNotFoundError2(error45)) {
12810
- return [];
12811
- }
12812
- throw error45;
12813
- }
12814
- },
12815
- deleteFileFn: async (filePath) => {
12816
- await rm2(filePath);
12817
- },
12818
- statFileFn: async (filePath) => {
12819
- const fileStat = await stat2(filePath);
12820
- return {
12821
- createdAt: fileStat.ctime.toISOString(),
12822
- updatedAt: fileStat.mtime.toISOString()
12823
- };
12824
- }
12825
- };
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
-
12853
- class DefaultFilesetService {
12854
- deps;
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
- }
12918
-
12919
- // src/toolkit.ts
12920
- class Toolkit {
12921
- dropperService;
12922
- filesetService;
12923
- dataDir;
13024
+ // src/index.ts
13025
+ class Program {
13026
+ sessionManager;
12924
13027
  log;
12925
- constructor(cwd, log, dropperService, filesetService) {
13028
+ messageHandler;
13029
+ constructor(sessionManager, log) {
13030
+ this.sessionManager = sessionManager;
12926
13031
  this.log = log;
12927
- this.dropperService = dropperService ?? new DefaultDropperService;
12928
- this.filesetService = filesetService ?? new DefaultFilesetService;
12929
- this.dataDir = path3.resolve(cwd, ".context-dropper");
12930
- }
12931
- async createDropper(filesetName, dropperName) {
12932
- this.log(`Creating dropper`, { dropperName, filesetName });
12933
- await this.dropperService.create({
12934
- dataDir: this.dataDir,
12935
- filesetName,
12936
- dropperName
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;
13032
+ this.messageHandler = new MessageHandler;
13033
+ this.messageHandler.use(":context-dropper <string:filesetName> <string:instructions>", async ({ filesetName, instructions }, { sessionId, messageId, input }) => {
13034
+ this.log(`Processing :context-dropper command`, { sessionId });
13035
+ const session = await this.sessionManager.createSession(sessionId, String(filesetName), String(instructions));
13036
+ try {
13037
+ const prompt = await session.getPrompt();
13038
+ if (messageId)
13039
+ session.pruneMessageId = messageId;
13040
+ return prompt;
13041
+ } catch (error45) {
13042
+ this.log(`Error handling :context-dropper command`, { error: error45.message }, "error");
13043
+ return `Error handling :context-dropper: ${error45.message}`;
12949
13044
  }
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
13045
  });
12979
13046
  }
12980
- async getFilePrompt(dropperName, instructions, isNext = false) {
12981
- const dump = await this.dropperService.dump({
12982
- dataDir: this.dataDir,
12983
- dropperName
12984
- });
12985
- const index = dump.pointer.currentIndex;
12986
- const filePath = index !== null && index >= 0 && index < dump.entries.length ? dump.entries[index]?.path ?? "Unknown File" : "Unknown File";
12987
- let fileContent = "";
12988
- try {
12989
- fileContent = await this.dropperService.show({
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.`;
13047
+ getActiveSession(messages) {
13048
+ if (!messages || messages.length === 0)
13049
+ return;
13050
+ const firstMessage = messages[0];
13051
+ if (!firstMessage || !firstMessage.info)
13052
+ return;
13053
+ const sessionId = firstMessage.info.sessionID;
13054
+ if (!sessionId)
13055
+ return;
13056
+ return this.sessionManager.getSession(sessionId);
13008
13057
  }
13009
- }
13010
-
13011
- // src/index.ts
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({
13058
+ get tools() {
13059
+ return {
13060
+ "context-dropper_init": tool({
13032
13061
  description: "Initializes the context-dropper task.",
13033
13062
  args: {
13034
13063
  filesetName: tool.schema.string().describe("The name of the fileset to process"),
13035
13064
  instructions: tool.schema.string().describe("Instructions on what to do with the files")
13036
13065
  },
13037
13066
  execute: async (args, context) => {
13038
- const dropperName = `session-${context.sessionID}`;
13039
- sessionManager.setSession(context.sessionID, {
13040
- dropperName,
13041
- instructions: args.instructions
13042
- });
13067
+ const { filesetName, instructions } = args;
13068
+ const sessionId = context.sessionID;
13069
+ const session = await this.sessionManager.createSession(sessionId, filesetName, instructions);
13043
13070
  try {
13044
- await toolkit.removeDropper(dropperName);
13045
- await toolkit.createDropper(args.filesetName, dropperName);
13046
- return await toolkit.getFilePrompt(dropperName, args.instructions, false);
13071
+ return await session.getPrompt();
13047
13072
  } catch (error45) {
13048
- log(`Error in tool execution`, { error: error45.message }, "error");
13049
- return `Error initializing context-dropper: ${error45.message}`;
13073
+ this.log(`Error in context-dropper_init`, { error: error45.message }, "error");
13074
+ return `Error in context-dropper_init: ${error45.message}`;
13050
13075
  }
13051
13076
  }
13052
13077
  }),
13053
- "context-dropper.next": tool({
13078
+ "context-dropper_next": tool({
13054
13079
  description: "Call this tool when you have finished processing the current file to save state, prune context, and fetch the next file.",
13055
13080
  args: {},
13056
13081
  execute: async (args, context) => {
13057
13082
  const sessionId = context.sessionID;
13058
- const state = sessionManager.getSession(sessionId);
13059
- if (!state) {
13083
+ const session = this.sessionManager.getSession(sessionId);
13084
+ if (!session) {
13060
13085
  return "No active context-dropper session found. Please initialize one first.";
13061
13086
  }
13062
13087
  try {
13063
- await toolkit.tagProcessed(state.dropperName);
13064
- const isDone = await toolkit.isDone(state.dropperName);
13088
+ await session.tagProcessed();
13089
+ const isDone = await session.isDone();
13065
13090
  if (isDone) {
13066
- log(`Session completed`, { dropperName: state.dropperName });
13067
- sessionManager.deleteSession(sessionId);
13091
+ this.log(`Session completed`, {
13092
+ dropperName: session.dropperName
13093
+ });
13094
+ this.sessionManager.deleteSession(sessionId);
13068
13095
  return `[Context-Dropper: All files have been processed. Task complete.]`;
13069
13096
  }
13070
- await toolkit.nextFile(state.dropperName);
13071
- const prompt = await toolkit.getFilePrompt(state.dropperName, state.instructions, true);
13072
- sessionManager.setPruneMessageId(sessionId, context.messageID);
13097
+ await session.nextFile();
13098
+ const prompt = await session.getPrompt();
13099
+ session.pruneMessageId = context.messageID;
13073
13100
  return prompt;
13074
13101
  } catch (error45) {
13075
- log(`Error during 'next' processing`, { error: error45.message }, "error");
13076
- return `[Context-Dropper Error: ${error45.message}]`;
13102
+ this.log(`Error in context-dropper_next`, { error: error45.message }, "error");
13103
+ return `[context-dropper_next error: ${error45.message}]`;
13077
13104
  }
13078
13105
  }
13079
13106
  })
13080
- },
13081
- "chat.message": async (input, output) => {
13082
- const sessionId = input.sessionID;
13083
- for (const part of output.parts) {
13084
- if (part.type === "text") {
13085
- const text = part.text.trim().toLowerCase();
13086
- log(`Processing message chunk`, {
13087
- sessionId,
13088
- startsWithDrop: text.startsWith("/drop")
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
- }
13107
+ };
13108
+ }
13109
+ get plugin() {
13110
+ return {
13111
+ tool: this.tools,
13112
+ "chat.message": this.messageHandler.handle,
13113
+ "experimental.chat.messages.transform": async (_input, output) => {
13114
+ const activeSession = this.getActiveSession(output.messages);
13115
+ activeSession?.pruneMessages(output.messages);
13152
13116
  }
13153
- }
13154
- };
13117
+ };
13118
+ }
13119
+ }
13120
+ var src_default = async (ctx) => {
13121
+ const log = createLogger("context-dropper", ctx.client);
13122
+ log(`Plugin initializing! Version: ${getPackageVersion()}`);
13123
+ const dropperService = new DefaultDropperService;
13124
+ const sessionManager = new SessionManager(ctx.worktree, log, dropperService);
13125
+ return new Program(sessionManager, log).plugin;
13155
13126
  };
13156
- var src_default = ContextDropperPlugin;
13157
13127
  export {
13158
13128
  src_default as default
13159
13129
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-context-dropper-plugin",
3
- "version": "0.1.6",
3
+ "version": "0.2.0",
4
4
  "description": "A Context Dropper plugin for OpenCode that automates file iteration context management.",
5
5
  "author": {
6
6
  "name": "Fardjad Davari",