opencode-snippets 1.0.0 → 1.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.
package/README.md CHANGED
@@ -1,34 +1,88 @@
1
1
  # opencode-snippets
2
2
 
3
- **Instant inline text expansion for OpenCode** - Type `#snippet` anywhere in your message and watch it transform.
3
+ **Instant inline text expansion for OpenCode** - Type `#snippet` anywhere in your message and watch it transform.
4
4
 
5
5
  ## Why Snippets?
6
6
 
7
- OpenCode has powerful `/slash` commands, but they must come first in your message. What if you want to inject context *mid-thought*?
7
+ As developers, we DRY (Don't Repeat Yourself) our code. We extract functions, create libraries, compose modules. Why should our prompts be any different?
8
+
9
+ Stop copy-pasting the same instructions into every message. Snippets bring software engineering principles to prompt engineering:
10
+
11
+ - 🔄 **DRY** - Write once, reuse everywhere
12
+ - 🧩 **Composability** - Build complex prompts from simple pieces
13
+ - 🔧 **Maintainability** - Update once, apply everywhere
14
+ - 🔍 **Discoverability** - Your team's best practices, always a `#hashtag` away
15
+
16
+ OpenCode's `/slash` commands must come first. Snippets work anywhere:
8
17
 
9
18
  ```
10
- # With slash commands (must be first):
19
+ # Slash commands (must be first):
11
20
  /git-status Please review my changes
12
21
 
13
- # With snippets (anywhere!):
22
+ # Snippets (anywhere!):
14
23
  Please review my changes #git-status and suggest improvements #code-style
15
24
  ```
16
25
 
17
- **Snippets work like `@file` mentions** - natural, inline, composable. Build complex prompts from reusable pieces without breaking your flow.
26
+ Snippets work like `@file` mentions - natural, inline, composable.
27
+
28
+ ### 🎯 Composable by Design
29
+
30
+ Snippets compose with each other and with slash commands. Reference `#snippets` anywhere - in your messages, in slash commands, even inside other snippets:
31
+
32
+ **Example: Extending snippets with logic**
33
+
34
+ `~/.config/opencode/command/commit-and-push.md`:
35
+ ```markdown
36
+ ---
37
+ description: Create a git commit and push to remote
38
+ ---
39
+ Please create a git commit with the current changes and push to the remote repository.
40
+
41
+ Here is the current git status:
42
+ !`git status`
43
+
44
+ Here are the staged changes:
45
+ !`git diff --cached`
46
+
47
+ #conventional-commits
48
+ #project-context
49
+ ```
50
+
51
+ **Example: Snippets composing snippets**
52
+
53
+ `~/.config/opencode/snippet/code-standards.md`:
54
+ ```markdown
55
+ #style-guide
56
+ #error-handling
57
+ #testing-requirements
58
+ ```
59
+
60
+ `~/.config/opencode/snippet/full-review.md`:
61
+ ```markdown
62
+ #code-standards
63
+ #security-checklist
64
+ #performance-tips
65
+ ```
66
+
67
+ Compose base snippets into higher-level ones. Type `#full-review` to inject all standards at once, keeping each concern in its own maintainable file.
68
+
69
+ **The power:** Mix and match. Type `#tdd #careful` for test-driven development with extra caution. Build `/commit #conventional-commits #project-context` for context-aware commits. Create layered prompts from small, reusable pieces.
18
70
 
19
71
  ## Installation
20
72
 
21
- ```bash
22
- # Add to your opencode.json plugins array:
23
- "plugins": ["opencode-snippets"]
73
+ Add to your `opencode.json` plugins array:
24
74
 
25
- # Then install:
26
- bun install
75
+ ```json
76
+ {
77
+ "plugins": [
78
+ "opencode-snippets"
79
+ ]
80
+ }
27
81
  ```
28
82
 
29
83
  ## Quick Start
30
84
 
31
- **1. Create a snippet file:**
85
+ **1. Create your global snippets directory:**
32
86
 
33
87
  ```bash
34
88
  mkdir -p ~/.config/opencode/snippet
@@ -39,7 +93,7 @@ mkdir -p ~/.config/opencode/snippet
39
93
  `~/.config/opencode/snippet/careful.md`:
40
94
  ```markdown
41
95
  ---
42
- aliases: ["safe", "cautious"]
96
+ aliases: safe
43
97
  ---
44
98
  Think step by step. Double-check your work before making changes.
45
99
  Ask clarifying questions if anything is ambiguous.
@@ -48,24 +102,15 @@ Ask clarifying questions if anything is ambiguous.
48
102
  **3. Use it anywhere:**
49
103
 
50
104
  ```
51
- Refactor this function #careful
52
- ```
53
-
54
- The LLM receives:
55
- ```
56
- Refactor this function Think step by step. Double-check your work before making changes.
105
+ Refactor this function. Think step by step. Double-check your work before making changes.
57
106
  Ask clarifying questions if anything is ambiguous.
58
107
  ```
59
108
 
60
- ## Features
61
-
62
- ### Hashtag Expansion
109
+ ## Where to Store Snippets
63
110
 
64
- Any `#snippet-name` is replaced with the contents of `~/.config/opencode/snippet/snippet-name.md`:
111
+ Snippets can be global (`~/.config/opencode/snippet/*.md`) or project-specific (`.opencode/snippet/*.md`). Both directories are loaded automatically. Project snippets override global ones with the same name, just like OpenCode's slash commands.
65
112
 
66
- ```
67
- #review-checklist Please check my PR
68
- ```
113
+ ## Features
69
114
 
70
115
  ### Aliases
71
116
 
@@ -73,7 +118,9 @@ Define multiple triggers for the same snippet:
73
118
 
74
119
  ```markdown
75
120
  ---
76
- aliases: ["cp", "pick"]
121
+ aliases:
122
+ - cp
123
+ - pick
77
124
  description: "Git cherry-pick helper"
78
125
  ---
79
126
  Always pick parent 1 for merge commits.
@@ -81,9 +128,18 @@ Always pick parent 1 for merge commits.
81
128
 
82
129
  Now `#cherry-pick`, `#cp`, and `#pick` all expand to the same content.
83
130
 
131
+ Single alias doesn't need array syntax:
132
+ ```markdown
133
+ ---
134
+ aliases: safe
135
+ ---
136
+ ```
137
+
138
+ You can also use JSON array style: `aliases: ["cp", "pick"]`
139
+
84
140
  ### Shell Command Substitution
85
141
 
86
- Inject live system data with `!`backtick\`` syntax:
142
+ Snippets support the same ``!`command` `` syntax as [OpenCode slash commands](https://opencode.ai/docs/commands/#shell-output) for injecting live command output:
87
143
 
88
144
  ```markdown
89
145
  Current branch: !`git branch --show-current`
@@ -91,58 +147,63 @@ Last commit: !`git log -1 --oneline`
91
147
  Working directory: !`pwd`
92
148
  ```
93
149
 
94
- Output:
95
- ```
96
- Current branch: $ git branch --show-current
97
- --> main
98
- Last commit: $ git log -1 --oneline
99
- --> abc123f feat: add new feature
100
- Working directory: $ pwd
101
- --> /home/user/project
102
- ```
150
+ > **Note:** Snippets deviate slightly from the regular slash command behavior. Instead of just passing the command output to the LLM, snippets prepend the command itself:
151
+ > ``!`ls` `` →
152
+ > ```
153
+ > $ ls
154
+ > --> <output>
155
+ > ```
156
+ > This tells the LLM which command was actually run and makes failures visible (empty output would otherwise be indistinguishable from success).
157
+ >
158
+ > **TODO:** This behavior should either be PR'd upstream to OpenCode or made configurable in opencode-snippets.
103
159
 
104
160
  ### Recursive Includes
105
161
 
106
- Snippets can include other snippets:
162
+ Snippets can include other snippets using `#snippet-name` syntax. This allows building complex, composable snippets from smaller pieces:
107
163
 
108
164
  ```markdown
109
- # In base-context.md:
110
- #project-info
111
- #coding-standards
112
- #git-conventions
165
+ # In base-style.md:
166
+ Use TypeScript strict mode. Always add JSDoc comments.
167
+
168
+ # In python-style.md:
169
+ Use type hints. Follow PEP 8.
170
+
171
+ # In review.md:
172
+ Review this code carefully:
173
+ #base-style
174
+ #python-style
175
+ #security-checklist
113
176
  ```
114
177
 
115
- Loop detection prevents infinite recursion.
178
+ **Loop Protection:** Snippets are expanded up to 15 times per message to support deep nesting. If a circular reference is detected (e.g., `#a` includes `#b` which includes `#a`), expansion stops after 15 iterations and the remaining hashtag is left as-is. A warning is logged to help debug the issue.
179
+
180
+ **Example of loop protection:**
181
+ ```markdown
182
+ # self.md contains: "I reference #self"
183
+ # Expanding #self produces:
184
+ I reference I reference I reference ... (15 times) ... I reference #self
185
+ ```
186
+
187
+ This generous limit supports complex snippet hierarchies while preventing infinite loops.
116
188
 
117
189
  ## Example Snippets
118
190
 
119
191
  ### `~/.config/opencode/snippet/context.md`
120
192
  ```markdown
121
193
  ---
122
- aliases: ["ctx"]
194
+ aliases: ctx
123
195
  ---
124
196
  Project: !`basename $(pwd)`
125
197
  Branch: !`git branch --show-current`
126
198
  Recent changes: !`git diff --stat HEAD~3 | tail -5`
127
199
  ```
128
200
 
129
- ### `~/.config/opencode/snippet/review.md`
130
- ```markdown
131
- ---
132
- aliases: ["pr", "check"]
133
- ---
134
- Review this code for:
135
- - Security vulnerabilities
136
- - Performance issues
137
- - Code style consistency
138
- - Missing error handling
139
- - Test coverage gaps
140
- ```
141
-
142
201
  ### `~/.config/opencode/snippet/minimal.md`
143
202
  ```markdown
144
203
  ---
145
- aliases: ["min", "terse"]
204
+ aliases:
205
+ - min
206
+ - terse
146
207
  ---
147
208
  Be extremely concise. No explanations unless asked.
148
209
  ```
@@ -151,23 +212,17 @@ Be extremely concise. No explanations unless asked.
151
212
 
152
213
  | Feature | `/commands` | `#snippets` |
153
214
  |---------|-------------|-------------|
154
- | Position | Must be first | Anywhere |
155
- | Multiple per message | No | Yes |
156
- | Live shell data | Via implementation | Built-in `!\`cmd\`` |
157
- | Best for | Actions & workflows | Context injection |
215
+ | Position | Must come first 🏁 | Anywhere 📍 |
216
+ | Multiple per message | No | Yes |
217
+ | Live shell data | Yes 💻 | Yes 💻 |
218
+ | Best for | Triggering actions & workflows | Context injection 📝 |
158
219
 
159
220
  **Use both together:**
160
221
  ```
161
222
  /commit #conventional-commits #project-context
162
223
  ```
163
224
 
164
- ## Configuration
165
-
166
- ### Snippet Directory
167
-
168
- All snippets live in `~/.config/opencode/snippet/` as `.md` files.
169
-
170
- ### Debug Logging
225
+ ## Configuration### Debug Logging
171
226
 
172
227
  Enable debug logs by setting an environment variable:
173
228
 
@@ -182,7 +237,7 @@ Logs are written to `~/.config/opencode/logs/snippets/daily/`.
182
237
  - Snippets are loaded once at plugin startup
183
238
  - Hashtag matching is **case-insensitive** (`#Hello` = `#hello`)
184
239
  - Unknown hashtags are left unchanged
185
- - Failed shell commands preserve the `!\`cmd\`` syntax
240
+ - Failed shell commands preserve the original syntax in output
186
241
  - Frontmatter is stripped from expanded content
187
242
  - Only user messages are processed (not assistant responses)
188
243
 
package/index.ts CHANGED
@@ -1,33 +1,63 @@
1
- import type { Plugin } from "@opencode-ai/plugin"
2
- import { loadSnippets } from "./src/loader.js"
3
- import { expandHashtags } from "./src/expander.js"
4
- import { executeShellCommands } from "./src/shell.js"
1
+ import type { Plugin } from "@opencode-ai/plugin";
2
+ import { expandHashtags } from "./src/expander.js";
3
+ import { loadSnippets } from "./src/loader.js";
4
+ import { logger } from "./src/logger.js";
5
+ import { executeShellCommands, type ShellContext } from "./src/shell.js";
5
6
 
6
7
  /**
7
8
  * Snippets Plugin for OpenCode
8
- *
9
+ *
9
10
  * Expands hashtag-based shortcuts in user messages into predefined text snippets.
10
- *
11
+ *
11
12
  * @see https://github.com/JosXa/opencode-snippets for full documentation
12
13
  */
13
14
  export const SnippetsPlugin: Plugin = async (ctx) => {
14
- // Load all snippets at startup
15
- const snippets = await loadSnippets()
15
+ // Load all snippets at startup (global + project directory)
16
+ const startupStart = performance.now();
17
+ const snippets = await loadSnippets(ctx.directory);
18
+ const startupTime = performance.now() - startupStart;
19
+
20
+ logger.debug("Plugin startup complete", {
21
+ startupTimeMs: startupTime.toFixed(2),
22
+ snippetCount: snippets.size,
23
+ });
16
24
 
17
25
  return {
18
- "chat.message": async (input, output) => {
26
+ "chat.message": async (_input, output) => {
19
27
  // Only process user messages, never assistant messages
20
- if (output.message.role !== "user") return
21
-
28
+ if (output.message.role !== "user") return;
29
+
30
+ const messageStart = performance.now();
31
+ let expandTimeTotal = 0;
32
+ let shellTimeTotal = 0;
33
+ let processedParts = 0;
34
+
22
35
  for (const part of output.parts) {
23
36
  if (part.type === "text" && part.text) {
24
37
  // 1. Expand hashtags recursively with loop detection
25
- part.text = expandHashtags(part.text, snippets)
26
-
38
+ const expandStart = performance.now();
39
+ part.text = expandHashtags(part.text, snippets);
40
+ const expandTime = performance.now() - expandStart;
41
+ expandTimeTotal += expandTime;
42
+
27
43
  // 2. Execute shell commands: !`command`
28
- part.text = await executeShellCommands(part.text, ctx)
44
+ const shellStart = performance.now();
45
+ part.text = await executeShellCommands(part.text, ctx as unknown as ShellContext);
46
+ const shellTime = performance.now() - shellStart;
47
+ shellTimeTotal += shellTime;
48
+ processedParts += 1;
29
49
  }
30
50
  }
31
- }
32
- }
33
- }
51
+
52
+ const totalTime = performance.now() - messageStart;
53
+ if (processedParts > 0) {
54
+ logger.debug("Message processing complete", {
55
+ totalTimeMs: totalTime.toFixed(2),
56
+ snippetExpandTimeMs: expandTimeTotal.toFixed(2),
57
+ shellTimeMs: shellTimeTotal.toFixed(2),
58
+ processedParts,
59
+ });
60
+ }
61
+ },
62
+ };
63
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-snippets",
3
- "version": "1.0.0",
3
+ "version": "1.2.0",
4
4
  "description": "Hashtag-based snippet expansion plugin for OpenCode - instant inline text shortcuts",
5
5
  "main": "index.ts",
6
6
  "type": "module",
@@ -34,12 +34,17 @@
34
34
  "@opencode-ai/plugin": ">=1.0.0"
35
35
  },
