@jmylchreest/aide-plugin 0.0.42 → 0.0.44

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.
@@ -26,6 +26,7 @@
26
26
  */
27
27
 
28
28
  import { execFileSync } from "child_process";
29
+ import { join } from "path";
29
30
  import { findAideBinary } from "../core/aide-client.js";
30
31
  import {
31
32
  ensureDirectories,
@@ -66,6 +67,7 @@ import {
66
67
  buildSummaryFromPartials,
67
68
  cleanupPartials,
68
69
  } from "../core/partial-memory.js";
70
+ import { ContextPruningTracker } from "../core/context-pruning/index.js";
69
71
  import type { MemoryInjection, SessionState } from "../core/types.js";
70
72
  import type {
71
73
  Hooks,
@@ -114,6 +116,8 @@ interface AideState {
114
116
  /** Per-session metadata for agent-like tracking */
115
117
  sessionInfoMap: Map<string, SessionInfo>;
116
118
  client: OpenCodeClient;
119
+ /** Context pruning tracker for dedup/supersede/purge of tool outputs */
120
+ pruningTracker: ContextPruningTracker;
117
121
  }
118
122
 
119
123
  /**
@@ -142,6 +146,7 @@ export async function createHooks(
142
146
  lastUserPrompt: null,
143
147
  sessionInfoMap: new Map(),
144
148
  client,
149
+ pruningTracker: new ContextPruningTracker(cwd),
145
150
  };
146
151
 
147
152
  // Run one-time initialization (directories, binary, config)
@@ -184,16 +189,49 @@ function createConfigHandler(
184
189
  // Discover all skills and register them as OpenCode commands
185
190
  const skills = discoverSkills(state.cwd, state.pluginRoot ?? undefined);
186
191
 
192
+ // Register our skill directories with OpenCode's native skill discovery
193
+ // as a fallback, so the native `skill` tool can also find them.
194
+ const skillsConfig = (input as Record<string, unknown>).skills as
195
+ | { paths?: string[]; urls?: string[] }
196
+ | undefined;
197
+ const existingPaths = skillsConfig?.paths ?? [];
198
+ const aidePaths = [
199
+ join(state.cwd, ".aide", "skills"),
200
+ join(state.cwd, "skills"),
201
+ ];
202
+ if (state.pluginRoot) {
203
+ aidePaths.push(join(state.pluginRoot, "skills"));
204
+ }
205
+ // Only add paths that aren't already registered
206
+ const newPaths = aidePaths.filter((p) => !existingPaths.includes(p));
207
+ if (newPaths.length > 0) {
208
+ (input as Record<string, unknown>).skills = {
209
+ ...skillsConfig,
210
+ paths: [...existingPaths, ...newPaths],
211
+ };
212
+ }
213
+
187
214
  if (!input.command) {
188
215
  input.command = {};
189
216
  }
190
217
 
191
218
  for (const skill of skills) {
219
+ // Skip skills restricted to other platforms
220
+ if (
221
+ skill.platforms &&
222
+ skill.platforms.length > 0 &&
223
+ !skill.platforms.includes("opencode")
224
+ ) {
225
+ continue;
226
+ }
192
227
  const commandName = `aide:${skill.name}`;
193
228
  // Only register if not already defined (user config takes priority)
194
229
  if (!input.command[commandName]) {
195
230
  input.command[commandName] = {
196
- template: `Activate the aide "${skill.name}" skill. {{arguments}}`,
231
+ // IMPORTANT: Template wording must NOT trigger the native "skill" tool.
232
+ // The actual instructions are injected into the system prompt by
233
+ // createCommandHandler → pendingSkillsContext → createSystemTransformHandler.
234
+ template: `Follow the aide "${skill.name}" instructions that have been injected into your system prompt. Do NOT use the skill tool. {{arguments}}`,
197
235
  description: skill.description || `aide ${skill.name} skill`,
198
236
  };
199
237
  }
@@ -234,9 +272,18 @@ function createCommandHandler(state: AideState): (
234
272
  // Format the skill content for injection
235
273
  const context = formatSkillsContext([skill]);
236
274
 
237
- // Store for system transform injection
275
+ // Store for system transform injection (into system prompt)
238
276
  state.pendingSkillsContext = context;
239
277
 
278
+ // Also inject into the command output parts so the model sees the
279
+ // instructions directly in the user message. This avoids reliance
280
+ // on the system transform alone and prevents the model from trying
281
+ // to call the native "skill" tool to find the instructions.
282
+ output.parts.push({
283
+ type: "text",
284
+ text: `<aide-instructions>\n${context}\n</aide-instructions>`,
285
+ });
286
+
240
287
  // Also store the arguments as the user prompt for the transform
241
288
  if (args) {
242
289
  state.lastUserPrompt = args;
@@ -299,7 +346,7 @@ function initializeAide(state: AideState): void {
299
346
  state.binary,
300
347
  state.cwd,
301
348
  projectName,
302
- 3,
349
+ 2,
303
350
  config,
304
351
  );
305
352
  }
@@ -506,14 +553,20 @@ async function handleSessionIdle(
506
553
  let summary: string | null = null;
507
554
 
508
555
  if (partials.length > 0) {
509
- const commits = getSessionCommits(state.cwd);
556
+ const commits = getSessionCommits(
557
+ state.cwd,
558
+ state.sessionState?.startedAt,
559
+ );
510
560
  summary = buildSummaryFromPartials(partials, commits, []);
511
561
  debug(SOURCE, `Built summary from ${partials.length} partials`);
512
562
  }
513
563
 
514
564
  // Fall back to state-only summary if no partials
515
565
  if (!summary) {
516
- summary = buildSessionSummaryFromState(state.cwd);
566
+ summary = buildSessionSummaryFromState(
567
+ state.cwd,
568
+ state.sessionState?.startedAt,
569
+ );
517
570
  }
518
571
 
519
572
  if (summary) {
@@ -584,7 +637,7 @@ async function handleMessagePartUpdated(
584
637
  state.lastUserPrompt = prompt;
585
638
 
586
639
  const skills = discoverSkills(state.cwd, state.pluginRoot ?? undefined);
587
- const matched = matchSkills(prompt, skills, 3);
640
+ const matched = matchSkills(prompt, skills, 3, "opencode");
588
641
 
589
642
  if (matched.length > 0) {
590
643
  try {
@@ -704,6 +757,29 @@ function createToolAfterHandler(
704
757
  debug(SOURCE, `Partial memory write failed (non-fatal): ${err}`);
705
758
  }
706
759
 
760
+ // Context pruning: dedup/supersede/purge tool outputs
761
+ try {
762
+ const toolArgs = (_output.metadata?.args || {}) as Record<
763
+ string,
764
+ unknown
765
+ >;
766
+ const pruneResult = state.pruningTracker.process(
767
+ input.callID,
768
+ input.tool,
769
+ toolArgs,
770
+ _output.output,
771
+ );
772
+ if (pruneResult.modified) {
773
+ _output.output = pruneResult.output;
774
+ debug(
775
+ SOURCE,
776
+ `Context pruning [${pruneResult.strategy}]: saved ${pruneResult.bytesSaved} bytes for ${input.tool}`,
777
+ );
778
+ }
779
+ } catch (err) {
780
+ debug(SOURCE, `Context pruning failed (non-fatal): ${err}`);
781
+ }
782
+
707
783
  // Comment checker: detect excessive comments in Write/Edit output
708
784
  try {
709
785
  const toolArgs = (_output.metadata?.args || {}) as Record<
@@ -742,6 +818,10 @@ function createCompactionHandler(
742
818
  output: { context: string[]; prompt?: string },
743
819
  ) => Promise<void> {
744
820
  return async (input, output) => {
821
+ // Reset context pruning tracker — compaction clears the conversation
822
+ // history, so dedup/supersede references would be stale.
823
+ state.pruningTracker.reset();
824
+
745
825
  // Save state snapshot before compaction
746
826
  if (state.binary) {
747
827
  saveStateSnapshot(state.binary, state.cwd, input.sessionID);
@@ -758,7 +838,10 @@ function createCompactionHandler(
758
838
  let summary: string | null = null;
759
839
 
760
840
  if (partials.length > 0) {
761
- const commits = getSessionCommits(state.cwd);
841
+ const commits = getSessionCommits(
842
+ state.cwd,
843
+ state.sessionState?.startedAt,
844
+ );
762
845
  summary = buildSummaryFromPartials(partials, commits, []);
763
846
  debug(
764
847
  SOURCE,
@@ -768,7 +851,10 @@ function createCompactionHandler(
768
851
 
769
852
  // Fall back to state-only summary if no partials
770
853
  if (!summary) {
771
- summary = buildSessionSummaryFromState(state.cwd);
854
+ summary = buildSessionSummaryFromState(
855
+ state.cwd,
856
+ state.sessionState?.startedAt,
857
+ );
772
858
  }
773
859
 
774
860
  if (summary) {
@@ -802,7 +888,7 @@ function createCompactionHandler(
802
888
  state.binary,
803
889
  state.cwd,
804
890
  projectName,
805
- 3,
891
+ 2,
806
892
  );
807
893
  state.memories = freshMemories;
808
894
  state.welcomeContext = buildWelcomeContext(
@@ -844,6 +930,17 @@ function createSystemTransformHandler(
844
930
  output.system.push(state.welcomeContext);
845
931
  }
846
932
 
933
+ // Inject context pruning notes only after the first prune fires.
934
+ // Avoids adding ~280 bytes to every session that never triggers pruning.
935
+ if (state.pruningTracker.getStats().prunedCalls > 0) {
936
+ output.system.push(`<aide-context-pruning>
937
+ Tool outputs may contain these tags from aide's context optimization:
938
+ - [aide:dedup] — This output is identical to a previous call. Refer to the earlier result.
939
+ - [aide:supersede] — A prior Read of this file is now stale after a Write/Edit.
940
+ - [aide:purge] — Large error output was trimmed. Re-run the command for full output.
941
+ </aide-context-pruning>`);
942
+ }
943
+
847
944
  // Inject per-session context only when swarm mode is active and session
848
945
  // has been assigned a worktree/role (e.g., by swarm orchestration).
849
946
  // Without this guard, every normal session would incorrectly be told
@@ -894,7 +991,12 @@ function createSystemTransformHandler(
894
991
  } else if (state.lastUserPrompt) {
895
992
  try {
896
993
  const skills = discoverSkills(state.cwd, state.pluginRoot ?? undefined);
897
- const matched = matchSkills(state.lastUserPrompt, skills, 3);
994
+ const matched = matchSkills(
995
+ state.lastUserPrompt,
996
+ skills,
997
+ 3,
998
+ "opencode",
999
+ );
898
1000
  if (matched.length > 0) {
899
1001
  output.system.push(formatSkillsContext(matched));
900
1002
  debug(
@@ -907,8 +1009,9 @@ function createSystemTransformHandler(
907
1009
  }
908
1010
  }
909
1011
 
910
- // Inject messaging protocol for multi-instance coordination
911
- output.system.push(`<aide-messaging>
1012
+ // Inject messaging protocol only in swarm mode (saves ~450 bytes per turn otherwise)
1013
+ if (rawMode === "swarm") {
1014
+ output.system.push(`<aide-messaging>
912
1015
 
913
1016
  ## Agent Messaging
914
1017
 
@@ -927,6 +1030,7 @@ Use aide MCP tools to coordinate with other agents or sessions:
927
1030
  - Check messages periodically for requests from other agents
928
1031
 
929
1032
  </aide-messaging>`);
1033
+ }
930
1034
  };
931
1035
  }
932
1036