dimcode-linux-arm64
Advanced tools
| interface: | ||
| display_name: "Dim Modality" | ||
| short_description: "Use DimCode multimodal CLI" | ||
| default_prompt: "Use $dim-modality to create or process media with the DimCode multimodal CLI." |
| # CLI Recipes | ||
| Use `--json` for all agent/script calls. Prefer output paths inside the current workspace. | ||
| ## Check Configuration | ||
| ```bash | ||
| dim modality list --json | ||
| dim modality get image.generate --json | ||
| ``` | ||
| ## Images | ||
| Generate an image: | ||
| ```bash | ||
| mkdir -p ./artifacts | ||
| dim image generate --prompt "A clean product poster on a white background with soft natural lighting" --out ./artifacts/image.png --json | ||
| ``` | ||
| Edit an image: | ||
| ```bash | ||
| mkdir -p ./artifacts | ||
| dim image edit --image ./input.png --prompt "Keep the subject, change the lighting to golden hour" --out ./artifacts/edited.png --json | ||
| ``` | ||
| Multi-image edit: | ||
| ```bash | ||
| dim image edit --image ./person.png --image ./style.png --prompt "Render the person in the illustration style of the reference image" --out ./artifacts/edited.png --json | ||
| ``` | ||
| Enhance an image: | ||
| ```bash | ||
| dim image enhance --image ./input.png --operation upscale --out ./artifacts/upscaled.png --json | ||
| ``` | ||
| ## Video | ||
| Text-to-video: | ||
| ```bash | ||
| mkdir -p ./artifacts | ||
| dim video generate --prompt "A robot walking slowly through a rainy neon street, cinematic camera movement" --duration 5 --out ./artifacts/video.mp4 --json --timeout 1800000 | ||
| ``` | ||
| Image-to-video: | ||
| ```bash | ||
| dim video generate --prompt "Make the person turn naturally toward the camera" --image ./input.png --duration 5 --out ./artifacts/video.mp4 --json --timeout 1800000 | ||
| ``` | ||
| Edit a video: | ||
| ```bash | ||
| dim video edit --video ./input.mp4 --operation add-audio --audio ./voice.mp3 --out ./artifacts/edited.mp4 --json | ||
| ``` | ||
| Enhance a video: | ||
| ```bash | ||
| dim video enhance --video ./input.mp4 --operation stabilize --out ./artifacts/stable.mp4 --json | ||
| ``` | ||
| ## Speech | ||
| Text-to-speech: | ||
| ```bash | ||
| mkdir -p ./artifacts | ||
| dim speech synthesize --text "Hello, this is a speech generation test." --out ./artifacts/speech.mp3 --json | ||
| ``` | ||
| Specify a voice: | ||
| ```bash | ||
| dim speech synthesize --text "Hello, this is a speech generation test." --voice "cosyvoice2:anna" --out ./artifacts/speech.mp3 --json | ||
| ``` | ||
| Transcribe audio: | ||
| ```bash | ||
| dim speech transcribe --audio ./meeting.mp3 --out ./artifacts/transcript.txt --json | ||
| ``` | ||
| ## Music And Audio | ||
| Generate music: | ||
| ```bash | ||
| mkdir -p ./artifacts | ||
| dim music generate --prompt "Warm lo-fi piano for a podcast intro" --title "warm intro" --out ./artifacts/music.mp3 --json | ||
| ``` | ||
| Generate a sound effect: | ||
| ```bash | ||
| dim audio generate --operation sound-effect --prompt "Raindrops tapping on a window" --duration 8 --out ./artifacts/rain.mp3 --json | ||
| ``` | ||
| Extract or generate audio from video: | ||
| ```bash | ||
| dim audio generate --operation video-to-audio --video ./input.mp4 --out ./artifacts/audio.mp3 --json | ||
| ``` | ||
| ## 3D Asset And Motion | ||
| ```bash | ||
| dim asset generate --prompt "low poly robot game asset" --out ./artifacts/robot.glb --json | ||
| dim motion generate --prompt "walk cycle for a friendly robot" --out ./artifacts/walk.fbx --json | ||
| ``` | ||
| ## Temporary Provider Or Model Override | ||
| Use this only when the user explicitly asks for a specific provider or model: | ||
| ```bash | ||
| dim image generate --provider siliconflow --model Qwen/Qwen-Image --prompt "..." --out ./artifacts/image.png --json | ||
| ``` | ||
| ## Provider-Specific Options | ||
| Write a JSON object file, then pass it with `--options`: | ||
| ```json | ||
| { | ||
| "seed": 1234, | ||
| "negativePrompt": "blurry, low quality" | ||
| } | ||
| ``` | ||
| ```bash | ||
| dim image generate --prompt "..." --options ./options.json --out ./artifacts/image.png --json | ||
| ``` |
| # Model Notes | ||
| This file is the entry point for provider/model experience. When a provider or model needs special handling, add a row here and put detailed notes under `references/models/`. | ||
| ## Current Entries | ||
| | Provider / model | Capability | Note | | ||
| |---|---|---| | ||
| | CosyVoice2 | `speech.synthesize` | `--voice` uses `model-name:voice-name` | | ||
| | Video generation models | `video.generate` | Generation can take a long time; use a generous `--timeout` and keep operation id / JSON errors | | ||
| | Image edit models | `image.edit` | Requires `--image` and `--prompt`; pass repeated `--image` flags for multi-image edit | | ||
| | ASR models | `speech.transcribe` | Input is `--audio`; output should be a text file | | ||
| ## CosyVoice2 Voices | ||
| CosyVoice2 `--voice` format: | ||
| ```text | ||
| model-name:voice-name | ||
| ``` | ||
| Available voice names: | ||
| ```text | ||
| alex | ||
| benjamin | ||
| charles | ||
| david | ||
| anna | ||
| bella | ||
| claire | ||
| diana | ||
| ``` | ||
| `alex`, `benjamin`, `charles`, and `david` are male voices. `anna`, `bella`, `claire`, and `diana` are female voices. | ||
| Example: | ||
| ```bash | ||
| dim speech synthesize --text "Hello, this is a test." --voice "CosyVoice2:anna" --out ./artifacts/speech.mp3 --json | ||
| ``` | ||
| If the configured model id is not exactly `CosyVoice2`, replace the part before `:` with the configured model name or model id. | ||
| ## Adding Future Model Notes | ||
| When adding a model note: | ||
| 1. Create a file under `references/models/` named after the provider and model. | ||
| 2. Add a row to the table above. | ||
| 3. Include: | ||
| - Supported capability. | ||
| - Required inputs. | ||
| - Recommended output format. | ||
| - Known slow paths or failure modes. | ||
| - One runnable `dim ... --json` example. |
| --- | ||
| name: dim-modality | ||
| description: Use DimCode multimodal CLI when the user asks the agent to generate or edit images, generate video, synthesize speech, transcribe audio, generate music or sound effects, create 3D assets or motion data, or inspect available multimodal defaults through `dim modality`. This skill teaches when to use the CLI, the main execution path, failure handling, and model-specific notes. | ||
| --- | ||
| # Dim Modality | ||
| Use this skill when media work should go through the DimCode multimodal CLI instead of legacy image tools or ad hoc provider calls. | ||
| ## When To Use | ||
| Use this skill for requests that involve: | ||
| - Generating, editing, enhancing, upscaling, or restoring images. | ||
| - Generating video from text, or generating video from an input image. | ||
| - Text-to-speech or speech-to-text. | ||
| - Music, sound effects, ambient audio, or audio extraction. | ||
| - 3D assets or motion data. | ||
| - Checking which multimodal defaults are configured. | ||
| - Debugging why a multimodal command failed. | ||
| Do not look for the old image-generation tool for these tasks. Do not add a native agent tool for each modality. The normal path is `exec` running `dim ... --json`. | ||
| ## Main Path | ||
| 1. Inspect configured multimodal defaults first: | ||
| ```bash | ||
| dim modality list --json | ||
| ``` | ||
| 2. Map the user request to a capability: | ||
| | User goal | Capability | CLI entry | | ||
| |---|---|---| | ||
| | Generate an image | `image.generate` | `dim image generate` | | ||
| | Edit an image | `image.edit` | `dim image edit` | | ||
| | Enhance an image | `image.enhance` | `dim image enhance` | | ||
| | Generate a video | `video.generate` | `dim video generate` | | ||
| | Synthesize speech | `speech.synthesize` | `dim speech synthesize` | | ||
| | Transcribe speech | `speech.transcribe` | `dim speech transcribe` | | ||
| | Generate music | `music.generate` | `dim music generate` | | ||
| | Generate audio or sound effects | `audio.generate` | `dim audio generate` | | ||
| | Generate a 3D asset | `asset.generate` | `dim asset generate` | | ||
| | Generate motion data | `motion.generate` | `dim motion generate` | | ||
| 3. Write outputs under the current workspace, usually `./artifacts/` unless the user asked for another path. | ||
| 4. Always use `--json`. Read output paths, provider/model ids, and errors from JSON instead of scraping human text. | ||
| 5. After success, reference the absolute output path. Images can be shown with Markdown image syntax. Audio and video should be reported as absolute paths until Desktop preview support is richer. | ||
| ## Operating Rules | ||
| - If the user did not specify provider/model, use the configured default for that capability. | ||
| - Only pass `--provider` and `--model` when the user explicitly asks for a temporary override. Overrides do not change defaults. | ||
| - If a default is missing, tell the user to configure that capability in Settings / Agent Defaults. | ||
| - Put provider-specific parameters in a JSON object file and pass it with `--options <path>`. | ||
| - Video generation can be slow. Use a generous `--timeout`, and never claim completion unless an output file exists. | ||
| - For failed commands, explain `error.code`, `error.message`, and `details` from the JSON output. Do not hide the original provider or CLI error. | ||
| - Keep LLM model selection separate from multimodal defaults. A chat model change does not change modality defaults. | ||
| - Do not expose API keys, credentials, headers, or provider secrets. | ||
| ## Recipes And References | ||
| Read `references/cli-recipes.md` when you need command templates. | ||
| Read `references/model-notes.md` when a provider or model may need special handling. Future model-specific reference docs should be linked from that file. | ||
| ## Failure Handling | ||
| | Situation | Response | | ||
| |---|---| | ||
| | Missing default model | Ask the user to configure the capability in Settings / Agent Defaults | | ||
| | Provider disconnected or missing key | Ask the user to check provider connection and API key | | ||
| | Model disabled | Ask the user to enable the model in the provider model list | | ||
| | Unsupported capability | Use a model that supports the capability, or ask the user to configure one | | ||
| | Output file missing | Do not report success; explain the CLI JSON error and stderr | | ||
| | Video timeout | Report timeout details and operation id when available | | ||
| ## Final Response Style | ||
| When reporting back: | ||
| - Say which capability was used. | ||
| - Give the output file path. | ||
| - If it failed, state the failure point and the next user action. | ||
| - Keep the response short unless the user asks for detailed debugging. |
| # Installing and updating Dim plugins | ||
| A Dim plugin is a self-describing directory under `~/.agents/plugins/<name>/`. Disk is the | ||
| single source of truth: Dim scans that directory, reads each `.claude-plugin/plugin.json`, and | ||
| loads the plugin's components. Installing a plugin means placing its folder there; removing the | ||
| folder removes the plugin. | ||
| ## Local development | ||
| 1. Scaffold the plugin directly under `~/.agents/plugins/<name>/` (the default of | ||
| `scripts/create_basic_plugin.py`): | ||
| ```bash | ||
| python3 scripts/create_basic_plugin.py my-plugin --with-skills | ||
| ``` | ||
| 2. Edit the manifest and component files. | ||
| 3. Start a **new session**. Dim re-scans `~/.agents/plugins`, and the plugin's skills, hooks, | ||
| and MCP servers load. A new session is the boundary at which changes are picked up. | ||
| To iterate, edit files in place and start another session. Removing the plugin directory | ||
| removes the plugin. | ||
| ## Activation by component | ||
| - **Skills** — discovered from `~/.agents/plugins/*/skills/` and loaded into the system prompt | ||
| automatically. They also appear in the desktop Skills list. | ||
| - **Hooks** — read from the file named by the manifest `hooks` field and registered on session | ||
| start. Command paths use `${CLAUDE_PLUGIN_ROOT}`, which Dim replaces with the plugin's | ||
| absolute root. | ||
| - **MCP servers** — read from `.mcp.json` (or the inline `mcpServers` object) and merged into | ||
| the MCP runtime. Server ids are namespaced `plugin:<plugin-name>/<serverId>`. | ||
| ## Toggling components | ||
| In the Dim desktop app, open Plugins → select the plugin to see its detail page. Each skill, | ||
| hook, and MCP server has its own on/off switch, plus a master switch to enable or disable all | ||
| components at once. | ||
| ## Sharing via git | ||
| To distribute a plugin to other users: | ||
| 1. Build the plugin in a directory you will push to git (use `--path`): | ||
| ```bash | ||
| python3 scripts/create_basic_plugin.py my-plugin --path ./plugins --with-skills | ||
| ``` | ||
| 2. Commit and push the plugin directory (including `.claude-plugin/plugin.json`) to a git repo. | ||
| 3. In the Dim desktop app, open Plugins → Add plugin and paste the git source — either | ||
| `owner/repo` or a full git URL, with an optional ref (branch/tag). Dim clones the repo, | ||
| reads the manifest, and installs the plugin into `~/.agents/plugins/<name>/`, recording the | ||
| source in a `dim-install.json` provenance file. | ||
| Updating a git-installed plugin is done from the same Add-plugin flow (reinstall from the | ||
| source) or by editing the on-disk copy under `~/.agents/plugins` for local tweaks. |
| # Plugin manifest spec (`.claude-plugin/plugin.json`) | ||
| Dim recognizes a plugin by the presence of `.claude-plugin/plugin.json` at the plugin root. | ||
| It parses only the fields below; any other field is ignored. Keep the manifest minimal and | ||
| valid. | ||
| ```json | ||
| { | ||
| "name": "my-plugin", | ||
| "version": "0.1.0", | ||
| "description": "Brief plugin description", | ||
| "interface": { | ||
| "displayName": "My Plugin", | ||
| "shortDescription": "Short subtitle shown in the plugin list", | ||
| "logo": "./assets/logo.png" | ||
| }, | ||
| "skills": "./skills/", | ||
| "hooks": "./hooks.json", | ||
| "mcpServers": "./.mcp.json" | ||
| } | ||
| ``` | ||
| ## Field guide | ||
| ### Top-level fields | ||
| - `name` (`string`, required): Plugin identifier (lower-case hyphen-case, no spaces). Matches | ||
| the plugin folder name. Sanitized to `[A-Za-z0-9._-]`, max 128 chars. | ||
| - `version` (`string`, optional): Strict semver, e.g. `0.1.0`. | ||
| - `description` (`string`, optional): Short purpose summary. Falls back to | ||
| `interface.shortDescription` in the UI when absent. | ||
| - `interface` (`object`, optional): Presentation metadata (see below). | ||
| - `skills` (`string`, optional): Relative path to the skills directory, normally `./skills/`. | ||
| - `hooks` (`string`, optional): Relative path to the hooks config file, e.g. `./hooks.json`. | ||
| - `mcpServers` (`string`, optional): Relative path to an `.mcp.json` file, e.g. `./.mcp.json`. | ||
| An inline object is not supported — the runtime loads only a path string. | ||
| ### `interface` fields | ||
| - `displayName` (`string`): User-facing title. Defaults to `name` when absent. | ||
| - `shortDescription` (`string`): Brief subtitle for compact views. | ||
| - `logo` (`string`): Relative path to a logo asset inside the plugin (for example | ||
| `./assets/logo.png`). | ||
| ### Path conventions | ||
| - Component path values are relative to the plugin root and begin with `./`. | ||
| - A component path appears in the manifest only when its file/directory exists. Do not declare | ||
| `skills`, `hooks`, or `mcpServers` when their companion files were not created. | ||
| ## Components | ||
| ### Skills | ||
| Skills live under `skills/<skill-name>/SKILL.md`. Each `SKILL.md` starts with YAML | ||
| frontmatter: | ||
| ```markdown | ||
| --- | ||
| name: my-skill | ||
| description: What it does and when to use it. | ||
| --- | ||
| # My Skill | ||
| Instructions for the assistant. | ||
| ``` | ||
| Dim discovers plugin skills automatically from `~/.agents/plugins/*/skills/` and loads them | ||
| into the system prompt. | ||
| ### Hooks | ||
| `hooks` points to a JSON file mapping lifecycle events to commands. Use | ||
| `${CLAUDE_PLUGIN_ROOT}` in command paths — Dim substitutes the plugin's absolute root at | ||
| runtime: | ||
| ```json | ||
| { | ||
| "hooks": { | ||
| "SessionStart": [ | ||
| { | ||
| "matcher": "startup|resume|clear|compact", | ||
| "hooks": [ | ||
| { "type": "command", "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/on-start.js\"" } | ||
| ] | ||
| } | ||
| ] | ||
| } | ||
| } | ||
| ``` | ||
| ### MCP servers | ||
| `mcpServers` points to an `.mcp.json` file. Server ids are namespaced as | ||
| `plugin:<plugin-name>/<serverId>`. Command paths support the same `${CLAUDE_PLUGIN_ROOT}` | ||
| substitution. The referenced `.mcp.json` has this shape: | ||
| ```json | ||
| { | ||
| "mcpServers": { | ||
| "example": { | ||
| "type": "stdio", | ||
| "command": "node", | ||
| "args": ["${CLAUDE_PLUGIN_ROOT}/mcp/server.js"] | ||
| } | ||
| } | ||
| } | ||
| ``` | ||
| ## Validation notes | ||
| - `name` must be a non-empty string; the folder name must match it. | ||
| - `version`, when present, must be strict semver. | ||
| - `interface.logo` and any declared component path must point to real files/directories inside | ||
| the plugin. | ||
| - Sub-skill `SKILL.md` files must have non-empty `name` and `description` frontmatter. | ||
| - The manifest must not contain `[TODO: ...]` placeholders. | ||
| Run `scripts/validate_plugin.py <plugin-path>` before handing back a generated plugin. |
| #!/usr/bin/env python3 | ||
| """Scaffold a Dim plugin directory with a `.claude-plugin/plugin.json` manifest. | ||
| Dim discovers plugins from ``~/.agents/plugins/<name>/``. The default parent is | ||
| ``~/.agents/plugins`` so a scaffolded plugin is live on the next session. Pass ``--path`` to | ||
| build a plugin elsewhere (e.g. a git repo to share). | ||
| """ | ||
| from __future__ import annotations | ||
| import argparse | ||
| import json | ||
| import re | ||
| from pathlib import Path | ||
| from typing import Any | ||
| MAX_PLUGIN_NAME_LENGTH = 64 | ||
| DEFAULT_PLUGIN_PARENT = Path.home() / ".agents" / "plugins" | ||
| MANIFEST_DIR = ".claude-plugin" | ||
| MANIFEST_FILE = "plugin.json" | ||
| def normalize_plugin_name(plugin_name: str) -> str: | ||
| """Normalize a plugin name to lowercase hyphen-case.""" | ||
| normalized = plugin_name.strip().lower() | ||
| normalized = re.sub(r"[^a-z0-9]+", "-", normalized) | ||
| normalized = normalized.strip("-") | ||
| normalized = re.sub(r"-{2,}", "-", normalized) | ||
| return normalized | ||
| def validate_plugin_name(plugin_name: str) -> None: | ||
| if not plugin_name: | ||
| raise ValueError("Plugin name must include at least one letter or digit.") | ||
| if len(plugin_name) > MAX_PLUGIN_NAME_LENGTH: | ||
| raise ValueError( | ||
| f"Plugin name '{plugin_name}' is too long ({len(plugin_name)} characters). " | ||
| f"Maximum is {MAX_PLUGIN_NAME_LENGTH} characters." | ||
| ) | ||
| def display_name_from_plugin_name(plugin_name: str) -> str: | ||
| return " ".join(part.capitalize() for part in re.split(r"[-_]+", plugin_name)) | ||
| def build_plugin_json( | ||
| plugin_name: str, | ||
| *, | ||
| with_skills: bool, | ||
| with_hooks: bool, | ||
| with_mcp: bool, | ||
| ) -> dict[str, Any]: | ||
| """Build the manifest Dim reads. Only the fields Dim parses are written.""" | ||
| display_name = display_name_from_plugin_name(plugin_name) | ||
| payload: dict[str, Any] = { | ||
| "name": plugin_name, | ||
| "version": "0.1.0", | ||
| "description": f"{display_name} plugin", | ||
| "interface": { | ||
| "displayName": display_name, | ||
| "shortDescription": f"Use {display_name} in Dim.", | ||
| }, | ||
| } | ||
| # Component paths are added only when their companion files are created, so the manifest | ||
| # never points at something that does not exist. | ||
| if with_skills: | ||
| payload["skills"] = "./skills/" | ||
| if with_hooks: | ||
| payload["hooks"] = "./hooks.json" | ||
| if with_mcp: | ||
| payload["mcpServers"] = "./.mcp.json" | ||
| return payload | ||
| def build_starter_skill(plugin_name: str) -> str: | ||
| """A minimal, valid starter skill so the plugin contributes something usable.""" | ||
| display_name = display_name_from_plugin_name(plugin_name) | ||
| return ( | ||
| "---\n" | ||
| f"name: {plugin_name}\n" | ||
| f"description: {display_name} starter skill. Replace this description with the real " | ||
| f"trigger so Dim knows when to use {display_name}.\n" | ||
| "---\n\n" | ||
| f"# {display_name}\n\n" | ||
| "Describe what this skill does and the steps the assistant should follow when it is " | ||
| "invoked. Keep instructions concrete and task-focused.\n" | ||
| ) | ||
| def write_json(path: Path, data: dict, force: bool) -> None: | ||
| if path.exists() and not force: | ||
| raise FileExistsError(f"{path} already exists. Use --force to overwrite.") | ||
| path.parent.mkdir(parents=True, exist_ok=True) | ||
| with path.open("w", encoding="utf-8") as handle: | ||
| json.dump(data, handle, indent=2) | ||
| handle.write("\n") | ||
| def write_text(path: Path, text: str, force: bool) -> None: | ||
| if path.exists() and not force: | ||
| return | ||
| path.parent.mkdir(parents=True, exist_ok=True) | ||
| path.write_text(text, encoding="utf-8") | ||
| def parse_args() -> argparse.Namespace: | ||
| parser = argparse.ArgumentParser( | ||
| description="Create a Dim plugin skeleton with a validation-ready plugin.json." | ||
| ) | ||
| parser.add_argument("plugin_name") | ||
| parser.add_argument( | ||
| "--path", | ||
| default=str(DEFAULT_PLUGIN_PARENT), | ||
| help=( | ||
| "Parent directory for the plugin (defaults to ~/.agents/plugins, where Dim " | ||
| "discovers plugins). Pass another directory to build a plugin for git sharing." | ||
| ), | ||
| ) | ||
| parser.add_argument( | ||
| "--with-skills", | ||
| action="store_true", | ||
| help="Create skills/<name>/SKILL.md starter skill and set manifest `skills`.", | ||
| ) | ||
| parser.add_argument( | ||
| "--with-hooks", | ||
| action="store_true", | ||
| help="Create hooks.json and set manifest `hooks`.", | ||
| ) | ||
| parser.add_argument( | ||
| "--with-mcp", | ||
| action="store_true", | ||
| help="Create .mcp.json and set manifest `mcpServers`.", | ||
| ) | ||
| parser.add_argument( | ||
| "--with-assets", | ||
| action="store_true", | ||
| help="Create an assets/ directory for icons and logos.", | ||
| ) | ||
| parser.add_argument("--force", action="store_true", help="Overwrite existing files") | ||
| return parser.parse_args() | ||
| def main() -> None: | ||
| args = parse_args() | ||
| raw_plugin_name = args.plugin_name | ||
| plugin_name = normalize_plugin_name(raw_plugin_name) | ||
| if plugin_name != raw_plugin_name: | ||
| print(f"Note: Normalized plugin name from '{raw_plugin_name}' to '{plugin_name}'.") | ||
| validate_plugin_name(plugin_name) | ||
| plugin_root = Path(args.path).expanduser().resolve() / plugin_name | ||
| plugin_root.mkdir(parents=True, exist_ok=True) | ||
| manifest_path = plugin_root / MANIFEST_DIR / MANIFEST_FILE | ||
| write_json( | ||
| manifest_path, | ||
| build_plugin_json( | ||
| plugin_name, | ||
| with_skills=args.with_skills, | ||
| with_hooks=args.with_hooks, | ||
| with_mcp=args.with_mcp, | ||
| ), | ||
| args.force, | ||
| ) | ||
| if args.with_skills: | ||
| write_text( | ||
| plugin_root / "skills" / plugin_name / "SKILL.md", | ||
| build_starter_skill(plugin_name), | ||
| args.force, | ||
| ) | ||
| if args.with_hooks: | ||
| # Empty hooks map; fill events with commands that use ${CLAUDE_PLUGIN_ROOT} paths. | ||
| write_json(plugin_root / "hooks.json", {"hooks": {}}, args.force) | ||
| if args.with_mcp: | ||
| write_json(plugin_root / ".mcp.json", {"mcpServers": {}}, args.force) | ||
| if args.with_assets: | ||
| (plugin_root / "assets").mkdir(parents=True, exist_ok=True) | ||
| print(f"Created plugin scaffold: {plugin_root}") | ||
| print(f"plugin manifest: {manifest_path}") | ||
| if __name__ == "__main__": | ||
| main() |
| #!/usr/bin/env python3 | ||
| """Validate a generated Dim plugin against the manifest contract Dim actually reads. | ||
| Dim recognizes a plugin by ``.claude-plugin/plugin.json`` and parses a small set of fields: | ||
| ``name`` (required), ``version``, ``description``, ``interface.{displayName,shortDescription, | ||
| logo}``, and the component paths ``skills`` / ``hooks`` / ``mcpServers``. Unknown fields are | ||
| ignored by Dim, so they are not rejected here. This validator also rejects leftover | ||
| ``[TODO: ...]`` placeholders and checks that declared component paths exist. | ||
| """ | ||
| from __future__ import annotations | ||
| import argparse | ||
| import json | ||
| import re | ||
| from pathlib import Path, PurePosixPath | ||
| from typing import Any | ||
| TODO_MARKER = "[TODO:" | ||
| SEMVER_RE = re.compile( | ||
| r"^(0|[1-9]\d*)\." | ||
| r"(0|[1-9]\d*)\." | ||
| r"(0|[1-9]\d*)" | ||
| r"(?:-(?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*)(?:\." | ||
| r"(?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*))*)?" | ||
| r"(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$" | ||
| ) | ||
| MANIFEST_REL = ".claude-plugin/plugin.json" | ||
| def parse_args() -> argparse.Namespace: | ||
| parser = argparse.ArgumentParser(description="Validate a local Dim plugin.") | ||
| parser.add_argument("plugin_path", help="Path to the plugin root directory") | ||
| return parser.parse_args() | ||
| def main() -> None: | ||
| args = parse_args() | ||
| plugin_root = Path(args.plugin_path).expanduser().resolve() | ||
| errors = validate_plugin(plugin_root) | ||
| if errors: | ||
| print("Plugin validation failed:") | ||
| for error in errors: | ||
| print(f"- {error}") | ||
| raise SystemExit(1) | ||
| print(f"Plugin validation passed: {plugin_root}") | ||
| def validate_plugin(plugin_root: Path) -> list[str]: | ||
| errors: list[str] = [] | ||
| manifest_path = plugin_root / ".claude-plugin" / "plugin.json" | ||
| manifest = load_json_object(manifest_path, errors) | ||
| if manifest is None: | ||
| return errors | ||
| reject_todo_markers(manifest, "$", errors) | ||
| validate_manifest(plugin_root, manifest, errors) | ||
| return errors | ||
| def load_json_object(path: Path, errors: list[str]) -> dict[str, Any] | None: | ||
| if not path.is_file(): | ||
| errors.append(f"missing `{MANIFEST_REL}`") | ||
| return None | ||
| try: | ||
| payload = json.loads(path.read_text(encoding="utf-8")) | ||
| except OSError: | ||
| errors.append(f"unable to read `{MANIFEST_REL}`") | ||
| return None | ||
| except json.JSONDecodeError: | ||
| errors.append(f"`{MANIFEST_REL}` must be valid JSON") | ||
| return None | ||
| if not isinstance(payload, dict): | ||
| errors.append(f"`{MANIFEST_REL}` must contain a JSON object") | ||
| return None | ||
| return payload | ||
| def reject_todo_markers(value: Any, path: str, errors: list[str]) -> None: | ||
| if isinstance(value, str): | ||
| if TODO_MARKER in value: | ||
| errors.append(f"{path} still contains a `[TODO: ...]` placeholder") | ||
| return | ||
| if isinstance(value, list): | ||
| for index, item in enumerate(value): | ||
| reject_todo_markers(item, f"{path}[{index}]", errors) | ||
| return | ||
| if isinstance(value, dict): | ||
| for key, item in value.items(): | ||
| reject_todo_markers(item, f"{path}.{key}", errors) | ||
| def validate_manifest(plugin_root: Path, manifest: dict[str, Any], errors: list[str]) -> None: | ||
| require_non_empty_string(manifest, "name", errors) | ||
| version = manifest.get("version") | ||
| if version is not None: | ||
| if not isinstance(version, str) or SEMVER_RE.fullmatch(version) is None: | ||
| errors.append("plugin.json field `version` must be strict semver") | ||
| validate_optional_non_empty_string(manifest, "description", errors) | ||
| interface = manifest.get("interface") | ||
| if interface is not None: | ||
| if not isinstance(interface, dict): | ||
| errors.append("plugin.json field `interface` must be an object") | ||
| else: | ||
| validate_optional_non_empty_string(interface, "displayName", errors, prefix="interface") | ||
| validate_optional_non_empty_string(interface, "shortDescription", errors, prefix="interface") | ||
| validate_optional_asset_path(plugin_root, interface, "logo", errors) | ||
| validate_component_path(plugin_root, manifest, "skills", errors) | ||
| validate_component_path(plugin_root, manifest, "hooks", errors) | ||
| validate_mcp_servers(plugin_root, manifest, errors) | ||
| validate_skill_manifests(plugin_root, errors) | ||
| def require_non_empty_string( | ||
| payload: dict[str, Any], | ||
| key: str, | ||
| errors: list[str], | ||
| *, | ||
| prefix: str | None = None, | ||
| ) -> None: | ||
| value = payload.get(key) | ||
| field = f"{prefix}.{key}" if prefix is not None else key | ||
| if not isinstance(value, str) or not value.strip(): | ||
| errors.append(f"plugin.json field `{field}` must be a non-empty string") | ||
| def validate_optional_non_empty_string( | ||
| payload: dict[str, Any], | ||
| key: str, | ||
| errors: list[str], | ||
| *, | ||
| prefix: str | None = None, | ||
| ) -> None: | ||
| value = payload.get(key) | ||
| if value is None: | ||
| return | ||
| field = f"{prefix}.{key}" if prefix is not None else key | ||
| if not isinstance(value, str) or not value.strip(): | ||
| errors.append(f"plugin.json field `{field}` must be a non-empty string") | ||
| def validate_component_path( | ||
| plugin_root: Path, | ||
| manifest: dict[str, Any], | ||
| key: str, | ||
| errors: list[str], | ||
| ) -> None: | ||
| """`skills` resolves to a directory; `hooks` resolves to a file. Both must exist.""" | ||
| value = manifest.get(key) | ||
| if value is None: | ||
| return | ||
| if not isinstance(value, str) or not value.strip(): | ||
| errors.append(f"plugin.json field `{key}` must be a non-empty relative path") | ||
| return | ||
| resolved = resolve_inside(plugin_root, value, key, errors) | ||
| if resolved is None: | ||
| return | ||
| if key == "skills" and not resolved.is_dir(): | ||
| errors.append(f"plugin.json field `skills` points to a missing directory") | ||
| if key == "hooks" and not resolved.is_file(): | ||
| errors.append(f"plugin.json field `hooks` points to a missing file") | ||
| def validate_mcp_servers(plugin_root: Path, manifest: dict[str, Any], errors: list[str]) -> None: | ||
| value = manifest.get("mcpServers") | ||
| if value is None: | ||
| return | ||
| # Runtime only loads `mcpServers` as a path to an `.mcp.json` file; an inline | ||
| # object is silently ignored by the loader, so reject it here too. | ||
| if isinstance(value, str) and value.strip(): | ||
| resolved = resolve_inside(plugin_root, value, "mcpServers", errors) | ||
| if resolved is not None and not resolved.is_file(): | ||
| errors.append("plugin.json field `mcpServers` points to a missing file") | ||
| return | ||
| errors.append("plugin.json field `mcpServers` must be a relative path string to an `.mcp.json` file") | ||
| def validate_optional_asset_path( | ||
| plugin_root: Path, | ||
| payload: dict[str, Any], | ||
| key: str, | ||
| errors: list[str], | ||
| ) -> None: | ||
| value = payload.get(key) | ||
| if value is None: | ||
| return | ||
| if not isinstance(value, str) or not value.strip(): | ||
| errors.append(f"plugin.json field `interface.{key}` must be a non-empty relative path") | ||
| return | ||
| resolved = resolve_inside(plugin_root, value, f"interface.{key}", errors) | ||
| if resolved is not None and not resolved.is_file(): | ||
| errors.append(f"plugin.json field `interface.{key}` points to a missing file") | ||
| def resolve_inside( | ||
| plugin_root: Path, | ||
| raw_path: str, | ||
| field: str, | ||
| errors: list[str], | ||
| ) -> Path | None: | ||
| """Resolve a relative path and confirm it stays inside the plugin root.""" | ||
| candidate = PurePosixPath(raw_path.replace("\\", "/")) | ||
| if candidate.is_absolute() or ".." in candidate.parts: | ||
| errors.append(f"plugin.json field `{field}` must stay inside the plugin directory") | ||
| return None | ||
| resolved = (plugin_root / candidate.as_posix()).resolve() | ||
| if not resolved.is_relative_to(plugin_root.resolve()): | ||
| errors.append(f"plugin.json field `{field}` must stay inside the plugin directory") | ||
| return None | ||
| return resolved | ||
| def validate_skill_manifests(plugin_root: Path, errors: list[str]) -> None: | ||
| skills_root = plugin_root / "skills" | ||
| if not skills_root.is_dir(): | ||
| return | ||
| for skill_root in sorted(skills_root.iterdir(), key=lambda path: path.name): | ||
| if skill_root.name.startswith(".") or not skill_root.is_dir(): | ||
| continue | ||
| validate_skill_manifest(skill_root, errors) | ||
| def validate_skill_manifest(skill_root: Path, errors: list[str]) -> None: | ||
| skill_md_path = skill_root / "SKILL.md" | ||
| if not skill_md_path.is_file(): | ||
| errors.append(f"skill `{skill_root.name}` is missing `SKILL.md`") | ||
| return | ||
| try: | ||
| contents = skill_md_path.read_text(encoding="utf-8") | ||
| except OSError: | ||
| errors.append(f"unable to read skill `{skill_root.name}`") | ||
| return | ||
| frontmatter = parse_frontmatter(contents) | ||
| if frontmatter is None: | ||
| errors.append(f"skill `{skill_root.name}` must start with closed YAML frontmatter") | ||
| return | ||
| if not frontmatter_field(frontmatter, "name"): | ||
| errors.append(f"skill `{skill_root.name}` frontmatter field `name` must be non-empty") | ||
| if not frontmatter_field(frontmatter, "description"): | ||
| errors.append(f"skill `{skill_root.name}` frontmatter field `description` must be non-empty") | ||
| def parse_frontmatter(contents: str) -> str | None: | ||
| """Return the raw frontmatter block, or None when it is missing/unclosed. | ||
| Parsed by hand (no yaml dependency) — only the closed `---` fenced block is needed. | ||
| """ | ||
| if not contents.startswith("---\n"): | ||
| return None | ||
| end = contents.find("\n---", 4) | ||
| if end == -1: | ||
| return None | ||
| return contents[4:end] | ||
| def frontmatter_field(frontmatter: str, key: str) -> bool: | ||
| """True when `key:` appears with a non-empty value on its own line.""" | ||
| pattern = re.compile(rf"^{re.escape(key)}:\s*(.+?)\s*$", re.MULTILINE) | ||
| match = pattern.search(frontmatter) | ||
| return bool(match and match.group(1).strip()) | ||
| if __name__ == "__main__": | ||
| main() |
| --- | ||
| name: plugin-creator | ||
| description: Create and scaffold Dim plugin directories with a required `.claude-plugin/plugin.json` manifest, optional skills/hooks/MCP components, and valid manifest defaults. Use when the user wants to create a new Dim plugin, add skills/hooks/MCP servers to a plugin, validate a plugin before sharing, or set up a plugin for local development under `~/.agents/plugins`. | ||
| metadata: | ||
| short-description: Create or scaffold a Dim plugin | ||
| --- | ||
| # Plugin Creator | ||
| Scaffold a Dim plugin: a directory with a `.claude-plugin/plugin.json` manifest plus | ||
| optional `skills/`, a `hooks` file, and an `.mcp.json`. Dim discovers plugins from | ||
| `~/.agents/plugins/<name>/`: each plugin folder is self-describing, so placing one under | ||
| `~/.agents/plugins` makes its skills, hooks, and MCP servers load on the next session. | ||
| ## Quick Start | ||
| 1. Scaffold the plugin. By default it is created live under `~/.agents/plugins/<name>/`, | ||
| so a new session picks it up immediately. | ||
| ```bash | ||
| # Names are normalized to lower-case hyphen-case (<= 64 chars). The folder name and the | ||
| # manifest `name` are always identical. Run from the skill root (the directory holding | ||
| # this SKILL.md). | ||
| python3 scripts/create_basic_plugin.py <plugin-name> | ||
| ``` | ||
| 2. Edit `<plugin-path>/.claude-plugin/plugin.json` when the request gives specific | ||
| metadata (description, display name, version). The scaffold starts with valid defaults | ||
| and must not contain `[TODO: ...]` placeholders. | ||
| 3. Add the components the plugin needs: | ||
| ```bash | ||
| python3 scripts/create_basic_plugin.py <plugin-name> \ | ||
| --with-skills --with-hooks --with-mcp --with-assets | ||
| ``` | ||
| - `--with-skills` creates `skills/<plugin-name>/SKILL.md`, a working starter skill, and | ||
| sets manifest `"skills": "./skills/"`. | ||
| - `--with-hooks` creates `hooks.json` and sets manifest `"hooks": "./hooks.json"`. | ||
| - `--with-mcp` creates `.mcp.json` and sets manifest `"mcpServers": "./.mcp.json"`. | ||
| - `--with-assets` creates an `assets/` folder for icons/logos. | ||
| 4. Build a plugin for git distribution instead of local dev with `--path`: | ||
| ```bash | ||
| # Create under a repo or scratch directory you will push to git. | ||
| python3 scripts/create_basic_plugin.py <plugin-name> --path <parent-directory> | ||
| ``` | ||
| 5. Validate before handing back or sharing: | ||
| ```bash | ||
| python3 scripts/validate_plugin.py <plugin-path> | ||
| ``` | ||
| 6. **Tell the user to start a new session/thread.** Dim loads a plugin's skills, hooks, and | ||
| MCP servers when a session starts, so a plugin created or changed in the *current* session | ||
| only becomes usable in the *next* one. If you scaffolded the plugin mid-conversation, the | ||
| skill will not be callable here yet — the user must open a new session to use it. | ||
| ## What this skill creates | ||
| - Plugin root at `<parent-directory>/<plugin-name>/` (default parent `~/.agents/plugins`). | ||
| - Always `<plugin-root>/.claude-plugin/plugin.json` with the manifest shape Dim reads. | ||
| - `<plugin-name>` is normalized: | ||
| - `My Plugin` → `my-plugin` | ||
| - `My--Plugin` → `my-plugin` | ||
| - underscores, spaces, and punctuation become `-`; consecutive hyphens collapse; result is | ||
| lower-case. | ||
| - Optional, created only when requested: | ||
| - `skills/<plugin-name>/SKILL.md` (a starter skill) | ||
| - `hooks.json` | ||
| - `.mcp.json` | ||
| - `assets/` | ||
| ## Manifest | ||
| Dim reads a small set of fields from `plugin.json`. The scaffold writes valid defaults for | ||
| all of them: | ||
| ```json | ||
| { | ||
| "name": "my-plugin", | ||
| "version": "0.1.0", | ||
| "description": "My Plugin plugin", | ||
| "interface": { | ||
| "displayName": "My Plugin", | ||
| "shortDescription": "Use My Plugin in Dim." | ||
| }, | ||
| "skills": "./skills/", | ||
| "hooks": "./hooks.json", | ||
| "mcpServers": "./.mcp.json" | ||
| } | ||
| ``` | ||
| Component paths (`skills`, `hooks`, `mcpServers`) appear only when their files exist. See | ||
| `references/plugin-json-spec.md` for the full field guide and the exact sample. | ||
| ## Components | ||
| - **Skills** — directories under `skills/<name>/` each containing a `SKILL.md` with valid | ||
| frontmatter (`name`, `description`). Dim discovers them automatically and loads them into | ||
| the system prompt. | ||
| - **Hooks** — a JSON file (manifest `hooks` field) mapping lifecycle events to commands. Use | ||
| `${CLAUDE_PLUGIN_ROOT}` in command paths; Dim substitutes the plugin's absolute root at | ||
| runtime, e.g. `node "${CLAUDE_PLUGIN_ROOT}/hooks/on-start.js"`. | ||
| - **MCP** — an `.mcp.json` with an `mcpServers` object. Server ids are namespaced as | ||
| `plugin:<plugin-name>/<serverId>`. Command paths support the same `${CLAUDE_PLUGIN_ROOT}` | ||
| substitution. | ||
| ## Required behavior | ||
| - The outer folder name and `plugin.json` `"name"` are always the same normalized name. | ||
| - Keep `.claude-plugin/plugin.json` present; never remove the required manifest. | ||
| - Do not leave `[TODO: ...]` placeholders in the manifest. | ||
| - Add a component path to the manifest only when its file actually exists. Keep `skills`, | ||
| `hooks`, and `mcpServers` out of the manifest when their companion files were not created. | ||
| - Use `--force` only when overwriting an existing plugin path is intentional. | ||
| - After creating or modifying a plugin, end by telling the user to start a new session/thread. | ||
| The current session loaded its skills, hooks, and MCP servers at startup and will not pick up | ||
| the new ones until a fresh session — do not claim the new skill is usable in this session. | ||
| ## Install and update | ||
| For the full local-development loop, the git-sharing path, and component toggling, see | ||
| `references/installing-and-updating.md`. In short: | ||
| - **Local dev** — scaffold into `~/.agents/plugins/<name>/`, then start a new session. Dim | ||
| re-scans the directory and loads the plugin's skills, hooks, and MCP servers. Editing files | ||
| in place and starting another session picks up the changes. | ||
| - **Share** — push the plugin directory to a git repo, then in the Dim desktop app open | ||
| Plugins → Add plugin and paste the git URL (`owner/repo` or a full URL). Dim reads the | ||
| manifest and installs the plugin into `~/.agents/plugins`. | ||
| ## Validation | ||
| After editing this `SKILL.md`, run: | ||
| ```bash | ||
| python3 ../skill-creator/scripts/quick_validate.py . | ||
| ``` | ||
| Before handing back a generated plugin, run: | ||
| ```bash | ||
| python3 scripts/validate_plugin.py <plugin-path> | ||
| ``` |
+1
-1
| { | ||
| "name": "dimcode-linux-arm64", | ||
| "version": "0.2.11", | ||
| "version": "0.2.12-beta.0", | ||
| "description": "dimcode binary for Linux ARM64", | ||
@@ -5,0 +5,0 @@ "os": [ |
Sorry, the diff of this file is not supported yet
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
167160625
1.37%407
2.26%37484
1.03%