36
36
  "devDependencies": {
37
+ "@biomejs/biome": "^2.3.11",
37
38
  "@types/bun": "latest",
38
39
  "@types/node": "^22",
40
+ "@typescript/native-preview": "^7.0.0-dev.20260120.1",
39
41
  "typescript": "^5"
40
42
  },
41
- "files": [
42
- "index.ts",
43
- "src/**/*.ts"
44
- ]
43
+ "scripts": {
44
+ "build": "tsgo --noEmit",
45
+ "format:check": "biome check .",
46
+ "format:fix": "biome check --write .",
47
+ "ai:check": "bun run format:fix"
48
+ },
49
+ "files": ["index.ts", "src/**/*.ts"]
45
50
  }
package/src/constants.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { join } from "node:path"
2
- import { homedir } from "node:os"
1
+ import { homedir } from "node:os";
2
+ import { join } from "node:path";
3
3
 
4
4
  /**
5
5
  * Regular expression patterns used throughout the plugin
@@ -7,15 +7,15 @@ import { homedir } from "node:os"
7
7
  export const PATTERNS = {
8
8
  /** Matches hashtags like #snippet-name */
9
9
  HASHTAG: /#([a-z0-9\-_]+)/gi,
10
-
10
+
11
11
  /** Matches shell commands like !`command` */
