opencode-snippets 1.2.0 β 1.4.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 +73 -13
- package/index.ts +20 -2
- package/package.json +4 -2
- package/src/commands.ts +335 -0
- package/src/constants.ts +2 -7
- package/src/expander.test.ts +468 -86
- package/src/expander.ts +167 -9
- package/src/loader.test.ts +25 -25
- package/src/loader.ts +154 -30
- package/src/logger.ts +2 -2
- package/src/notification.ts +29 -0
- package/src/types.ts +38 -2
package/README.md
CHANGED
|
@@ -2,11 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
β¨ **Instant inline text expansion for OpenCode** - Type `#snippet` anywhere in your message and watch it transform.
|
|
4
4
|
|
|
5
|
+
> [!TIP]
|
|
6
|
+
> **Share Your Snippets!**
|
|
7
|
+
> Got a snippet that saves you time? Share yours or steal ideas from the community!
|
|
8
|
+
> Browse and contribute in [GitHub Discussions](https://github.com/JosXa/opencode-snippets/discussions/categories/snippets).
|
|
9
|
+
|
|
5
10
|
## Why Snippets?
|
|
6
11
|
|
|
7
12
|
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
13
|
|
|
9
|
-
Stop copy-pasting the same instructions into every message. Snippets bring software engineering principles to prompt engineering:
|
|
14
|
+
Stop copy-pasting (or worse, *typing* π€’) the same instructions into every message. Snippets bring software engineering principles to prompt engineering:
|
|
10
15
|
|
|
11
16
|
- π **DRY** - Write once, reuse everywhere
|
|
12
17
|
- π§© **Composability** - Build complex prompts from simple pieces
|
|
@@ -29,7 +34,7 @@ Snippets work like `@file` mentions - natural, inline, composable.
|
|
|
29
34
|
|
|
30
35
|
Snippets compose with each other and with slash commands. Reference `#snippets` anywhere - in your messages, in slash commands, even inside other snippets:
|
|
31
36
|
|
|
32
|
-
**Example: Extending
|
|
37
|
+
**Example: Extending commands with snippets**
|
|
33
38
|
|
|
34
39
|
`~/.config/opencode/command/commit-and-push.md`:
|
|
35
40
|
```markdown
|
|
@@ -37,6 +42,7 @@ Snippets compose with each other and with slash commands. Reference `#snippets`
|
|
|
37
42
|
description: Create a git commit and push to remote
|
|
38
43
|
---
|
|
39
44
|
Please create a git commit with the current changes and push to the remote repository.
|
|
45
|
+
#use-conventional-commits
|
|
40
46
|
|
|
41
47
|
Here is the current git status:
|
|
42
48
|
!`git status`
|
|
@@ -44,10 +50,11 @@ Here is the current git status:
|
|
|
44
50
|
Here are the staged changes:
|
|
45
51
|
!`git diff --cached`
|
|
46
52
|
|
|
47
|
-
#conventional-commits
|
|
48
53
|
#project-context
|
|
49
54
|
```
|
|
50
55
|
|
|
56
|
+
You could also make "current git status and staged changes" a shell-enabled snippet of its own.
|
|
57
|
+
|
|
51
58
|
**Example: Snippets composing snippets**
|
|
52
59
|
|
|
53
60
|
`~/.config/opencode/snippet/code-standards.md`:
|
|
@@ -101,10 +108,7 @@ Ask clarifying questions if anything is ambiguous.
|
|
|
101
108
|
|
|
102
109
|
**3. Use it anywhere:**
|
|
103
110
|
|
|
104
|
-
|
|
105
|
-
Refactor this function. Think step by step. Double-check your work before making changes.
|
|
106
|
-
Ask clarifying questions if anything is ambiguous.
|
|
107
|
-
```
|
|
111
|
+
https://github.com/user-attachments/assets/d31b69b5-cc7a-4208-9f6e-71c1a278536a
|
|
108
112
|
|
|
109
113
|
## Where to Store Snippets
|
|
110
114
|
|
|
@@ -186,6 +190,48 @@ I reference I reference I reference ... (15 times) ... I reference #self
|
|
|
186
190
|
|
|
187
191
|
This generous limit supports complex snippet hierarchies while preventing infinite loops.
|
|
188
192
|
|
|
193
|
+
### Prepend and Append Blocks
|
|
194
|
+
|
|
195
|
+
For long reference material that would break your writing flow, use `<append>` blocks to place content at the end of your message:
|
|
196
|
+
|
|
197
|
+
```markdown
|
|
198
|
+
---
|
|
199
|
+
aliases: jira-mcp
|
|
200
|
+
---
|
|
201
|
+
Jira MCP server
|
|
202
|
+
<append>
|
|
203
|
+
## Jira MCP Usage
|
|
204
|
+
|
|
205
|
+
Use these custom field mappings when creating issues:
|
|
206
|
+
- customfield_16570 => Acceptance Criteria
|
|
207
|
+
- customfield_11401 => Team
|
|
208
|
+
</append>
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
**Input:** `Create a bug ticket in #jira-mcp about the memory leak`
|
|
212
|
+
|
|
213
|
+
**Output:**
|
|
214
|
+
```
|
|
215
|
+
Create a bug ticket in Jira MCP server about the memory leak
|
|
216
|
+
|
|
217
|
+
## Jira MCP Usage
|
|
218
|
+
|
|
219
|
+
Use these custom field mappings when creating issues:
|
|
220
|
+
- customfield_16570 => Acceptance Criteria
|
|
221
|
+
- customfield_11401 => Team
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
Write naturallyβreference what you need mid-sentenceβand the context follows at the bottom.
|
|
225
|
+
|
|
226
|
+
Use `<prepend>` for content that should appear at the top of your message. Multiple blocks of the same type are concatenated in order of appearance.
|
|
227
|
+
|
|
228
|
+
**Block behavior:**
|
|
229
|
+
- Content outside `<prepend>`/`<append>` blocks replaces the hashtag inline
|
|
230
|
+
- If a snippet has only blocks (no inline content), the hashtag is simply removed
|
|
231
|
+
- Blocks from nested snippets are collected and assembled in the final message
|
|
232
|
+
- Unclosed tags are handled leniently (rest of content becomes the block)
|
|
233
|
+
- Nested `<prepend>` inside `<append>` (or vice versa) is an errorβthe hashtag is left unchanged
|
|
234
|
+
|
|
189
235
|
## Example Snippets
|
|
190
236
|
|
|
191
237
|
### `~/.config/opencode/snippet/context.md`
|
|
@@ -217,12 +263,25 @@ Be extremely concise. No explanations unless asked.
|
|
|
217
263
|
| Live shell data | Yes π» | Yes π» |
|
|
218
264
|
| Best for | Triggering actions & workflows β‘ | Context injection π |
|
|
219
265
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
266
|
+
> [!TIP]
|
|
267
|
+
> #### My recommendation:
|
|
268
|
+
>
|
|
269
|
+
> Use /slash commands for **triggering actions and workflows** imperatively - anything that needs to happen _right now_: `/commit-and-push`, `/add-worktree`, or `/pull-rebase`.
|
|
270
|
+
> Use #snippets for **all other context engineering**.
|
|
271
|
+
>
|
|
272
|
+
> If you can't decide, get the best of both worlds and just have your command proxy through to the snippet:
|
|
273
|
+
>
|
|
274
|
+
> `~/.config/opencode/command/pull.md`:
|
|
275
|
+
> ```markdown
|
|
276
|
+
> ---
|
|
277
|
+
> description: Proxy through to the snippet at snippet/pull.md
|
|
278
|
+
> ---
|
|
279
|
+
> #pull
|
|
280
|
+
> ```
|
|
281
|
+
|
|
282
|
+
## Configuration
|
|
224
283
|
|
|
225
|
-
|
|
284
|
+
### Debug Logging
|
|
226
285
|
|
|
227
286
|
Enable debug logs by setting an environment variable:
|
|
228
287
|
|
|
@@ -243,7 +302,8 @@ Logs are written to `~/.config/opencode/logs/snippets/daily/`.
|
|
|
243
302
|
|
|
244
303
|
## Contributing
|
|
245
304
|
|
|
246
|
-
Contributions welcome! Please open an issue or PR on GitHub.
|
|
305
|
+
Contributions welcome! Please open an issue or PR on GitHub.
|
|
306
|
+
π₯ [Discord Forum](https://discord.com/channels/1391832426048651334/1463378026833379409)
|
|
247
307
|
|
|
248
308
|
## License
|
|
249
309
|
|
package/index.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Plugin } from "@opencode-ai/plugin";
|
|
2
|
-
import {
|
|
2
|
+
import { createCommandExecuteHandler } from "./src/commands.js";
|
|
3
|
+
import { assembleMessage, expandHashtags } from "./src/expander.js";
|
|
3
4
|
import { loadSnippets } from "./src/loader.js";
|
|
4
5
|
import { logger } from "./src/logger.js";
|
|
5
6
|
import { executeShellCommands, type ShellContext } from "./src/shell.js";
|
|
@@ -8,6 +9,7 @@ import { executeShellCommands, type ShellContext } from "./src/shell.js";
|
|
|
8
9
|
* Snippets Plugin for OpenCode
|
|
9
10
|
*
|
|
10
11
|
* Expands hashtag-based shortcuts in user messages into predefined text snippets.
|
|
12
|
+
* Also provides /snippet command for managing snippets.
|
|
11
13
|
*
|
|
12
14
|
* @see https://github.com/JosXa/opencode-snippets for full documentation
|
|
13
15
|
*/
|
|
@@ -22,7 +24,22 @@ export const SnippetsPlugin: Plugin = async (ctx) => {
|
|
|
22
24
|
snippetCount: snippets.size,
|
|
23
25
|
});
|
|
24
26
|
|
|
27
|
+
// Create command handler
|
|
28
|
+
const commandHandler = createCommandExecuteHandler(ctx.client, snippets, ctx.directory);
|
|
29
|
+
|
|
25
30
|
return {
|
|
31
|
+
// Register /snippet command
|
|
32
|
+
config: async (opencodeConfig) => {
|
|
33
|
+
opencodeConfig.command ??= {};
|
|
34
|
+
opencodeConfig.command.snippet = {
|
|
35
|
+
template: "",
|
|
36
|
+
description: "Manage text snippets (create, delete, list, help)",
|
|
37
|
+
};
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
// Handle /snippet command execution
|
|
41
|
+
"command.execute.before": commandHandler,
|
|
42
|
+
|
|
26
43
|
"chat.message": async (_input, output) => {
|
|
27
44
|
// Only process user messages, never assistant messages
|
|
28
45
|
if (output.message.role !== "user") return;
|
|
@@ -36,7 +53,8 @@ export const SnippetsPlugin: Plugin = async (ctx) => {
|
|
|
36
53
|
if (part.type === "text" && part.text) {
|
|
37
54
|
// 1. Expand hashtags recursively with loop detection
|
|
38
55
|
const expandStart = performance.now();
|
|
39
|
-
|
|
56
|
+
const expansionResult = expandHashtags(part.text, snippets);
|
|
57
|
+
part.text = assembleMessage(expansionResult);
|
|
40
58
|
const expandTime = performance.now() - expandStart;
|
|
41
59
|
expandTimeTotal += expandTime;
|
|
42
60
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-snippets",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"description": "Hashtag-based snippet expansion plugin for OpenCode - instant inline text shortcuts",
|
|
5
5
|
"main": "index.ts",
|
|
6
6
|
"type": "module",
|
|
@@ -38,13 +38,15 @@
|
|
|
38
38
|
"@types/bun": "latest",
|
|
39
39
|
"@types/node": "^22",
|
|
40
40
|
"@typescript/native-preview": "^7.0.0-dev.20260120.1",
|
|
41
|
+
"husky": "^9.1.7",
|
|
41
42
|
"typescript": "^5"
|
|
42
43
|
},
|
|
43
44
|
"scripts": {
|
|
44
45
|
"build": "tsgo --noEmit",
|
|
45
46
|
"format:check": "biome check .",
|
|
46
47
|
"format:fix": "biome check --write .",
|
|
47
|
-
"ai:check": "bun run format:fix"
|
|
48
|
+
"ai:check": "bun run format:fix",
|
|
49
|
+
"prepare": "husky"
|
|
48
50
|
},
|
|
49
51
|
"files": ["index.ts", "src/**/*.ts"]
|
|
50
52
|
}
|
package/src/commands.ts
ADDED
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
import { PATHS } from "./constants.js";
|
|
2
|
+
import { createSnippet, deleteSnippet, listSnippets, reloadSnippets } from "./loader.js";
|
|
3
|
+
import { logger } from "./logger.js";
|
|
4
|
+
import { sendIgnoredMessage } from "./notification.js";
|
|
5
|
+
import type { SnippetRegistry } from "./types.js";
|
|
6
|
+
|
|
7
|
+
/** Marker error to indicate command was handled */
|
|
8
|
+
const COMMAND_HANDLED_MARKER = "__SNIPPETS_COMMAND_HANDLED__";
|
|
9
|
+
|
|
10
|
+
interface CommandContext {
|
|
11
|
+
client: any;
|
|
12
|
+
sessionId: string;
|
|
13
|
+
args: string[];
|
|
14
|
+
rawArguments: string;
|
|
15
|
+
snippets: SnippetRegistry;
|
|
16
|
+
projectDir?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Creates the command execute handler for the snippets command
|
|
21
|
+
*/
|
|
22
|
+
export function createCommandExecuteHandler(
|
|
23
|
+
client: any,
|
|
24
|
+
snippets: SnippetRegistry,
|
|
25
|
+
projectDir?: string,
|
|
26
|
+
) {
|
|
27
|
+
return async (input: { command: string; sessionID: string; arguments: string }) => {
|
|
28
|
+
if (input.command !== "snippet") return;
|
|
29
|
+
|
|
30
|
+
const args = input.arguments.split(/\s+/).filter(Boolean);
|
|
31
|
+
const subcommand = args[0]?.toLowerCase() || "help";
|
|
32
|
+
|
|
33
|
+
const ctx: CommandContext = {
|
|
34
|
+
client,
|
|
35
|
+
sessionId: input.sessionID,
|
|
36
|
+
args: args.slice(1),
|
|
37
|
+
rawArguments: input.arguments,
|
|
38
|
+
snippets,
|
|
39
|
+
projectDir,
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
switch (subcommand) {
|
|
44
|
+
case "add":
|
|
45
|
+
case "create":
|
|
46
|
+
case "new":
|
|
47
|
+
await handleAddCommand(ctx);
|
|
48
|
+
break;
|
|
49
|
+
case "delete":
|
|
50
|
+
case "remove":
|
|
51
|
+
case "rm":
|
|
52
|
+
await handleDeleteCommand(ctx);
|
|
53
|
+
break;
|
|
54
|
+
case "list":
|
|
55
|
+
case "ls":
|
|
56
|
+
await handleListCommand(ctx);
|
|
57
|
+
break;
|
|
58
|
+
default:
|
|
59
|
+
await handleHelpCommand(ctx);
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
} catch (error) {
|
|
63
|
+
if (error instanceof Error && error.message === COMMAND_HANDLED_MARKER) {
|
|
64
|
+
throw error;
|
|
65
|
+
}
|
|
66
|
+
logger.error("Command execution failed", {
|
|
67
|
+
subcommand,
|
|
68
|
+
error: error instanceof Error ? error.message : String(error),
|
|
69
|
+
});
|
|
70
|
+
await sendIgnoredMessage(
|
|
71
|
+
ctx.client,
|
|
72
|
+
ctx.sessionId,
|
|
73
|
+
`Error: ${error instanceof Error ? error.message : String(error)}`,
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Signal that command was handled
|
|
78
|
+
throw new Error(COMMAND_HANDLED_MARKER);
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Handle /snippet add <name> ["content"] [--project] [--alias=<alias>] [--desc=<description>]
|
|
84
|
+
*/
|
|
85
|
+
async function handleAddCommand(ctx: CommandContext): Promise<void> {
|
|
86
|
+
const { client, sessionId, args, rawArguments, snippets, projectDir } = ctx;
|
|
87
|
+
|
|
88
|
+
if (args.length === 0) {
|
|
89
|
+
await sendIgnoredMessage(
|
|
90
|
+
client,
|
|
91
|
+
sessionId,
|
|
92
|
+
'Usage: /snippet add <name> ["content"] [options]\n\n' +
|
|
93
|
+
"Adds a new snippet. Defaults to global directory.\n\n" +
|
|
94
|
+
"Examples:\n" +
|
|
95
|
+
" /snippet add greeting\n" +
|
|
96
|
+
' /snippet add bye "see you later"\n' +
|
|
97
|
+
' /snippet add hi "hello there" --aliases hello,hey\n' +
|
|
98
|
+
' /snippet add fix "fix imports" --project\n\n' +
|
|
99
|
+
"Options:\n" +
|
|
100
|
+
" --project Add to project directory (.opencode/snippet/)\n" +
|
|
101
|
+
" --aliases X,Y,Z Add aliases (comma-separated)\n" +
|
|
102
|
+
' --desc "..." Add a description',
|
|
103
|
+
);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const name = args[0];
|
|
108
|
+
|
|
109
|
+
// Extract quoted content from raw arguments
|
|
110
|
+
// Match content between quotes after the subcommand and name
|
|
111
|
+
const quotedMatch = rawArguments.match(/(?:add|create|new)\s+\S+\s+"([^"]+)"/i);
|
|
112
|
+
const content = quotedMatch ? quotedMatch[1] : "";
|
|
113
|
+
|
|
114
|
+
const isProject = args.includes("--project");
|
|
115
|
+
const aliases: string[] = [];
|
|
116
|
+
let description: string | undefined;
|
|
117
|
+
|
|
118
|
+
// Parse arguments with --param value syntax
|
|
119
|
+
for (let i = 1; i < args.length; i++) {
|
|
120
|
+
const arg = args[i];
|
|
121
|
+
|
|
122
|
+
// Handle --aliases
|
|
123
|
+
if (arg === "--aliases") {
|
|
124
|
+
const nextArg = args[i + 1];
|
|
125
|
+
if (nextArg && !nextArg.startsWith("--")) {
|
|
126
|
+
const values = nextArg
|
|
127
|
+
.split(",")
|
|
128
|
+
.map((s) => s.trim())
|
|
129
|
+
.filter(Boolean);
|
|
130
|
+
aliases.push(...values);
|
|
131
|
+
i++; // Skip the value arg
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
// Handle --desc or --description
|
|
135
|
+
else if (arg === "--desc" || arg === "--description") {
|
|
136
|
+
const nextArg = args[i + 1];
|
|
137
|
+
if (nextArg && !nextArg.startsWith("--")) {
|
|
138
|
+
description = nextArg;
|
|
139
|
+
i++; // Skip the value arg
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Default to global, --project puts it in project directory
|
|
145
|
+
const targetDir = isProject ? projectDir : undefined;
|
|
146
|
+
const location = isProject && projectDir ? "project" : "global";
|
|
147
|
+
|
|
148
|
+
try {
|
|
149
|
+
const filePath = await createSnippet(name, content, { aliases, description }, targetDir);
|
|
150
|
+
|
|
151
|
+
// Reload snippets
|
|
152
|
+
await reloadSnippets(snippets, projectDir);
|
|
153
|
+
|
|
154
|
+
let message = `Added ${location} snippet: ${name}\nFile: ${filePath}`;
|
|
155
|
+
if (content) {
|
|
156
|
+
message += `\nContent: "${truncate(content, 50)}"`;
|
|
157
|
+
} else {
|
|
158
|
+
message += "\n\nEdit the file to add your snippet content.";
|
|
159
|
+
}
|
|
160
|
+
if (aliases.length > 0) {
|
|
161
|
+
message += `\nAliases: ${aliases.join(", ")}`;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
await sendIgnoredMessage(client, sessionId, message);
|
|
165
|
+
} catch (error) {
|
|
166
|
+
await sendIgnoredMessage(
|
|
167
|
+
client,
|
|
168
|
+
sessionId,
|
|
169
|
+
`Failed to add snippet: ${error instanceof Error ? error.message : String(error)}`,
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Handle /snippet delete <name>
|
|
176
|
+
*/
|
|
177
|
+
async function handleDeleteCommand(ctx: CommandContext): Promise<void> {
|
|
178
|
+
const { client, sessionId, args, snippets, projectDir } = ctx;
|
|
179
|
+
|
|
180
|
+
if (args.length === 0) {
|
|
181
|
+
await sendIgnoredMessage(
|
|
182
|
+
client,
|
|
183
|
+
sessionId,
|
|
184
|
+
"Usage: /snippet delete <name>\n\nDeletes a snippet by name. " +
|
|
185
|
+
"Project snippets are checked first, then global.",
|
|
186
|
+
);
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const name = args[0];
|
|
191
|
+
|
|
192
|
+
const deletedPath = await deleteSnippet(name, projectDir);
|
|
193
|
+
|
|
194
|
+
if (deletedPath) {
|
|
195
|
+
// Reload snippets
|
|
196
|
+
await reloadSnippets(snippets, projectDir);
|
|
197
|
+
|
|
198
|
+
await sendIgnoredMessage(
|
|
199
|
+
client,
|
|
200
|
+
sessionId,
|
|
201
|
+
`Deleted snippet: #${name}\nRemoved: ${deletedPath}`,
|
|
202
|
+
);
|
|
203
|
+
} else {
|
|
204
|
+
await sendIgnoredMessage(
|
|
205
|
+
client,
|
|
206
|
+
sessionId,
|
|
207
|
+
`Snippet not found: #${name}\n\nUse /snippet list to see available snippets.`,
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** Maximum characters for snippet content preview */
|
|
213
|
+
const MAX_CONTENT_PREVIEW_LENGTH = 200;
|
|
214
|
+
/** Maximum characters for aliases display */
|
|
215
|
+
const MAX_ALIASES_LENGTH = 50;
|
|
216
|
+
/** Divider line */
|
|
217
|
+
const DIVIDER = "ββββββββββββββββββββββββββββββββββββββββββββββββ";
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Truncate text with ellipsis if it exceeds maxLength
|
|
221
|
+
*/
|
|
222
|
+
function truncate(text: string, maxLength: number): string {
|
|
223
|
+
if (text.length <= maxLength) return text;
|
|
224
|
+
return text.slice(0, maxLength - 3) + "...";
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Format aliases for display, truncating if needed
|
|
229
|
+
*/
|
|
230
|
+
function formatAliases(aliases: string[]): string {
|
|
231
|
+
if (aliases.length === 0) return "";
|
|
232
|
+
|
|
233
|
+
const joined = aliases.join(", ");
|
|
234
|
+
if (joined.length <= MAX_ALIASES_LENGTH) {
|
|
235
|
+
return ` (aliases: ${joined})`;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Truncate and show count
|
|
239
|
+
const truncated = truncate(joined, MAX_ALIASES_LENGTH - 10);
|
|
240
|
+
return ` (aliases: ${truncated} +${aliases.length})`;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Format a single snippet for display
|
|
245
|
+
*/
|
|
246
|
+
function formatSnippetEntry(s: { name: string; content: string; aliases: string[] }): string {
|
|
247
|
+
const header = `${s.name}${formatAliases(s.aliases)}`;
|
|
248
|
+
const content = truncate(s.content.trim(), MAX_CONTENT_PREVIEW_LENGTH);
|
|
249
|
+
|
|
250
|
+
return `${header}\n${DIVIDER}\n${content || "(empty)"}`;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Handle /snippet list
|
|
255
|
+
*/
|
|
256
|
+
async function handleListCommand(ctx: CommandContext): Promise<void> {
|
|
257
|
+
const { client, sessionId, snippets, projectDir } = ctx;
|
|
258
|
+
|
|
259
|
+
const snippetList = listSnippets(snippets);
|
|
260
|
+
|
|
261
|
+
if (snippetList.length === 0) {
|
|
262
|
+
await sendIgnoredMessage(
|
|
263
|
+
client,
|
|
264
|
+
sessionId,
|
|
265
|
+
"No snippets found.\n\n" +
|
|
266
|
+
`Global snippets: ${PATHS.SNIPPETS_DIR}\n` +
|
|
267
|
+
(projectDir
|
|
268
|
+
? `Project snippets: ${projectDir}/.opencode/snippet/`
|
|
269
|
+
: "No project directory detected.") +
|
|
270
|
+
"\n\nUse /snippet add <name> to add a new snippet.",
|
|
271
|
+
);
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const lines: string[] = [];
|
|
276
|
+
|
|
277
|
+
// Group by source
|
|
278
|
+
const globalSnippets = snippetList.filter((s) => s.source === "global");
|
|
279
|
+
const projectSnippets = snippetList.filter((s) => s.source === "project");
|
|
280
|
+
|
|
281
|
+
if (globalSnippets.length > 0) {
|
|
282
|
+
lines.push(`ββ Global (${PATHS.SNIPPETS_DIR}) ββ`, "");
|
|
283
|
+
for (const s of globalSnippets) {
|
|
284
|
+
lines.push(formatSnippetEntry(s), "");
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (projectSnippets.length > 0) {
|
|
289
|
+
lines.push(`ββ Project (${projectDir}/.opencode/snippet/) ββ`, "");
|
|
290
|
+
for (const s of projectSnippets) {
|
|
291
|
+
lines.push(formatSnippetEntry(s), "");
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
await sendIgnoredMessage(client, sessionId, lines.join("\n").trimEnd());
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Handle /snippet help
|
|
300
|
+
*/
|
|
301
|
+
async function handleHelpCommand(ctx: CommandContext): Promise<void> {
|
|
302
|
+
const { client, sessionId } = ctx;
|
|
303
|
+
|
|
304
|
+
const helpText = `Snippets Command - Manage text snippets
|
|
305
|
+
|
|
306
|
+
Usage: /snippet <command> [options]
|
|
307
|
+
|
|
308
|
+
Commands:
|
|
309
|
+
add <name> ["content"] [options]
|
|
310
|
+
--project Add to project directory (default: global)
|
|
311
|
+
--aliases X,Y,Z Add aliases (comma-separated)
|
|
312
|
+
--desc "..." Add a description
|
|
313
|
+
|
|
314
|
+
delete <name> Delete a snippet
|
|
315
|
+
list List all available snippets
|
|
316
|
+
help Show this help message
|
|
317
|
+
|
|
318
|
+
Snippet Locations:
|
|
319
|
+
Global: ~/.config/opencode/snippet/
|
|
320
|
+
Project: <project>/.opencode/snippet/
|
|
321
|
+
|
|
322
|
+
Usage in messages:
|
|
323
|
+
Type #snippet-name to expand a snippet inline.
|
|
324
|
+
Snippets can reference other snippets recursively.
|
|
325
|
+
|
|
326
|
+
Examples:
|
|
327
|
+
/snippet add greeting
|
|
328
|
+
/snippet add bye "see you later"
|
|
329
|
+
/snippet add hi "hello there" --aliases hello,hey
|
|
330
|
+
/snippet add fix "fix imports" --project
|
|
331
|
+
/snippet delete old-snippet
|
|
332
|
+
/snippet list`;
|
|
333
|
+
|
|
334
|
+
await sendIgnoredMessage(client, sessionId, helpText);
|
|
335
|
+
}
|
package/src/constants.ts
CHANGED
|
@@ -12,20 +12,15 @@ export const PATTERNS = {
|
|
|
12
12
|
SHELL_COMMAND: /!`([^`]+)`/g,
|
|
13
13
|
} as const;
|
|
14
14
|
|
|
15
|
-
/**
|
|
16
|
-
* OpenCode configuration directory
|
|
17
|
-
*/
|
|
18
|
-
export const OPENCODE_CONFIG_DIR = join(homedir(), ".config", "opencode");
|
|
19
|
-
|
|
20
15
|
/**
|
|
21
16
|
* File system paths
|
|
22
17
|
*/
|
|
23
18
|
export const PATHS = {
|
|
24
19
|
/** OpenCode configuration directory */
|
|
25
|
-
CONFIG_DIR:
|
|
20
|
+
CONFIG_DIR: join(homedir(), ".config", "opencode"),
|
|
26
21
|
|
|
27
22
|
/** Snippets directory */
|
|
28
|
-
SNIPPETS_DIR: join(
|
|
23
|
+
SNIPPETS_DIR: join(join(homedir(), ".config", "opencode"), "snippet"),
|
|
29
24
|
} as const;
|
|
30
25
|
|
|
31
26
|
/**
|