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 +124 -69
- package/index.ts +47 -17
- package/package.json +10 -5
- package/src/constants.ts +8 -8
- package/src/expander.test.ts +456 -0
- package/src/expander.ts +47 -37
- package/src/loader.test.ts +261 -0
- package/src/loader.ts +99 -45
- package/src/logger.test.ts +136 -0
- package/src/logger.ts +95 -95
- package/src/shell.ts +30 -24
- package/src/types.ts +6 -6
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
|
-
|
|
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
|
-
#
|
|
19
|
+
# Slash commands (must be first):
|
|
11
20
|
/git-status Please review my changes
|
|
12
21
|
|
|
13
|
-
#
|
|
22
|
+
# Snippets (anywhere!):
|
|
14
23
|
Please review my changes #git-status and suggest improvements #code-style
|
|
15
24
|
```
|
|
16
25
|
|
|
17
|
-
|
|
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
|
-
|
|
22
|
-
# Add to your opencode.json plugins array:
|
|
23
|
-
"plugins": ["opencode-snippets"]
|
|
73
|
+
Add to your `opencode.json` plugins array:
|
|
24
74
|
|
|
25
|
-
|
|
26
|
-
|
|
75
|
+
```json
|
|
76
|
+
{
|
|
77
|
+
"plugins": [
|
|
78
|
+
"opencode-snippets"
|
|
79
|
+
]
|
|
80
|
+
}
|
|
27
81
|
```
|
|
28
82
|
|
|
29
83
|
## Quick Start
|
|
30
84
|
|
|
31
|
-
**1. Create
|
|
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:
|
|
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
|
|
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
|
-
##
|
|
61
|
-
|
|
62
|
-
### Hashtag Expansion
|
|
109
|
+
## Where to Store Snippets
|
|
63
110
|
|
|
64
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
#
|
|
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
|
|
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:
|
|
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:
|
|
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
|
|
155
|
-
| Multiple per message | No | Yes |
|
|
156
|
-
| Live shell data |
|
|
157
|
-
| Best for |
|
|
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
|
|
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 {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
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
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
"
|
|
42
|
-
"
|
|
43
|
-
"
|
|
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 {
|
|
2
|
-
import {
|
|
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;
|