12
12
  SHELL_COMMAND: /!`([^`]+)`/g,
13
- } as const
13
+ } as const;
14
14
 
15
15
  /**
16
16
  * OpenCode configuration directory
17
17
  */
18
- export const OPENCODE_CONFIG_DIR = join(homedir(), ".config", "opencode")
18
+ export const OPENCODE_CONFIG_DIR = join(homedir(), ".config", "opencode");
19
19
 
20
20
  /**
21
21
  * File system paths
@@ -23,10 +23,10 @@ export const OPENCODE_CONFIG_DIR = join(homedir(), ".config", "opencode")
23
23
  export const PATHS = {
24
24
  /** OpenCode configuration directory */
25
25
  CONFIG_DIR: OPENCODE_CONFIG_DIR,
26
-
26
+
27
27
  /** Snippets directory */
28
28
  SNIPPETS_DIR: join(OPENCODE_CONFIG_DIR, "snippet"),
29
- } as const
29
+ } as const;
30
30
 
31
31
  /**
32
32
  * Plugin configuration
@@ -34,4 +34,4 @@ export const PATHS = {
34
34
  export const CONFIG = {
35
35
  /** File extension for snippet files */
36
36
  SNIPPET_EXTENSION: ".md",
37
- } as const
37
+ } as const;