opencodekit 0.16.10 → 0.16.13

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.
@@ -1,34 +0,0 @@
1
- import type { Plugin } from "@opencode-ai/plugin";
2
-
3
- const session_set = new Set<string>();
4
- const cwd = process.cwd();
5
- const env_ctx = `
6
- <environment_context>
7
- <cwd>${cwd}</cwd>
8
- </environment_context>
9
- `.trim();
10
-
11
- export const EnvironmentContextPlugin: Plugin = async () => {
12
- return {
13
- async "chat.message"(_input, output) {
14
- const { message } = output;
15
- const sessionID = message?.sessionID;
16
- if (!sessionID) return;
17
-
18
- if (session_set.has(sessionID)) {
19
- return;
20
- }
21
-
22
- output.parts.unshift({
23
- id: `env-ctx-${Date.now()}`,
24
- messageID: message.id,
25
- sessionID,
26
- type: "text",
27
- text: env_ctx,
28
- synthetic: true,
29
- });
30
-
31
- session_set.add(sessionID);
32
- },
33
- };
34
- };
@@ -1,301 +0,0 @@
1
- /**
2
- * LSP Plugin - Active LSP Tool Enforcement
3
- *
4
- * Forces agents to actively use LSP tools when code files are detected.
5
- * This is NOT a suggestion - agents MUST execute LSP operations immediately.
6
- *
7
- * Mechanism:
8
- * 1. Hooks into grep/glob/read tool outputs (tool.execute.after)
9
- * 2. Hooks into user messages mentioning code files (chat.message)
10
- * 3. Injects MANDATORY LSP execution commands
11
- * 4. Uses strong language to override agent tendencies to skip
12
- *
13
- * Based on oh-my-opencode's LSP forcing pattern.
14
- */
15
-
16
- import type { Plugin } from "@opencode-ai/plugin";
17
-
18
- // File extensions that support LSP
19
- const LSP_SUPPORTED_EXTENSIONS = new Set([
20
- ".ts",
21
- ".tsx",
22
- ".js",
23
- ".jsx",
24
- ".py",
25
- ".go",
26
- ".rs",
27
- ".java",
28
- ".c",
29
- ".cpp",
30
- ".h",
31
- ".hpp",
32
- ".cs",
33
- ".rb",
34
- ".php",
35
- ".swift",
36
- ".kt",
37
- ".scala",
38
- ".lua",
39
- ".zig",
40
- ".vue",
41
- ".svelte",
42
- ]);
43
-
44
- // Regex to extract file:line patterns from tool output
45
- const FILE_LINE_PATTERNS = [
46
- // Standard grep output: path/file.ts:42: content
47
- /^([^\s:]+\.(ts|tsx|js|jsx|py|go|rs|java|c|cpp|h|hpp|cs|rb|php|swift|kt|scala|lua|zig|vue|svelte)):(\d+):/gm,
48
- // Just path with line: path/file.ts:42
49
- /([^\s:]+\.(ts|tsx|js|jsx|py|go|rs|java|c|cpp|h|hpp|cs|rb|php|swift|kt|scala|lua|zig|vue|svelte)):(\d+)/g,
50
- // Glob/list output with just paths
51
- /^([^\s:]+\.(ts|tsx|js|jsx|py|go|rs|java|c|cpp|h|hpp|cs|rb|php|swift|kt|scala|lua|zig|vue|svelte))$/gm,
52
- ];
53
-
54
- // Patterns that indicate user is asking about code
55
- const CODE_INTENT_PATTERNS = [
56
- /\b(edit|modify|change|update|fix|refactor|add|remove|delete)\b.*\.(ts|tsx|js|jsx|py|go|rs)/i,
57
- /\b(function|class|method|variable|type|interface)\s+\w+/i,
58
- /\b(implement|create|build)\b.*\b(feature|component|module)/i,
59
- /@[^\s]+\.(ts|tsx|js|jsx|py|go|rs)/i, // @file.ts mentions
60
- /\b(src|lib|app|components?)\/[^\s]+\.(ts|tsx|js|jsx)/i, // Path patterns
61
- ];
62
-
63
- interface FileMatch {
64
- filePath: string;
65
- line?: number;
66
- character?: number;
67
- }
68
-
69
- function extractFileMatches(output: string): FileMatch[] {
70
- const matches: FileMatch[] = [];
71
- const seen = new Set<string>();
72
-
73
- for (const pattern of FILE_LINE_PATTERNS) {
74
- pattern.lastIndex = 0;
75
-
76
- let match = pattern.exec(output);
77
- while (match !== null) {
78
- const filePath = match[1];
79
- const line = match[3] ? Number.parseInt(match[3], 10) : undefined;
80
-
81
- const key = `${filePath}:${line || 0}`;
82
- if (!seen.has(key)) {
83
- seen.add(key);
84
- matches.push({
85
- filePath,
86
- line,
87
- character: 1,
88
- });
89
- }
90
-
91
- match = pattern.exec(output);
92
- }
93
- }
94
-
95
- return matches.slice(0, 5);
96
- }
97
-
98
- function extractFilesFromUserMessage(text: string): string[] {
99
- const files: string[] = [];
100
- const seen = new Set<string>();
101
-
102
- // Match @file.ts patterns
103
- const atMentions = text.match(/@([^\s]+\.(ts|tsx|js|jsx|py|go|rs))/gi) || [];
104
- for (const mention of atMentions) {
105
- const file = mention.replace("@", "");
106
- if (!seen.has(file)) {
107
- seen.add(file);
108
- files.push(file);
109
- }
110
- }
111
-
112
- // Match path patterns like src/foo.ts
113
- const pathPatterns =
114
- text.match(/\b(src|lib|app|components?)\/[^\s]+\.(ts|tsx|js|jsx|py|go|rs)/gi) || [];
115
- for (const path of pathPatterns) {
116
- if (!seen.has(path)) {
117
- seen.add(path);
118
- files.push(path);
119
- }
120
- }
121
-
122
- return files.slice(0, 3);
123
- }
124
-
125
- function generateLspCommands(matches: FileMatch[]): string {
126
- if (matches.length === 0) return "";
127
-
128
- const commands: string[] = [];
129
-
130
- for (const match of matches) {
131
- if (match.line) {
132
- // With line number - get definition and references
133
- commands.push(
134
- `lsp({ operation: "goToDefinition", filePath: "${match.filePath}", line: ${match.line}, character: ${match.character || 1} })`,
135
- );
136
- commands.push(
137
- `lsp({ operation: "findReferences", filePath: "${match.filePath}", line: ${match.line}, character: ${match.character || 1} })`,
138
- );
139
- commands.push(
140
- `lsp({ operation: "hover", filePath: "${match.filePath}", line: ${match.line}, character: ${match.character || 1} })`,
141
- );
142
- } else {
143
- // Just file path - get document symbols
144
- commands.push(
145
- `lsp({ operation: "documentSymbol", filePath: "${match.filePath}", line: 1, character: 1 })`,
146
- );
147
- }
148
- }
149
-
150
- const uniqueCommands = [...new Set(commands)].slice(0, 6);
151
-
152
- // AGGRESSIVE enforcement format - designed to force action
153
- return `
154
-
155
- ╔══════════════════════════════════════════════════════════════════════════════╗
156
- ║ ⚡ LSP EXECUTION REQUIRED - DO NOT SKIP ║
157
- ╠══════════════════════════════════════════════════════════════════════════════╣
158
- ║ ║
159
- ║ Code files detected. You MUST execute these LSP operations NOW. ║
160
- ║ DO NOT proceed with edits until LSP context is retrieved. ║
161
- ║ ║
162
- ║ EXECUTE IMMEDIATELY (copy-paste ready): ║
163
- ║ ║
164
- ${uniqueCommands.map((cmd) => `║ → ${cmd}`).join("\n")}
165
- ║ ║
166
- ╠══════════════════════════════════════════════════════════════════════════════╣
167
- ║ WHY THIS IS MANDATORY: ║
168
- ║ • grep/read show text, LSP shows semantic meaning ║
169
- ║ • LSP provides accurate type info, definitions, call hierarchies ║
170
- ║ • Editing without LSP context leads to broken code ║
171
- ║ • This is a HARD RULE, not a suggestion ║
172
- ╠══════════════════════════════════════════════════════════════════════════════╣
173
- ║ WORKFLOW: grep/read → LSP → understand → THEN edit ║
174
- ╚══════════════════════════════════════════════════════════════════════════════╝
175
- `;
176
- }
177
-
178
- function generateUserMessageNudge(files: string[]): string {
179
- const lspCommands = files.map(
180
- (f) => `lsp({ operation: "documentSymbol", filePath: "${f}", line: 1, character: 1 })`,
181
- );
182
-
183
- return `
184
- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
185
- ⚡ [LSP CONTEXT REQUIRED]
186
-
187
- Before making ANY changes to these files, you MUST first retrieve LSP context:
188
-
189
- ${lspCommands.map((cmd) => `→ ${cmd}`).join("\n")}
190
-
191
- This is a HARD RULE. Do NOT guess about code structure.
192
- Workflow: READ file → LSP documentSymbol/hover → UNDERSTAND → THEN edit
193
- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
194
- `;
195
- }
196
-
197
- function shouldInjectNudge(toolName: string): boolean {
198
- return ["grep", "glob", "read", "batch"].includes(toolName);
199
- }
200
-
201
- function hasCodeFiles(output: string): boolean {
202
- for (const ext of LSP_SUPPORTED_EXTENSIONS) {
203
- if (output.includes(ext)) return true;
204
- }
205
- return false;
206
- }
207
-
208
- function userMessageMentionsCode(text: string): boolean {
209
- return CODE_INTENT_PATTERNS.some((pattern) => pattern.test(text));
210
- }
211
-
212
- export const Lsp: Plugin = async () => {
213
- return {
214
- name: "lsp",
215
- version: "1.1.0",
216
-
217
- /**
218
- * Hook: chat.message
219
- * Injects LSP reminder when user mentions code files in their message
220
- * This catches intent BEFORE tools are executed
221
- */
222
- "chat.message": async (input, output) => {
223
- const { sessionID, messageID } = input;
224
- const { message, parts } = output;
225
-
226
- // Only process user messages
227
- if (message.role !== "user") return;
228
-
229
- // Extract text from all parts
230
- const fullText = parts
231
- .filter((p) => p.type === "text")
232
- .map((p) => ("text" in p ? p.text : ""))
233
- .join(" ");
234
-
235
- // Check if user is mentioning code files or asking about code
236
- if (!userMessageMentionsCode(fullText)) return;
237
-
238
- // Extract specific files mentioned
239
- const files = extractFilesFromUserMessage(fullText);
240
-
241
- // If files found, inject specific LSP commands
242
- // If no specific files but code intent detected, inject general reminder
243
- const nudgeText =
244
- files.length > 0
245
- ? generateUserMessageNudge(files)
246
- : `
247
- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
248
- ⚡ [LSP FIRST - HARD RULE]
249
-
250
- Code modification detected. Before editing:
251
- 1. Use grep/glob to find relevant files
252
- 2. Use READ to view the file
253
- 3. Use LSP (documentSymbol, goToDefinition, findReferences) to understand structure
254
- 4. ONLY THEN make edits
255
-
256
- Do NOT skip LSP. Editing without semantic context leads to broken code.
257
- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
258
- `;
259
-
260
- // Inject synthetic message part with all required fields
261
- const partId = `lsp-nudge-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
262
- parts.push({
263
- id: partId,
264
- sessionID,
265
- messageID: messageID || "",
266
- type: "text",
267
- text: nudgeText,
268
- synthetic: true,
269
- } as import("@opencode-ai/sdk").Part);
270
- },
271
-
272
- /**
273
- * Hook: tool.execute.after
274
- * Injects LSP commands after grep/glob/read returns code file results
275
- * This catches AFTER tools show code files
276
- */
277
- "tool.execute.after": async (input, output) => {
278
- const { tool: toolName } = input;
279
- const result = output.output;
280
-
281
- // Skip if not a search/read tool
282
- if (!shouldInjectNudge(toolName)) return;
283
-
284
- // Skip if no code files in output
285
- if (typeof result !== "string" || !hasCodeFiles(result)) return;
286
-
287
- // Extract file matches
288
- const matches = extractFileMatches(result);
289
- if (matches.length === 0) return;
290
-
291
- // Generate LSP commands (not suggestions - COMMANDS)
292
- const lspBlock = generateLspCommands(matches);
293
- if (!lspBlock) return;
294
-
295
- // Append mandatory LSP block to tool output
296
- output.output = `${result}${lspBlock}`;
297
- },
298
- };
299
- };
300
-
301
- export default Lsp;
@@ -1,59 +0,0 @@
1
- /**
2
- * OpenCode Truncator Plugin
3
- * Warns when tools return large outputs under context pressure
4
- *
5
- * Note: This doesn't actually truncate - OpenCode handles that via compaction.prune.
6
- * This only adds WARNINGS that OpenCode doesn't provide.
7
- */
8
-
9
- import type { Plugin } from "@opencode-ai/plugin";
10
-
11
- export const TruncatorPlugin: Plugin = async ({ client }) => {
12
- const sessionContext = new Map<string, number>();
13
-
14
- return {
15
- event: async ({ event }) => {
16
- const props = event.properties as Record<string, unknown>;
17
-
18
- if (event.type === "session.updated") {
19
- const info = props?.info as Record<string, unknown> | undefined;
20
- const tokenStats = (info?.tokens || props?.tokens) as
21
- | { used: number; limit: number }
22
- | undefined;
23
- const sessionId = (info?.id || props?.sessionID) as string | undefined;
24
-
25
- if (sessionId && tokenStats?.used && tokenStats?.limit) {
26
- sessionContext.set(sessionId, Math.round((tokenStats.used / tokenStats.limit) * 100));
27
- }
28
- }
29
-
30
- if (event.type === "session.deleted") {
31
- const sessionId = props?.sessionID as string | undefined;
32
- if (sessionId) {
33
- sessionContext.delete(sessionId);
34
- }
35
- }
36
- },
37
-
38
- "tool.execute.after": async (input, output) => {
39
- const pct = sessionContext.get(input.sessionID) || 0;
40
- if (pct < 70) return; // Only warn under pressure
41
-
42
- // Thresholds get tighter as context fills up
43
- const threshold = pct >= 95 ? 5000 : pct >= 85 ? 10000 : 20000;
44
- const outputStr = output.output || "";
45
-
46
- if (outputStr.length > threshold) {
47
- await client.app
48
- .log({
49
- body: {
50
- service: "truncator",
51
- level: pct >= 95 ? "warn" : "info",
52
- message: `Large output from ${input.tool}: ${outputStr.length} chars (threshold: ${threshold}, context: ${pct}%)`,
53
- },
54
- })
55
- .catch(() => {});
56
- }
57
- },
58
- };
59
- };