dsh-plugin
DayuanJiang/next-ai-draw-iodsh 的 draw.io 绘图插件:通过 next-ai-drawio MCP 服务器作图,点回复里的链接在右侧栏打开实时预览,可以手动修改图,AI 的每次修改都是可以恢复的版本。需 Node.js 18+,首次使用由 npx 下载 MCP 服务器。
发送前把消息中的 #tag 展开为片段库配置的正文;支持多片段库、别名、变量占位符与递归防护。
Type #tag and it is automatically replaced with the full text configured in your snippet library before the message is sent. Write high-frequency instructions once, then trigger them with a single short tag.
User types: Please check this code with #review-lens
Actually sent: Please check this code with Please review strictly: first clarify the task goal, then reason step by step;
… (full review checklist) …
#tag in a message and the message.beforeSend hook performs the replacement before sending.aliases).{{name}} and {{name:default}}, with values supplied by the host.#tags and expand layer by layer; built-in triple protection covers loop detection, a depth limit, and a count limit.#tags inside fenced code blocks and inline code are never expanded by mistake.steno.save / steno.remove tools plus a CLI let you edit and persist snippet libraries on the fly.steno.search retrieves snippets by relevance; the CLI dsh-steno preview shows the fully expanded result.npm install # install dependencies and build (the prepare hook builds automatically)
npx dsh-steno init # initialize an example library at ~/.dsh/steno/core.yaml (or use --dir)
Edit ~/.dsh/steno/core.yaml and add a snippet:
name: core
entries:
- tag: careful
aliases: [safe]
description: strict mode
body: |
Reason step by step, self-check before output, ask when in doubt, do not speculate.
Write #careful in a message. Snippet libraries support two auto-discovery locations (both can be overridden by explicit configuration):
| Location | Path |
|---|---|
| Project-level | <project dir>/.dsh/steno/*.yaml |
| User-level | ~/.dsh/steno/*.yaml |
When you do not rely on auto-discovery, you can use the environment variable DSH_STENO_LIBRARIES (semicolon-separated path list) or the libraries configuration passed by the host.
A snippet library is a YAML file, a single top-level mapping whose entries is a list of snippets:
name: dev-tools # library name (defaults to file name); used as target for steno.save
description: description # optional
entries:
- tag: review-lens # required: trigger tag
aliases: [review] # optional: string or string array
description: description # optional
body: | # required: string / multi-line string / string array
#focus
Review for correctness, maintainability, performance, and security.
- tag: commit
body: |
{{type:feat}}({{scope:optional}}): {{subject}}
Field rules:
tag: must start with a letter (including Unicode letters such as Chinese), may contain letters/digits/_/-, up to 64 characters; matching is case-insensitive.aliases: a single string or list of strings, same rules as tag.body: a string, multi-line string (YAML block scalar), or string array (joined line by line).tags or invalid fields within one library fail the whole load (the error lists every problem); duplicates across libraries keep the earlier-loaded one and only warn, without blocking.Note: when a library file is edited via the tools/CLI, the file is re-serialized (format normalized, comments lost). Manual edits are unaffected — you can edit the YAML directly at any time.
# + tag name; # must not be immediately preceded by an ASCII letter/digit/_/-/# or a backslash (avoiding foo#tag and ##tag).#tag directly adjacent to Chinese characters is valid (e.g., 请#专注模式).# must be immediately followed by a letter; #123 and markdown headings like # Heading do not match.#tags inside fenced code blocks (``` and ~~~) and inline code (`x`) are not expanded; unclosed fences/inline code are treated as plain text.\#tag outputs the literal #tag without expansion.| Syntax | Behavior |
|---|---|
{{name}} | Replaced when a variable value is present; otherwise kept verbatim and counted in unresolved |
{{name:default}} | Uses the default value when no variable value is available |
\{{name}} | Outputs the literal {{name}} |
The host can pass variables when invoking hooks/tools (e.g., {"topic": "release plan"}). Each occurrence of a default is resolved independently.
Placeholder regions are opaque: #tags inside {{...}} do not participate in expansion, and defaults are used literally; under the default config (skipCode: true), placeholders inside code regions are not replaced either.
Snippet bodies can reference other snippets (e.g., review-lens referencing focus). Expansion has triple protection; when triggered, the literal text is kept and a warning is emitted:
a → b → a terminates immediately).maxDepth configurable).maxExpansions configurable), preventing combinatorial explosion.The host can pass a config object when loading the plugin (JSON Schema in config.schema.json):
{
"libraries": ["~/.dsh/steno/core.yaml", ".dsh/steno/project.yaml"],
"defaultLibrary": "core",
"maxDepth": 8,
"maxExpansions": 200,
"skipCode": true,
"keepUnknown": true
}
| Field | Default | Description |
|---|---|---|
libraries | auto-discovered | Array of library file paths, order is priority (earlier wins); supports ~ and ${VAR} |
defaultLibrary | first library | Write target for steno.save when no library is specified |
maxDepth | 8 | Maximum recursion depth for expansion |
maxExpansions | 200 | Upper bound of replacements in a single expansion |
skipCode | true | Whether to skip tags inside code regions |
keepUnknown | true | Whether to keep placeholders verbatim when they have no value and no default |
Library path resolution order: explicit libraries → environment variable DSH_STENO_LIBRARIES → auto-discovery.
dsh plugin --profile demo add github:JohnXu22786/snippet-expander
After installation you can use #tag in messages right away. To remove:
dsh plugin --profile demo remove dsh-steno
The plugin is self-contained; the manifest lives in plugin.json, and the loading contract is as follows:
const mod = await import('./dsh-plugin/snippet-expander/dist/index.js');
const plugin = await mod.createPlugin({
config: { libraries: ['~/.dsh/steno/core.yaml'] },
logger: console, // optional
});
// or use the default export: await mod.default({ config });
The returned instance:
{
id: 'steno',
name: 'Steno',
version: '1.0.0',
hooks: { 'message.beforeSend': handler }, // event interface
tools: [ ToolDef, ... ], // tool interface (5 in total)
config: ResolvedConfig,
warnings: string[], // load warnings
registry, engine, // programmatic access
dispose(): Promise<void>,
}
| Event | Timing | Behavior |
|---|---|---|
message.beforeSend | Before the user message is sent to the model | Expands #tags in the message, returns the new message and meta.steno (touched tags, warnings, unresolved placeholders) |
const out = await plugin.hooks['message.beforeSend']({
message: 'Please #focus this',
variables: { topic: 'release' }, // optional
});
// out.message is the expanded text; out.meta.steno contains touched / warnings / unresolved
Non-string messages pass through unchanged. The host can attach meta.steno.warnings to the session context so the model knows what happened.
The host registers the following tools as LLM-callable functions (each tool has name / description / inputSchema / run):
| Tool | Purpose |
|---|---|
steno.list | List all snippets (optionally in a specific library) |
steno.search | Search by relevance over tag/alias/description/body |
steno.expand | Expand arbitrary text and return results and warnings |
steno.save | Add/update a snippet and persist it (tag, body, aliases, description, library) |
steno.remove | Delete a snippet |
skills/steno.md is a model-facing skill description (name/description + usage) that hosts with skill loading support can load directly.
plugin.json declares: apiVersion: dsh/plugin@1, runtime.node (entry: dist/index.js, factory: createPlugin, ESM default export), hooks, tools, skills, configSchema. Hosts can use it for capability discovery and validation.
The package also declares dsh.bundle (package.json → cordis.patch.yml), so dsh plugin add github:JohnXu22786/snippet-expander installs it as a Cordis plugin: dist/index.js additionally exports name (dsh-steno), inject = ['tools'] and apply(ctx, config). apply loads the plugin through createPlugin(), registers the 5 tools as dsh ToolDefinitions, and attaches the message.beforeSend hook when the harness emits that event; unload (hot reload) disposes all registrations. The row config is the same StenoConfig documented in Configuration.
dsh-steno list [--library <name>] [--json]
dsh-steno search <term> [--limit <n>] [--json]
dsh-steno preview <tag> [--var k=v] [--json]
dsh-steno expand <text...> [--var k=v] [--json] # read stdin when text is -
dsh-steno add <tag> <body...> [--library <name>] [--alias <a>] [--description <d>] [--stdin]
dsh-steno remove <tag> [--library <name>]
dsh-steno paths # show config resolution results and warnings
dsh-steno init [--dir <path>]
The CLI shares the same snippet libraries and expansion logic as the plugin and can be used standalone for debugging.
npm run build # compile TypeScript to dist/
npm test # build + full node:test suite
npm run demo # simulate a host loading the plugin and exercise hooks and tools
Source layout:
src/
core/ matcher (tag scanning) / placeholders ({{variables}}) / engine (expansion engine)
store/ library (library file parsing/serialization) / registry (multi-library index and management)
plugin/ hooks (event hooks) / tools (tool definitions)
index.ts plugin entry (createPlugin)
config.ts config parsing
cli.ts CLI entry
test/ node:test tests
libs/ example snippet libraries skills/ skill docs scripts/ demo scripts
list shows the real contents of library files; shadowed (later-loaded duplicate-named) entries are also listed so priority issues are easier to diagnose._/-, and do not support spaces.maxExpansions limit; in extreme cases some tags stay literal and produce warnings.This project is released under the MIT License.