opencodekit 0.9.2 → 0.10.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.
@@ -2,27 +2,51 @@
2
2
 
3
3
  TypeScript plugins for extending OpenCode functionality following official best practices.
4
4
 
5
+ ## Directory Structure
6
+
7
+ ```
8
+ plugin/
9
+ ├── lib/
10
+ │ └── notify.ts # Shared notification utilities
11
+ ├── injector.ts # AGENTS.md hierarchy walker
12
+ ├── compactor.ts # Context usage warnings
13
+ ├── enforcer.ts # TODO completion enforcement
14
+ ├── notification.ts # Session completion alerts
15
+ ├── sessions.ts # Session management tools
16
+ ├── truncator.ts # Output size monitoring
17
+ └── README.md
18
+ ```
19
+
5
20
  ## Installed Plugins
6
21
 
7
- ### sessions.ts 🆕
22
+ ### injector.ts
8
23
 
9
- **Session management and context transfer** - enables short, focused sessions by providing session discovery and context reads.
24
+ **AGENTS.md hierarchy walker** - solves OpenCode's limitation where findUp only finds the first AGENTS.md match.
10
25
 
11
- Based on [AmpCode's 200k tokens article](https://ampcode.com/200k-tokens-is-plenty): long context makes agents "drunk" (hallucinate, argue, fail) while costing exponentially more.
26
+ - Hooks into `tool.execute.after` for `read` tool
27
+ - Walks up from file directory to project root
28
+ - Collects ALL AGENTS.md files in the path
29
+ - Injects in order: root → specific (T-shaped context loading)
30
+ - Caches per session to avoid duplicate injections
12
31
 
13
- **Tools:**
32
+ **Example:** When reading `src/components/Button.tsx`:
14
33
 
15
- - `list_sessions(project?, since?, limit?)` - Discover available sessions with metadata
16
- - `project`: "current" (default), "all", or absolute path
17
- - `since`: "today", "yesterday", "this week", or ISO date
18
- - `limit`: Max sessions to return (default: 20)
34
+ ```
35
+ Injects:
36
+ 1. /project/AGENTS.md (root context)
37
+ 2. /project/src/AGENTS.md (src context)
38
+ 3. /project/src/components/AGENTS.md (component context)
39
+ ```
19
40
 
20
- - `read_session(session_reference, project?, focus?)` - Load context from previous sessions
21
- - `session_reference`: "last", "previous", "2 ago", date, or session ID
22
- - `project`: Filter when using relative/date references
23
- - `focus`: Optional filter (e.g., "test failures", "bug findings")
41
+ ### sessions.ts
42
+
43
+ **Session management and context transfer** - enables short, focused sessions.
44
+
45
+ **Tools:**
24
46
 
25
- **Returns:** Session metadata, user tasks, tool usage stats, file changes
47
+ - `list_sessions(project?, since?, limit?)` - Discover available sessions
48
+ - `read_session(session_reference, project?, focus?)` - Load context from previous sessions
49
+ - `summarize_session(session_id)` - Generate AI summary of a session
26
50
 
27
51
  **Workflow pattern:**
28
52
 
@@ -30,133 +54,121 @@ Based on [AmpCode's 200k tokens article](https://ampcode.com/200k-tokens-is-plen
30
54
  Session 1: Implementation (80k) → close
31
55
  Session 2: read_session("last") → Refactor (60k) → close
32
56
  Session 3: read_session("previous") → Tests (90k) → close
33
- Session 4: read_session refs → Review (100k)
34
57
  ```
35
58
 
36
- Result: 4 fresh contexts vs 1 bloated 330k thread with degraded performance.
59
+ ### compactor.ts
37
60
 
38
- **When to start new session:**
61
+ **Context usage warnings** - notifies at token thresholds before hitting limits.
39
62
 
40
- - Task complete
41
- - Token usage > 150k
42
- - Switching phases (implementation review → testing)
43
- - After handoff
63
+ - Warns at 70% (info), 85% (warn), 95% (critical)
64
+ - Sends native notifications at 85%+
65
+ - Tracks per-session to avoid duplicate warnings
44
66
 
45
- ### notification.ts
67
+ ### enforcer.ts
46
68
 
47
- Native notification plugin that alerts when sessions complete:
69
+ **TODO completion enforcement** - forces continuation when session idles with incomplete work.
48
70
 
49
- - Sends notifications when AI finishes (via `session.idle` event)
50
- - Extracts session summary from messages
51
- - Cross-platform support (macOS via `osascript`, Linux via `notify-send`)
52
- - Simplified implementation following official docs pattern
71
+ - Tracks TODOs per session via `todo.updated` events
72
+ - On `session.idle`, checks for incomplete high-priority or in-progress TODOs
73
+ - **ENFORCES** continuation by calling `client.session.promptAsync()` (not just notification)
74
+ - Injects prompt: "Continue working on incomplete TODOs: [list]"
75
+ - 5-minute cooldown to prevent spam
76
+ - Falls back to OS notification if prompt injection fails
53
77
 
54
- **Official Docs:** [OpenCode Plugins - Send notifications](https://opencode.ai/docs/plugins/#send-notifications)
78
+ **Behavior:**
55
79
 
56
- ### superpowers.ts
80
+ - High-priority or in-progress TODOs → Inject continuation prompt
81
+ - Low-priority pending TODOs → OS notification only (no forced continuation)
57
82
 
58
- Custom tools for loading and discovering skills:
83
+ ### notification.ts
59
84
 
60
- - `use_skill` - Load a specific skill to guide your work
61
- - `find_skills` - List all available skills (project, personal, superpowers)
62
- - Skills loaded on-demand, not auto-injected
63
- - Supports skill resolution priority: project > personal > superpowers
85
+ **Session completion alerts** - sends native notifications when AI finishes.
64
86
 
65
- **Official Docs:** [OpenCode Plugins - Custom tools](https://opencode.ai/docs/plugins/#custom-tools)
87
+ - Extracts session summary from messages
88
+ - Cross-platform (macOS, Linux, WSL, Windows)
89
+ - Uses shared `lib/notify.ts` utilities
66
90
 
67
- ## Best Practices Applied
91
+ ### truncator.ts
68
92
 
69
- ### Correct Import Syntax
93
+ **Output size monitoring** - logs warnings for large outputs under context pressure.
70
94
 
71
- ```typescript
72
- import { type Plugin, tool } from "@opencode-ai/plugin";
73
- ```
95
+ - Monitors `tool.execute.after` events
96
+ - Warns when outputs exceed thresholds based on context usage
97
+ - Note: Actual truncation requires OpenCode core changes; this is observation-only
74
98
 
75
- ### Proper Plugin Structure
99
+ ## Shared Library
76
100
 
77
- ```typescript
78
- export const MyPlugin: Plugin = async ({ project, client, $, directory }) => {
79
- return {
80
- event: async ({ event }) => {
81
- /* ... */
82
- },
83
- tool: {
84
- mytool: tool({
85
- /* ... */
86
- }),
87
- },
88
- };
89
- };
90
- ```
101
+ ### lib/notify.ts
91
102
 
92
- ### TypeScript Configuration
103
+ Shared utilities used by multiple plugins:
93
104
 
94
- - Minimal `tsconfig.json` for plugin development
95
- - No compilation needed (OpenCode loads `.ts` directly)
96
- - Type safety with `@opencode-ai/plugin` types
105
+ ```typescript
106
+ import { notify, THRESHOLDS, getContextPercentage } from "./lib/notify";
97
107
 
98
- ### Registered in opencode.json
108
+ // Send cross-platform notification using $ shell API
109
+ await notify($, "Title", "Message");
99
110
 
100
- ```json
101
- {
102
- "plugin": ["./plugin/superpowers.ts", "./plugin/notification.ts"]
103
- }
111
+ // Context thresholds
112
+ THRESHOLDS.MODERATE; // 70%
113
+ THRESHOLDS.URGENT; // 85%
114
+ THRESHOLDS.CRITICAL; // 95%
104
115
  ```
105
116
 
106
- ## Development
117
+ ## Best Practices Applied
107
118
 
108
- All plugins are written in TypeScript with proper type safety:
119
+ ### Use `$` Shell API (not `exec`)
109
120
 
110
- - Import types from `@opencode-ai/plugin`
111
- - Use structured logging with `client.app.log()`
112
- - Handle errors gracefully with `.catch(() => {})`
113
- - Follow OpenCode plugin conventions from official docs
121
+ ```typescript
122
+ // ✅ Correct - uses Bun shell from plugin context
123
+ export const MyPlugin: Plugin = async ({ $ }) => {
124
+ await $`osascript -e 'display notification "Done!"'`;
125
+ };
114
126
 
115
- ## Available Hooks
127
+ // Wrong - manual exec with escaping
128
+ import { exec } from "child_process";
129
+ exec(`osascript -e '...'`, () => {});
130
+ ```
116
131
 
117
- From [official documentation](https://opencode.ai/docs/plugins/#events):
132
+ ### Share Common Code
118
133
 
119
- - `event` - Listen to OpenCode events (session.idle, file.edited, etc.)
120
- - `tool.execute.before` - Hook before tool execution
121
- - `tool.execute.after` - Hook after tool execution
122
- - `tool` - Add custom tools to OpenCode
134
+ ```typescript
135
+ // Correct - import from shared lib
136
+ import { notify } from "./lib/notify";
123
137
 
124
- ## Plugin Examples
138
+ // Wrong - copy-paste same code in every plugin
139
+ function notify() {
140
+ /* duplicated */
141
+ }
142
+ ```
125
143
 
126
- ### Send Notifications (notification.ts)
144
+ ### Proper Plugin Structure
127
145
 
128
146
  ```typescript
129
- export const NotificationPlugin: Plugin = async ({ $, client }) => {
147
+ import type { Plugin } from "@opencode-ai/plugin";
148
+
149
+ export const MyPlugin: Plugin = async ({ client, $ }) => {
130
150
  return {
131
151
  event: async ({ event }) => {
132
- if (event.type === "session.idle") {
133
- await $`osascript -e 'display notification "Done!" with title "OpenCode"'`;
134
- }
152
+ /* ... */
153
+ },
154
+ "tool.execute.after": async (input, output) => {
155
+ /* ... */
135
156
  },
136
157
  };
137
158
  };
138
159
  ```
139
160
 
140
- ### Custom Tools (superpowers.ts)
161
+ ## Available Hooks
141
162
 
142
- ```typescript
143
- export const SuperpowersPlugin: Plugin = async ({ directory }) => {
144
- return {
145
- tool: {
146
- use_skill: tool({
147
- description: "Load a skill to guide your work",
148
- args: { skill_name: tool.schema.string() },
149
- async execute(args, ctx) {
150
- // Load and return skill content
151
- },
152
- }),
153
- },
154
- };
155
- };
156
- ```
163
+ | Hook | Purpose |
164
+ | --------------------- | --------------------------------------------------------------- |
165
+ | `event` | Listen to OpenCode events (session.idle, session.updated, etc.) |
166
+ | `tool.execute.before` | Hook before tool execution |
167
+ | `tool.execute.after` | Hook after tool execution (observation only) |
168
+ | `tool` | Add custom tools |
157
169
 
158
170
  ## Resources
159
171
 
160
172
  - [OpenCode Plugin Documentation](https://opencode.ai/docs/plugins/)
161
- - [OpenCode Ecosystem Plugins](https://opencode.ai/docs/ecosystem#plugins)
173
+ - [OpenCode Custom Tools](https://opencode.ai/docs/custom-tools/)
162
174
  - [Community Examples](https://github.com/sst/opencode/discussions)
@@ -1,183 +1,107 @@
1
1
  /**
2
2
  * OpenCode Compactor Plugin
3
- * Warns at token thresholds before hitting limits - graceful context management
4
- *
5
- * Inspired by oh-my-opencode's context-window-monitor and anthropic-auto-compact
3
+ * Warns at token thresholds before hitting limits
6
4
  */
7
5
 
8
6
  import type { Plugin } from "@opencode-ai/plugin";
9
- import { exec } from "child_process";
10
- import { readFileSync } from "fs";
11
-
12
- // Notification helpers
13
- function isWSL(): boolean {
14
- try {
15
- const release = readFileSync("/proc/version", "utf8").toLowerCase();
16
- return release.includes("microsoft") || release.includes("wsl");
17
- } catch {
18
- return false;
19
- }
20
- }
7
+ import {
8
+ THRESHOLDS,
9
+ type TokenStats,
10
+ getContextPercentage,
11
+ notify,
12
+ } from "./lib/notify";
21
13
 
22
- function escapeShell(str: string): string {
23
- return str.replace(/"/g, '\\"').replace(/\$/g, "\\$").replace(/`/g, "\\`");
24
- }
14
+ type LogLevel = "debug" | "info" | "warn" | "error";
25
15
 
26
- function notify(title: string, message: string): void {
27
- const platform = process.platform;
28
- const safeTitle = title ? String(title) : "Notification";
29
- const safeMessage = message ? String(message) : "";
30
- const escapedTitle = escapeShell(safeTitle);
31
- const escapedMessage = escapeShell(safeMessage);
32
-
33
- let command: string;
34
-
35
- if (platform === "darwin") {
36
- command = `osascript -e 'display notification "${escapedMessage}" with title "${escapedTitle}"'`;
37
- } else if (platform === "linux") {
38
- command = `notify-send "${escapedTitle}" "${escapedMessage}"`;
39
- if (isWSL()) {
40
- command = `(${command}) 2>&1 || echo "WSL notification failed"`;
41
- }
42
- } else if (platform === "win32") {
43
- command = `powershell -Command "Add-Type -AssemblyName System.Windows.Forms; [System.Windows.Forms.MessageBox]::Show('${escapedMessage}', '${escapedTitle}')"`;
44
- } else {
45
- return;
46
- }
47
-
48
- exec(command, () => {});
16
+ interface WarningLevel {
17
+ level: LogLevel;
18
+ emoji: string;
19
+ action: string;
49
20
  }
50
21
 
51
- interface TokenStats {
52
- used: number;
53
- limit: number;
54
- percentage?: number;
22
+ function getWarningLevel(percentage: number): WarningLevel | null {
23
+ if (percentage >= THRESHOLDS.CRITICAL) {
24
+ return {
25
+ level: "error",
26
+ emoji: "🚨",
27
+ action: "CRITICAL: Prune immediately or start new session.",
28
+ };
29
+ }
30
+ if (percentage >= THRESHOLDS.URGENT) {
31
+ return {
32
+ level: "warn",
33
+ emoji: "⚠️",
34
+ action: "Consider pruning completed tool outputs.",
35
+ };
36
+ }
37
+ if (percentage >= THRESHOLDS.MODERATE) {
38
+ return {
39
+ level: "info",
40
+ emoji: "📈",
41
+ action: "Context at 70%. Consider consolidating.",
42
+ };
43
+ }
44
+ return null;
55
45
  }
56
46
 
57
- const THRESHOLDS = {
58
- MODERATE: 70,
59
- URGENT: 85,
60
- CRITICAL: 95,
61
- } as const;
62
-
63
- type LogLevel = "debug" | "info" | "warn" | "error";
64
-
65
- export const CompactorPlugin: Plugin = async ({ client }) => {
66
- const warnedSessions = new Map<string, number>();
67
-
68
- client.app
69
- .log({
70
- body: {
71
- service: "compactor-plugin",
72
- level: "info",
73
- message:
74
- "📊 Compactor Plugin loaded - context window monitoring active",
75
- },
76
- })
77
- .catch(() => {});
78
-
79
- function getWarningLevel(
80
- percentage: number,
81
- ): { level: LogLevel; emoji: string; action: string } | null {
82
- if (percentage >= THRESHOLDS.CRITICAL) {
83
- return {
84
- level: "error",
85
- emoji: "🚨",
86
- action:
87
- "CRITICAL: Prune immediately or start new session. Context nearly exhausted.",
88
- };
89
- }
90
- if (percentage >= THRESHOLDS.URGENT) {
91
- return {
92
- level: "warn",
93
- emoji: "⚠️",
94
- action:
95
- "Consider pruning completed tool outputs or summarizing findings.",
96
- };
97
- }
98
- if (percentage >= THRESHOLDS.MODERATE) {
99
- return {
100
- level: "info",
101
- emoji: "📈",
102
- action:
103
- "Context at 70%. Still plenty of room - no rush, but consider consolidating.",
104
- };
105
- }
106
- return null;
107
- }
108
-
109
- return {
110
- event: async ({ event }) => {
111
- const props = event.properties as Record<string, unknown>;
112
-
113
- if (event.type === "session.updated") {
114
- const info = props?.info as Record<string, unknown> | undefined;
115
- const tokenStats = (info?.tokens || props?.tokens) as
116
- | TokenStats
117
- | undefined;
118
- const sessionId = (info?.id || props?.sessionID) as string | undefined;
119
-
120
- if (!sessionId || !tokenStats?.used || !tokenStats?.limit) return;
121
-
122
- const pct =
123
- tokenStats.percentage ||
124
- Math.round((tokenStats.used / tokenStats.limit) * 100);
125
- const lastWarned = warnedSessions.get(sessionId) || 0;
126
-
127
- const warning = getWarningLevel(pct);
128
- if (!warning) return;
129
-
130
- let currentThreshold = 0;
131
- if (pct >= THRESHOLDS.CRITICAL) currentThreshold = THRESHOLDS.CRITICAL;
132
- else if (pct >= THRESHOLDS.URGENT) currentThreshold = THRESHOLDS.URGENT;
133
- else if (pct >= THRESHOLDS.MODERATE)
134
- currentThreshold = THRESHOLDS.MODERATE;
135
-
136
- if (lastWarned >= currentThreshold) return;
137
-
138
- warnedSessions.set(sessionId, currentThreshold);
139
-
140
- const message = `${warning.emoji} Context: ${pct}% (${tokenStats.used.toLocaleString()}/${tokenStats.limit.toLocaleString()} tokens). ${warning.action}`;
141
-
142
- client.app
143
- .log({
144
- body: {
145
- service: "compactor-plugin",
146
- level: warning.level,
147
- message,
148
- },
149
- })
150
- .catch(() => {});
151
-
152
- if (pct >= THRESHOLDS.URGENT) {
153
- notify(`Context ${pct}%`, warning.action);
154
- }
155
- }
156
-
157
- if (event.type === "session.compacted") {
158
- const sessionId = props?.sessionID as string | undefined;
159
-
160
- if (sessionId) {
161
- client.app
162
- .log({
163
- body: {
164
- service: "compactor-plugin",
165
- level: "info",
166
- message: "♻️ Session compacted - context freed",
167
- },
168
- })
169
- .catch(() => {});
170
-
171
- warnedSessions.set(sessionId, 0);
172
- }
173
- }
174
-
175
- if (event.type === "session.deleted") {
176
- const sessionId = props?.sessionID as string | undefined;
177
- if (sessionId) {
178
- warnedSessions.delete(sessionId);
179
- }
180
- }
181
- },
182
- };
47
+ export const CompactorPlugin: Plugin = async ({ client, $ }) => {
48
+ const warnedSessions = new Map<string, number>();
49
+
50
+ return {
51
+ event: async ({ event }) => {
52
+ const props = event.properties as Record<string, unknown>;
53
+
54
+ if (event.type === "session.updated") {
55
+ const info = props?.info as Record<string, unknown> | undefined;
56
+ const tokenStats = (info?.tokens || props?.tokens) as
57
+ | TokenStats
58
+ | undefined;
59
+ const sessionId = (info?.id || props?.sessionID) as string | undefined;
60
+
61
+ if (!sessionId || !tokenStats?.used || !tokenStats?.limit) return;
62
+
63
+ const pct = getContextPercentage(tokenStats);
64
+ const lastWarned = warnedSessions.get(sessionId) || 0;
65
+
66
+ const warning = getWarningLevel(pct);
67
+ if (!warning) return;
68
+
69
+ let currentThreshold = 0;
70
+ if (pct >= THRESHOLDS.CRITICAL) currentThreshold = THRESHOLDS.CRITICAL;
71
+ else if (pct >= THRESHOLDS.URGENT) currentThreshold = THRESHOLDS.URGENT;
72
+ else if (pct >= THRESHOLDS.MODERATE)
73
+ currentThreshold = THRESHOLDS.MODERATE;
74
+
75
+ if (lastWarned >= currentThreshold) return;
76
+
77
+ warnedSessions.set(sessionId, currentThreshold);
78
+
79
+ const message = `${warning.emoji} Context: ${pct}% (${tokenStats.used.toLocaleString()}/${tokenStats.limit.toLocaleString()} tokens). ${warning.action}`;
80
+
81
+ client.app
82
+ .log({
83
+ body: { service: "compactor", level: warning.level, message },
84
+ })
85
+ .catch(() => {});
86
+
87
+ if (pct >= THRESHOLDS.URGENT) {
88
+ await notify($, `Context ${pct}%`, warning.action);
89
+ }
90
+ }
91
+
92
+ if (event.type === "session.compacted") {
93
+ const sessionId = props?.sessionID as string | undefined;
94
+ if (sessionId) {
95
+ warnedSessions.set(sessionId, 0);
96
+ }
97
+ }
98
+
99
+ if (event.type === "session.deleted") {
100
+ const sessionId = props?.sessionID as string | undefined;
101
+ if (sessionId) {
102
+ warnedSessions.delete(sessionId);
103
+ }
104
+ }
105
+ },
106
+ };
183
107
  };