dimcode-darwin-arm64 0.2.11-beta.0 → 0.2.12-beta.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/bin/dimcode CHANGED
Binary file
@@ -0,0 +1,88 @@
1
+ ---
2
+ name: dim-modality
3
+ 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.
4
+ ---
5
+
6
+ # Dim Modality
7
+
8
+ Use this skill when media work should go through the DimCode multimodal CLI instead of legacy image tools or ad hoc provider calls.
9
+
10
+ ## When To Use
11
+
12
+ Use this skill for requests that involve:
13
+
14
+ - Generating, editing, enhancing, upscaling, or restoring images.
15
+ - Generating video from text, or generating video from an input image.
16
+ - Text-to-speech or speech-to-text.
17
+ - Music, sound effects, ambient audio, or audio extraction.
18
+ - 3D assets or motion data.
19
+ - Checking which multimodal defaults are configured.
20
+ - Debugging why a multimodal command failed.
21
+
22
+ 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`.
23
+
24
+ ## Main Path
25
+
26
+ 1. Inspect configured multimodal defaults first:
27
+
28
+ ```bash
29
+ dim modality list --json
30
+ ```
31
+
32
+ 2. Map the user request to a capability:
33
+
34
+ | User goal | Capability | CLI entry |
35
+ |---|---|---|
36
+ | Generate an image | `image.generate` | `dim image generate` |
37
+ | Edit an image | `image.edit` | `dim image edit` |
38
+ | Enhance an image | `image.enhance` | `dim image enhance` |
39
+ | Generate a video | `video.generate` | `dim video generate` |
40
+ | Synthesize speech | `speech.synthesize` | `dim speech synthesize` |
41
+ | Transcribe speech | `speech.transcribe` | `dim speech transcribe` |
42
+ | Generate music | `music.generate` | `dim music generate` |
43
+ | Generate audio or sound effects | `audio.generate` | `dim audio generate` |
44
+ | Generate a 3D asset | `asset.generate` | `dim asset generate` |
45
+ | Generate motion data | `motion.generate` | `dim motion generate` |
46
+
47
+ 3. Write outputs under the current workspace, usually `./artifacts/` unless the user asked for another path.
48
+
49
+ 4. Always use `--json`. Read output paths, provider/model ids, and errors from JSON instead of scraping human text.
50
+
51
+ 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.
52
+
53
+ ## Operating Rules
54
+
55
+ - If the user did not specify provider/model, use the configured default for that capability.
56
+ - Only pass `--provider` and `--model` when the user explicitly asks for a temporary override. Overrides do not change defaults.
57
+ - If a default is missing, tell the user to configure that capability in Settings / Agent Defaults.
58
+ - Put provider-specific parameters in a JSON object file and pass it with `--options <path>`.
59
+ - Video generation can be slow. Use a generous `--timeout`, and never claim completion unless an output file exists.
60
+ - For failed commands, explain `error.code`, `error.message`, and `details` from the JSON output. Do not hide the original provider or CLI error.
61
+ - Keep LLM model selection separate from multimodal defaults. A chat model change does not change modality defaults.
62
+ - Do not expose API keys, credentials, headers, or provider secrets.
63
+
64
+ ## Recipes And References
65
+
66
+ Read `references/cli-recipes.md` when you need command templates.
67
+
68
+ 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.
69
+
70
+ ## Failure Handling
71
+
72
+ | Situation | Response |
73
+ |---|---|
74
+ | Missing default model | Ask the user to configure the capability in Settings / Agent Defaults |
75
+ | Provider disconnected or missing key | Ask the user to check provider connection and API key |
76
+ | Model disabled | Ask the user to enable the model in the provider model list |
77
+ | Unsupported capability | Use a model that supports the capability, or ask the user to configure one |
78
+ | Output file missing | Do not report success; explain the CLI JSON error and stderr |
79
+ | Video timeout | Report timeout details and operation id when available |
80
+
81
+ ## Final Response Style
82
+
83
+ When reporting back:
84
+
85
+ - Say which capability was used.
86
+ - Give the output file path.
87
+ - If it failed, state the failure point and the next user action.
88
+ - Keep the response short unless the user asks for detailed debugging.
@@ -0,0 +1,4 @@
1
+ interface:
2
+ display_name: "Dim Modality"
3
+ short_description: "Use DimCode multimodal CLI"
4
+ default_prompt: "Use $dim-modality to create or process media with the DimCode multimodal CLI."
@@ -0,0 +1,137 @@
1
+ # CLI Recipes
2
+
3
+ Use `--json` for all agent/script calls. Prefer output paths inside the current workspace.
4
+
5
+ ## Check Configuration
6
+
7
+ ```bash
8
+ dim modality list --json
9
+ dim modality get image.generate --json
10
+ ```
11
+
12
+ ## Images
13
+
14
+ Generate an image:
15
+
16
+ ```bash
17
+ mkdir -p ./artifacts
18
+ dim image generate --prompt "A clean product poster on a white background with soft natural lighting" --out ./artifacts/image.png --json
19
+ ```
20
+
21
+ Edit an image:
22
+
23
+ ```bash
24
+ mkdir -p ./artifacts
25
+ dim image edit --image ./input.png --prompt "Keep the subject, change the lighting to golden hour" --out ./artifacts/edited.png --json
26
+ ```
27
+
28
+ Multi-image edit:
29
+
30
+ ```bash
31
+ 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
32
+ ```
33
+
34
+ Enhance an image:
35
+
36
+ ```bash
37
+ dim image enhance --image ./input.png --operation upscale --out ./artifacts/upscaled.png --json
38
+ ```
39
+
40
+ ## Video
41
+
42
+ Text-to-video:
43
+
44
+ ```bash
45
+ mkdir -p ./artifacts
46
+ 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
47
+ ```
48
+
49
+ Image-to-video:
50
+
51
+ ```bash
52
+ dim video generate --prompt "Make the person turn naturally toward the camera" --image ./input.png --duration 5 --out ./artifacts/video.mp4 --json --timeout 1800000
53
+ ```
54
+
55
+ Edit a video:
56
+
57
+ ```bash
58
+ dim video edit --video ./input.mp4 --operation add-audio --audio ./voice.mp3 --out ./artifacts/edited.mp4 --json
59
+ ```
60
+
61
+ Enhance a video:
62
+
63
+ ```bash
64
+ dim video enhance --video ./input.mp4 --operation stabilize --out ./artifacts/stable.mp4 --json
65
+ ```
66
+
67
+ ## Speech
68
+
69
+ Text-to-speech:
70
+
71
+ ```bash
72
+ mkdir -p ./artifacts
73
+ dim speech synthesize --text "Hello, this is a speech generation test." --out ./artifacts/speech.mp3 --json
74
+ ```
75
+
76
+ Specify a voice:
77
+
78
+ ```bash
79
+ dim speech synthesize --text "Hello, this is a speech generation test." --voice "cosyvoice2:anna" --out ./artifacts/speech.mp3 --json
80
+ ```
81
+
82
+ Transcribe audio:
83
+
84
+ ```bash
85
+ dim speech transcribe --audio ./meeting.mp3 --out ./artifacts/transcript.txt --json
86
+ ```
87
+
88
+ ## Music And Audio
89
+
90
+ Generate music:
91
+
92
+ ```bash
93
+ mkdir -p ./artifacts
94
+ dim music generate --prompt "Warm lo-fi piano for a podcast intro" --title "warm intro" --out ./artifacts/music.mp3 --json
95
+ ```
96
+
97
+ Generate a sound effect:
98
+
99
+ ```bash
100
+ dim audio generate --operation sound-effect --prompt "Raindrops tapping on a window" --duration 8 --out ./artifacts/rain.mp3 --json
101
+ ```
102
+
103
+ Extract or generate audio from video:
104
+
105
+ ```bash
106
+ dim audio generate --operation video-to-audio --video ./input.mp4 --out ./artifacts/audio.mp3 --json
107
+ ```
108
+
109
+ ## 3D Asset And Motion
110
+
111
+ ```bash
112
+ dim asset generate --prompt "low poly robot game asset" --out ./artifacts/robot.glb --json
113
+ dim motion generate --prompt "walk cycle for a friendly robot" --out ./artifacts/walk.fbx --json
114
+ ```
115
+
116
+ ## Temporary Provider Or Model Override
117
+
118
+ Use this only when the user explicitly asks for a specific provider or model:
119
+
120
+ ```bash
121
+ dim image generate --provider siliconflow --model Qwen/Qwen-Image --prompt "..." --out ./artifacts/image.png --json
122
+ ```
123
+
124
+ ## Provider-Specific Options
125
+
126
+ Write a JSON object file, then pass it with `--options`:
127
+
128
+ ```json
129
+ {
130
+ "seed": 1234,
131
+ "negativePrompt": "blurry, low quality"
132
+ }
133
+ ```
134
+
135
+ ```bash
136
+ dim image generate --prompt "..." --options ./options.json --out ./artifacts/image.png --json
137
+ ```
@@ -0,0 +1,56 @@
1
+ # Model Notes
2
+
3
+ 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/`.
4
+
5
+ ## Current Entries
6
+
7
+ | Provider / model | Capability | Note |
8
+ |---|---|---|
9
+ | CosyVoice2 | `speech.synthesize` | `--voice` uses `model-name:voice-name` |
10
+ | Video generation models | `video.generate` | Generation can take a long time; use a generous `--timeout` and keep operation id / JSON errors |
11
+ | Image edit models | `image.edit` | Requires `--image` and `--prompt`; pass repeated `--image` flags for multi-image edit |
12
+ | ASR models | `speech.transcribe` | Input is `--audio`; output should be a text file |
13
+
14
+ ## CosyVoice2 Voices
15
+
16
+ CosyVoice2 `--voice` format:
17
+
18
+ ```text
19
+ model-name:voice-name
20
+ ```
21
+
22
+ Available voice names:
23
+
24
+ ```text
25
+ alex
26
+ benjamin
27
+ charles
28
+ david
29
+ anna
30
+ bella
31
+ claire
32
+ diana
33
+ ```
34
+
35
+ `alex`, `benjamin`, `charles`, and `david` are male voices. `anna`, `bella`, `claire`, and `diana` are female voices.
36
+
37
+ Example:
38
+
39
+ ```bash
40
+ dim speech synthesize --text "Hello, this is a test." --voice "CosyVoice2:anna" --out ./artifacts/speech.mp3 --json
41
+ ```
42
+
43
+ If the configured model id is not exactly `CosyVoice2`, replace the part before `:` with the configured model name or model id.
44
+
45
+ ## Adding Future Model Notes
46
+
47
+ When adding a model note:
48
+
49
+ 1. Create a file under `references/models/` named after the provider and model.
50
+ 2. Add a row to the table above.
51
+ 3. Include:
52
+ - Supported capability.
53
+ - Required inputs.
54
+ - Recommended output format.
55
+ - Known slow paths or failure modes.
56
+ - One runnable `dim ... --json` example.
@@ -0,0 +1,148 @@
1
+ ---
2
+ name: plugin-creator
3
+ 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`.
4
+ metadata:
5
+ short-description: Create or scaffold a Dim plugin
6
+ ---
7
+
8
+ # Plugin Creator
9
+
10
+ Scaffold a Dim plugin: a directory with a `.claude-plugin/plugin.json` manifest plus
11
+ optional `skills/`, a `hooks` file, and an `.mcp.json`. Dim discovers plugins from
12
+ `~/.agents/plugins/<name>/`: each plugin folder is self-describing, so placing one under
13
+ `~/.agents/plugins` makes its skills, hooks, and MCP servers load on the next session.
14
+
15
+ ## Quick Start
16
+
17
+ 1. Scaffold the plugin. By default it is created live under `~/.agents/plugins/<name>/`,
18
+ so a new session picks it up immediately.
19
+
20
+ ```bash
21
+ # Names are normalized to lower-case hyphen-case (<= 64 chars). The folder name and the
22
+ # manifest `name` are always identical. Run from the skill root (the directory holding
23
+ # this SKILL.md).
24
+ python3 scripts/create_basic_plugin.py <plugin-name>
25
+ ```
26
+
27
+ 2. Edit `<plugin-path>/.claude-plugin/plugin.json` when the request gives specific
28
+ metadata (description, display name, version). The scaffold starts with valid defaults
29
+ and must not contain `[TODO: ...]` placeholders.
30
+
31
+ 3. Add the components the plugin needs:
32
+
33
+ ```bash
34
+ python3 scripts/create_basic_plugin.py <plugin-name> \
35
+ --with-skills --with-hooks --with-mcp --with-assets
36
+ ```
37
+
38
+ - `--with-skills` creates `skills/<plugin-name>/SKILL.md`, a working starter skill, and
39
+ sets manifest `"skills": "./skills/"`.
40
+ - `--with-hooks` creates `hooks.json` and sets manifest `"hooks": "./hooks.json"`.
41
+ - `--with-mcp` creates `.mcp.json` and sets manifest `"mcpServers": "./.mcp.json"`.
42
+ - `--with-assets` creates an `assets/` folder for icons/logos.
43
+
44
+ 4. Build a plugin for git distribution instead of local dev with `--path`:
45
+
46
+ ```bash
47
+ # Create under a repo or scratch directory you will push to git.
48
+ python3 scripts/create_basic_plugin.py <plugin-name> --path <parent-directory>
49
+ ```
50
+
51
+ 5. Validate before handing back or sharing:
52
+
53
+ ```bash
54
+ python3 scripts/validate_plugin.py <plugin-path>
55
+ ```
56
+
57
+ 6. **Tell the user to start a new session/thread.** Dim loads a plugin's skills, hooks, and
58
+ MCP servers when a session starts, so a plugin created or changed in the *current* session
59
+ only becomes usable in the *next* one. If you scaffolded the plugin mid-conversation, the
60
+ skill will not be callable here yet — the user must open a new session to use it.
61
+
62
+ ## What this skill creates
63
+
64
+ - Plugin root at `<parent-directory>/<plugin-name>/` (default parent `~/.agents/plugins`).
65
+ - Always `<plugin-root>/.claude-plugin/plugin.json` with the manifest shape Dim reads.
66
+ - `<plugin-name>` is normalized:
67
+ - `My Plugin` → `my-plugin`
68
+ - `My--Plugin` → `my-plugin`
69
+ - underscores, spaces, and punctuation become `-`; consecutive hyphens collapse; result is
70
+ lower-case.
71
+ - Optional, created only when requested:
72
+ - `skills/<plugin-name>/SKILL.md` (a starter skill)
73
+ - `hooks.json`
74
+ - `.mcp.json`
75
+ - `assets/`
76
+
77
+ ## Manifest
78
+
79
+ Dim reads a small set of fields from `plugin.json`. The scaffold writes valid defaults for
80
+ all of them:
81
+
82
+ ```json
83
+ {
84
+ "name": "my-plugin",
85
+ "version": "0.1.0",
86
+ "description": "My Plugin plugin",
87
+ "interface": {
88
+ "displayName": "My Plugin",
89
+ "shortDescription": "Use My Plugin in Dim."
90
+ },
91
+ "skills": "./skills/",
92
+ "hooks": "./hooks.json",
93
+ "mcpServers": "./.mcp.json"
94
+ }
95
+ ```
96
+
97
+ Component paths (`skills`, `hooks`, `mcpServers`) appear only when their files exist. See
98
+ `references/plugin-json-spec.md` for the full field guide and the exact sample.
99
+
100
+ ## Components
101
+
102
+ - **Skills** — directories under `skills/<name>/` each containing a `SKILL.md` with valid
103
+ frontmatter (`name`, `description`). Dim discovers them automatically and loads them into
104
+ the system prompt.
105
+ - **Hooks** — a JSON file (manifest `hooks` field) mapping lifecycle events to commands. Use
106
+ `${CLAUDE_PLUGIN_ROOT}` in command paths; Dim substitutes the plugin's absolute root at
107
+ runtime, e.g. `node "${CLAUDE_PLUGIN_ROOT}/hooks/on-start.js"`.
108
+ - **MCP** — an `.mcp.json` with an `mcpServers` object. Server ids are namespaced as
109
+ `plugin:<plugin-name>/<serverId>`. Command paths support the same `${CLAUDE_PLUGIN_ROOT}`
110
+ substitution.
111
+
112
+ ## Required behavior
113
+
114
+ - The outer folder name and `plugin.json` `"name"` are always the same normalized name.
115
+ - Keep `.claude-plugin/plugin.json` present; never remove the required manifest.
116
+ - Do not leave `[TODO: ...]` placeholders in the manifest.
117
+ - Add a component path to the manifest only when its file actually exists. Keep `skills`,
118
+ `hooks`, and `mcpServers` out of the manifest when their companion files were not created.
119
+ - Use `--force` only when overwriting an existing plugin path is intentional.
120
+ - After creating or modifying a plugin, end by telling the user to start a new session/thread.
121
+ The current session loaded its skills, hooks, and MCP servers at startup and will not pick up
122
+ the new ones until a fresh session — do not claim the new skill is usable in this session.
123
+
124
+ ## Install and update
125
+
126
+ For the full local-development loop, the git-sharing path, and component toggling, see
127
+ `references/installing-and-updating.md`. In short:
128
+
129
+ - **Local dev** — scaffold into `~/.agents/plugins/<name>/`, then start a new session. Dim
130
+ re-scans the directory and loads the plugin's skills, hooks, and MCP servers. Editing files
131
+ in place and starting another session picks up the changes.
132
+ - **Share** — push the plugin directory to a git repo, then in the Dim desktop app open
133
+ Plugins → Add plugin and paste the git URL (`owner/repo` or a full URL). Dim reads the
134
+ manifest and installs the plugin into `~/.agents/plugins`.
135
+
136
+ ## Validation
137
+
138
+ After editing this `SKILL.md`, run:
139
+
140
+ ```bash
141
+ python3 ../skill-creator/scripts/quick_validate.py .
142
+ ```
143
+
144
+ Before handing back a generated plugin, run:
145
+
146
+ ```bash
147
+ python3 scripts/validate_plugin.py <plugin-path>
148
+ ```
@@ -0,0 +1,59 @@
1
+ # Installing and updating Dim plugins
2
+
3
+ A Dim plugin is a self-describing directory under `~/.agents/plugins/<name>/`. Disk is the
4
+ single source of truth: Dim scans that directory, reads each `.claude-plugin/plugin.json`, and
5
+ loads the plugin's components. Installing a plugin means placing its folder there; removing the
6
+ folder removes the plugin.
7
+
8
+ ## Local development
9
+
10
+ 1. Scaffold the plugin directly under `~/.agents/plugins/<name>/` (the default of
11
+ `scripts/create_basic_plugin.py`):
12
+
13
+ ```bash
14
+ python3 scripts/create_basic_plugin.py my-plugin --with-skills
15
+ ```
16
+
17
+ 2. Edit the manifest and component files.
18
+
19
+ 3. Start a **new session**. Dim re-scans `~/.agents/plugins`, and the plugin's skills, hooks,
20
+ and MCP servers load. A new session is the boundary at which changes are picked up.
21
+
22
+ To iterate, edit files in place and start another session. Removing the plugin directory
23
+ removes the plugin.
24
+
25
+ ## Activation by component
26
+
27
+ - **Skills** — discovered from `~/.agents/plugins/*/skills/` and loaded into the system prompt
28
+ automatically. They also appear in the desktop Skills list.
29
+ - **Hooks** — read from the file named by the manifest `hooks` field and registered on session
30
+ start. Command paths use `${CLAUDE_PLUGIN_ROOT}`, which Dim replaces with the plugin's
31
+ absolute root.
32
+ - **MCP servers** — read from `.mcp.json` (or the inline `mcpServers` object) and merged into
33
+ the MCP runtime. Server ids are namespaced `plugin:<plugin-name>/<serverId>`.
34
+
35
+ ## Toggling components
36
+
37
+ In the Dim desktop app, open Plugins → select the plugin to see its detail page. Each skill,
38
+ hook, and MCP server has its own on/off switch, plus a master switch to enable or disable all
39
+ components at once.
40
+
41
+ ## Sharing via git
42
+
43
+ To distribute a plugin to other users:
44
+
45
+ 1. Build the plugin in a directory you will push to git (use `--path`):
46
+
47
+ ```bash
48
+ python3 scripts/create_basic_plugin.py my-plugin --path ./plugins --with-skills
49
+ ```
50
+
51
+ 2. Commit and push the plugin directory (including `.claude-plugin/plugin.json`) to a git repo.
52
+
53
+ 3. In the Dim desktop app, open Plugins → Add plugin and paste the git source — either
54
+ `owner/repo` or a full git URL, with an optional ref (branch/tag). Dim clones the repo,
55
+ reads the manifest, and installs the plugin into `~/.agents/plugins/<name>/`, recording the
56
+ source in a `dim-install.json` provenance file.
57
+
58
+ Updating a git-installed plugin is done from the same Add-plugin flow (reinstall from the
59
+ source) or by editing the on-disk copy under `~/.agents/plugins` for local tweaks.
@@ -0,0 +1,120 @@
1
+ # Plugin manifest spec (`.claude-plugin/plugin.json`)
2
+
3
+ Dim recognizes a plugin by the presence of `.claude-plugin/plugin.json` at the plugin root.
4
+ It parses only the fields below; any other field is ignored. Keep the manifest minimal and
5
+ valid.
6
+
7
+ ```json
8
+ {
9
+ "name": "my-plugin",
10
+ "version": "0.1.0",
11
+ "description": "Brief plugin description",
12
+ "interface": {
13
+ "displayName": "My Plugin",
14
+ "shortDescription": "Short subtitle shown in the plugin list",
15
+ "logo": "./assets/logo.png"
16
+ },
17
+ "skills": "./skills/",
18
+ "hooks": "./hooks.json",
19
+ "mcpServers": "./.mcp.json"
20
+ }
21
+ ```
22
+
23
+ ## Field guide
24
+
25
+ ### Top-level fields
26
+
27
+ - `name` (`string`, required): Plugin identifier (lower-case hyphen-case, no spaces). Matches
28
+ the plugin folder name. Sanitized to `[A-Za-z0-9._-]`, max 128 chars.
29
+ - `version` (`string`, optional): Strict semver, e.g. `0.1.0`.
30
+ - `description` (`string`, optional): Short purpose summary. Falls back to
31
+ `interface.shortDescription` in the UI when absent.
32
+ - `interface` (`object`, optional): Presentation metadata (see below).
33
+ - `skills` (`string`, optional): Relative path to the skills directory, normally `./skills/`.
34
+ - `hooks` (`string`, optional): Relative path to the hooks config file, e.g. `./hooks.json`.
35
+ - `mcpServers` (`string`, optional): Relative path to an `.mcp.json` file, e.g. `./.mcp.json`.
36
+ An inline object is not supported — the runtime loads only a path string.
37
+
38
+ ### `interface` fields
39
+
40
+ - `displayName` (`string`): User-facing title. Defaults to `name` when absent.
41
+ - `shortDescription` (`string`): Brief subtitle for compact views.
42
+ - `logo` (`string`): Relative path to a logo asset inside the plugin (for example
43
+ `./assets/logo.png`).
44
+
45
+ ### Path conventions
46
+
47
+ - Component path values are relative to the plugin root and begin with `./`.
48
+ - A component path appears in the manifest only when its file/directory exists. Do not declare
49
+ `skills`, `hooks`, or `mcpServers` when their companion files were not created.
50
+
51
+ ## Components
52
+
53
+ ### Skills
54
+
55
+ Skills live under `skills/<skill-name>/SKILL.md`. Each `SKILL.md` starts with YAML
56
+ frontmatter:
57
+
58
+ ```markdown
59
+ ---
60
+ name: my-skill
61
+ description: What it does and when to use it.
62
+ ---
63
+
64
+ # My Skill
65
+
66
+ Instructions for the assistant.
67
+ ```
68
+
69
+ Dim discovers plugin skills automatically from `~/.agents/plugins/*/skills/` and loads them
70
+ into the system prompt.
71
+
72
+ ### Hooks
73
+
74
+ `hooks` points to a JSON file mapping lifecycle events to commands. Use
75
+ `${CLAUDE_PLUGIN_ROOT}` in command paths — Dim substitutes the plugin's absolute root at
76
+ runtime:
77
+
78
+ ```json
79
+ {
80
+ "hooks": {
81
+ "SessionStart": [
82
+ {
83
+ "matcher": "startup|resume|clear|compact",
84
+ "hooks": [
85
+ { "type": "command", "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/on-start.js\"" }
86
+ ]
87
+ }
88
+ ]
89
+ }
90
+ }
91
+ ```
92
+
93
+ ### MCP servers
94
+
95
+ `mcpServers` points to an `.mcp.json` file. Server ids are namespaced as
96
+ `plugin:<plugin-name>/<serverId>`. Command paths support the same `${CLAUDE_PLUGIN_ROOT}`
97
+ substitution. The referenced `.mcp.json` has this shape:
98
+
99
+ ```json
100
+ {
101
+ "mcpServers": {
102
+ "example": {
103
+ "type": "stdio",
104
+ "command": "node",
105
+ "args": ["${CLAUDE_PLUGIN_ROOT}/mcp/server.js"]
106
+ }
107
+ }
108
+ }
109
+ ```
110
+
111
+ ## Validation notes
112
+
113
+ - `name` must be a non-empty string; the folder name must match it.
114
+ - `version`, when present, must be strict semver.
115
+ - `interface.logo` and any declared component path must point to real files/directories inside
116
+ the plugin.
117
+ - Sub-skill `SKILL.md` files must have non-empty `name` and `description` frontmatter.
118
+ - The manifest must not contain `[TODO: ...]` placeholders.
119
+
120
+ Run `scripts/validate_plugin.py <plugin-path>` before handing back a generated plugin.
@@ -0,0 +1,189 @@
1
+ #!/usr/bin/env python3
2
+ """Scaffold a Dim plugin directory with a `.claude-plugin/plugin.json` manifest.
3
+
4
+ Dim discovers plugins from ``~/.agents/plugins/<name>/``. The default parent is
5
+ ``~/.agents/plugins`` so a scaffolded plugin is live on the next session. Pass ``--path`` to
6
+ build a plugin elsewhere (e.g. a git repo to share).
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import argparse
12
+ import json
13
+ import re
14
+ from pathlib import Path
15
+ from typing import Any
16
+
17
+
18
+ MAX_PLUGIN_NAME_LENGTH = 64
19
+ DEFAULT_PLUGIN_PARENT = Path.home() / ".agents" / "plugins"
20
+ MANIFEST_DIR = ".claude-plugin"
21
+ MANIFEST_FILE = "plugin.json"
22
+
23
+
24
+ def normalize_plugin_name(plugin_name: str) -> str:
25
+ """Normalize a plugin name to lowercase hyphen-case."""
26
+ normalized = plugin_name.strip().lower()
27
+ normalized = re.sub(r"[^a-z0-9]+", "-", normalized)
28
+ normalized = normalized.strip("-")
29
+ normalized = re.sub(r"-{2,}", "-", normalized)
30
+ return normalized
31
+
32
+
33
+ def validate_plugin_name(plugin_name: str) -> None:
34
+ if not plugin_name:
35
+ raise ValueError("Plugin name must include at least one letter or digit.")
36
+ if len(plugin_name) > MAX_PLUGIN_NAME_LENGTH:
37
+ raise ValueError(
38
+ f"Plugin name '{plugin_name}' is too long ({len(plugin_name)} characters). "
39
+ f"Maximum is {MAX_PLUGIN_NAME_LENGTH} characters."
40
+ )
41
+
42
+
43
+ def display_name_from_plugin_name(plugin_name: str) -> str:
44
+ return " ".join(part.capitalize() for part in re.split(r"[-_]+", plugin_name))
45
+
46
+
47
+ def build_plugin_json(
48
+ plugin_name: str,
49
+ *,
50
+ with_skills: bool,
51
+ with_hooks: bool,
52
+ with_mcp: bool,
53
+ ) -> dict[str, Any]:
54
+ """Build the manifest Dim reads. Only the fields Dim parses are written."""
55
+ display_name = display_name_from_plugin_name(plugin_name)
56
+ payload: dict[str, Any] = {
57
+ "name": plugin_name,
58
+ "version": "0.1.0",
59
+ "description": f"{display_name} plugin",
60
+ "interface": {
61
+ "displayName": display_name,
62
+ "shortDescription": f"Use {display_name} in Dim.",
63
+ },
64
+ }
65
+ # Component paths are added only when their companion files are created, so the manifest
66
+ # never points at something that does not exist.
67
+ if with_skills:
68
+ payload["skills"] = "./skills/"
69
+ if with_hooks:
70
+ payload["hooks"] = "./hooks.json"
71
+ if with_mcp:
72
+ payload["mcpServers"] = "./.mcp.json"
73
+ return payload
74
+
75
+
76
+ def build_starter_skill(plugin_name: str) -> str:
77
+ """A minimal, valid starter skill so the plugin contributes something usable."""
78
+ display_name = display_name_from_plugin_name(plugin_name)
79
+ return (
80
+ "---\n"
81
+ f"name: {plugin_name}\n"
82
+ f"description: {display_name} starter skill. Replace this description with the real "
83
+ f"trigger so Dim knows when to use {display_name}.\n"
84
+ "---\n\n"
85
+ f"# {display_name}\n\n"
86
+ "Describe what this skill does and the steps the assistant should follow when it is "
87
+ "invoked. Keep instructions concrete and task-focused.\n"
88
+ )
89
+
90
+
91
+ def write_json(path: Path, data: dict, force: bool) -> None:
92
+ if path.exists() and not force:
93
+ raise FileExistsError(f"{path} already exists. Use --force to overwrite.")
94
+ path.parent.mkdir(parents=True, exist_ok=True)
95
+ with path.open("w", encoding="utf-8") as handle:
96
+ json.dump(data, handle, indent=2)
97
+ handle.write("\n")
98
+
99
+
100
+ def write_text(path: Path, text: str, force: bool) -> None:
101
+ if path.exists() and not force:
102
+ return
103
+ path.parent.mkdir(parents=True, exist_ok=True)
104
+ path.write_text(text, encoding="utf-8")
105
+
106
+
107
+ def parse_args() -> argparse.Namespace:
108
+ parser = argparse.ArgumentParser(
109
+ description="Create a Dim plugin skeleton with a validation-ready plugin.json."
110
+ )
111
+ parser.add_argument("plugin_name")
112
+ parser.add_argument(
113
+ "--path",
114
+ default=str(DEFAULT_PLUGIN_PARENT),
115
+ help=(
116
+ "Parent directory for the plugin (defaults to ~/.agents/plugins, where Dim "
117
+ "discovers plugins). Pass another directory to build a plugin for git sharing."
118
+ ),
119
+ )
120
+ parser.add_argument(
121
+ "--with-skills",
122
+ action="store_true",
123
+ help="Create skills/<name>/SKILL.md starter skill and set manifest `skills`.",
124
+ )
125
+ parser.add_argument(
126
+ "--with-hooks",
127
+ action="store_true",
128
+ help="Create hooks.json and set manifest `hooks`.",
129
+ )
130
+ parser.add_argument(
131
+ "--with-mcp",
132
+ action="store_true",
133
+ help="Create .mcp.json and set manifest `mcpServers`.",
134
+ )
135
+ parser.add_argument(
136
+ "--with-assets",
137
+ action="store_true",
138
+ help="Create an assets/ directory for icons and logos.",
139
+ )
140
+ parser.add_argument("--force", action="store_true", help="Overwrite existing files")
141
+ return parser.parse_args()
142
+
143
+
144
+ def main() -> None:
145
+ args = parse_args()
146
+ raw_plugin_name = args.plugin_name
147
+ plugin_name = normalize_plugin_name(raw_plugin_name)
148
+ if plugin_name != raw_plugin_name:
149
+ print(f"Note: Normalized plugin name from '{raw_plugin_name}' to '{plugin_name}'.")
150
+ validate_plugin_name(plugin_name)
151
+
152
+ plugin_root = Path(args.path).expanduser().resolve() / plugin_name
153
+ plugin_root.mkdir(parents=True, exist_ok=True)
154
+
155
+ manifest_path = plugin_root / MANIFEST_DIR / MANIFEST_FILE
156
+ write_json(
157
+ manifest_path,
158
+ build_plugin_json(
159
+ plugin_name,
160
+ with_skills=args.with_skills,
161
+ with_hooks=args.with_hooks,
162
+ with_mcp=args.with_mcp,
163
+ ),
164
+ args.force,
165
+ )
166
+
167
+ if args.with_skills:
168
+ write_text(
169
+ plugin_root / "skills" / plugin_name / "SKILL.md",
170
+ build_starter_skill(plugin_name),
171
+ args.force,
172
+ )
173
+
174
+ if args.with_hooks:
175
+ # Empty hooks map; fill events with commands that use ${CLAUDE_PLUGIN_ROOT} paths.
176
+ write_json(plugin_root / "hooks.json", {"hooks": {}}, args.force)
177
+
178
+ if args.with_mcp:
179
+ write_json(plugin_root / ".mcp.json", {"mcpServers": {}}, args.force)
180
+
181
+ if args.with_assets:
182
+ (plugin_root / "assets").mkdir(parents=True, exist_ok=True)
183
+
184
+ print(f"Created plugin scaffold: {plugin_root}")
185
+ print(f"plugin manifest: {manifest_path}")
186
+
187
+
188
+ if __name__ == "__main__":
189
+ main()
@@ -0,0 +1,269 @@
1
+ #!/usr/bin/env python3
2
+ """Validate a generated Dim plugin against the manifest contract Dim actually reads.
3
+
4
+ Dim recognizes a plugin by ``.claude-plugin/plugin.json`` and parses a small set of fields:
5
+ ``name`` (required), ``version``, ``description``, ``interface.{displayName,shortDescription,
6
+ logo}``, and the component paths ``skills`` / ``hooks`` / ``mcpServers``. Unknown fields are
7
+ ignored by Dim, so they are not rejected here. This validator also rejects leftover
8
+ ``[TODO: ...]`` placeholders and checks that declared component paths exist.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import argparse
14
+ import json
15
+ import re
16
+ from pathlib import Path, PurePosixPath
17
+ from typing import Any
18
+
19
+
20
+ TODO_MARKER = "[TODO:"
21
+ SEMVER_RE = re.compile(
22
+ r"^(0|[1-9]\d*)\."
23
+ r"(0|[1-9]\d*)\."
24
+ r"(0|[1-9]\d*)"
25
+ r"(?:-(?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*)(?:\."
26
+ r"(?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*))*)?"
27
+ r"(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$"
28
+ )
29
+ MANIFEST_REL = ".claude-plugin/plugin.json"
30
+
31
+
32
+ def parse_args() -> argparse.Namespace:
33
+ parser = argparse.ArgumentParser(description="Validate a local Dim plugin.")
34
+ parser.add_argument("plugin_path", help="Path to the plugin root directory")
35
+ return parser.parse_args()
36
+
37
+
38
+ def main() -> None:
39
+ args = parse_args()
40
+ plugin_root = Path(args.plugin_path).expanduser().resolve()
41
+ errors = validate_plugin(plugin_root)
42
+ if errors:
43
+ print("Plugin validation failed:")
44
+ for error in errors:
45
+ print(f"- {error}")
46
+ raise SystemExit(1)
47
+ print(f"Plugin validation passed: {plugin_root}")
48
+
49
+
50
+ def validate_plugin(plugin_root: Path) -> list[str]:
51
+ errors: list[str] = []
52
+ manifest_path = plugin_root / ".claude-plugin" / "plugin.json"
53
+ manifest = load_json_object(manifest_path, errors)
54
+ if manifest is None:
55
+ return errors
56
+
57
+ reject_todo_markers(manifest, "$", errors)
58
+ validate_manifest(plugin_root, manifest, errors)
59
+ return errors
60
+
61
+
62
+ def load_json_object(path: Path, errors: list[str]) -> dict[str, Any] | None:
63
+ if not path.is_file():
64
+ errors.append(f"missing `{MANIFEST_REL}`")
65
+ return None
66
+ try:
67
+ payload = json.loads(path.read_text(encoding="utf-8"))
68
+ except OSError:
69
+ errors.append(f"unable to read `{MANIFEST_REL}`")
70
+ return None
71
+ except json.JSONDecodeError:
72
+ errors.append(f"`{MANIFEST_REL}` must be valid JSON")
73
+ return None
74
+ if not isinstance(payload, dict):
75
+ errors.append(f"`{MANIFEST_REL}` must contain a JSON object")
76
+ return None
77
+ return payload
78
+
79
+
80
+ def reject_todo_markers(value: Any, path: str, errors: list[str]) -> None:
81
+ if isinstance(value, str):
82
+ if TODO_MARKER in value:
83
+ errors.append(f"{path} still contains a `[TODO: ...]` placeholder")
84
+ return
85
+ if isinstance(value, list):
86
+ for index, item in enumerate(value):
87
+ reject_todo_markers(item, f"{path}[{index}]", errors)
88
+ return
89
+ if isinstance(value, dict):
90
+ for key, item in value.items():
91
+ reject_todo_markers(item, f"{path}.{key}", errors)
92
+
93
+
94
+ def validate_manifest(plugin_root: Path, manifest: dict[str, Any], errors: list[str]) -> None:
95
+ require_non_empty_string(manifest, "name", errors)
96
+
97
+ version = manifest.get("version")
98
+ if version is not None:
99
+ if not isinstance(version, str) or SEMVER_RE.fullmatch(version) is None:
100
+ errors.append("plugin.json field `version` must be strict semver")
101
+
102
+ validate_optional_non_empty_string(manifest, "description", errors)
103
+
104
+ interface = manifest.get("interface")
105
+ if interface is not None:
106
+ if not isinstance(interface, dict):
107
+ errors.append("plugin.json field `interface` must be an object")
108
+ else:
109
+ validate_optional_non_empty_string(interface, "displayName", errors, prefix="interface")
110
+ validate_optional_non_empty_string(interface, "shortDescription", errors, prefix="interface")
111
+ validate_optional_asset_path(plugin_root, interface, "logo", errors)
112
+
113
+ validate_component_path(plugin_root, manifest, "skills", errors)
114
+ validate_component_path(plugin_root, manifest, "hooks", errors)
115
+ validate_mcp_servers(plugin_root, manifest, errors)
116
+ validate_skill_manifests(plugin_root, errors)
117
+
118
+
119
+ def require_non_empty_string(
120
+ payload: dict[str, Any],
121
+ key: str,
122
+ errors: list[str],
123
+ *,
124
+ prefix: str | None = None,
125
+ ) -> None:
126
+ value = payload.get(key)
127
+ field = f"{prefix}.{key}" if prefix is not None else key
128
+ if not isinstance(value, str) or not value.strip():
129
+ errors.append(f"plugin.json field `{field}` must be a non-empty string")
130
+
131
+
132
+ def validate_optional_non_empty_string(
133
+ payload: dict[str, Any],
134
+ key: str,
135
+ errors: list[str],
136
+ *,
137
+ prefix: str | None = None,
138
+ ) -> None:
139
+ value = payload.get(key)
140
+ if value is None:
141
+ return
142
+ field = f"{prefix}.{key}" if prefix is not None else key
143
+ if not isinstance(value, str) or not value.strip():
144
+ errors.append(f"plugin.json field `{field}` must be a non-empty string")
145
+
146
+
147
+ def validate_component_path(
148
+ plugin_root: Path,
149
+ manifest: dict[str, Any],
150
+ key: str,
151
+ errors: list[str],
152
+ ) -> None:
153
+ """`skills` resolves to a directory; `hooks` resolves to a file. Both must exist."""
154
+ value = manifest.get(key)
155
+ if value is None:
156
+ return
157
+ if not isinstance(value, str) or not value.strip():
158
+ errors.append(f"plugin.json field `{key}` must be a non-empty relative path")
159
+ return
160
+ resolved = resolve_inside(plugin_root, value, key, errors)
161
+ if resolved is None:
162
+ return
163
+ if key == "skills" and not resolved.is_dir():
164
+ errors.append(f"plugin.json field `skills` points to a missing directory")
165
+ if key == "hooks" and not resolved.is_file():
166
+ errors.append(f"plugin.json field `hooks` points to a missing file")
167
+
168
+
169
+ def validate_mcp_servers(plugin_root: Path, manifest: dict[str, Any], errors: list[str]) -> None:
170
+ value = manifest.get("mcpServers")
171
+ if value is None:
172
+ return
173
+ # Runtime only loads `mcpServers` as a path to an `.mcp.json` file; an inline
174
+ # object is silently ignored by the loader, so reject it here too.
175
+ if isinstance(value, str) and value.strip():
176
+ resolved = resolve_inside(plugin_root, value, "mcpServers", errors)
177
+ if resolved is not None and not resolved.is_file():
178
+ errors.append("plugin.json field `mcpServers` points to a missing file")
179
+ return
180
+ errors.append("plugin.json field `mcpServers` must be a relative path string to an `.mcp.json` file")
181
+
182
+
183
+ def validate_optional_asset_path(
184
+ plugin_root: Path,
185
+ payload: dict[str, Any],
186
+ key: str,
187
+ errors: list[str],
188
+ ) -> None:
189
+ value = payload.get(key)
190
+ if value is None:
191
+ return
192
+ if not isinstance(value, str) or not value.strip():
193
+ errors.append(f"plugin.json field `interface.{key}` must be a non-empty relative path")
194
+ return
195
+ resolved = resolve_inside(plugin_root, value, f"interface.{key}", errors)
196
+ if resolved is not None and not resolved.is_file():
197
+ errors.append(f"plugin.json field `interface.{key}` points to a missing file")
198
+
199
+
200
+ def resolve_inside(
201
+ plugin_root: Path,
202
+ raw_path: str,
203
+ field: str,
204
+ errors: list[str],
205
+ ) -> Path | None:
206
+ """Resolve a relative path and confirm it stays inside the plugin root."""
207
+ candidate = PurePosixPath(raw_path.replace("\\", "/"))
208
+ if candidate.is_absolute() or ".." in candidate.parts:
209
+ errors.append(f"plugin.json field `{field}` must stay inside the plugin directory")
210
+ return None
211
+ resolved = (plugin_root / candidate.as_posix()).resolve()
212
+ if not resolved.is_relative_to(plugin_root.resolve()):
213
+ errors.append(f"plugin.json field `{field}` must stay inside the plugin directory")
214
+ return None
215
+ return resolved
216
+
217
+
218
+ def validate_skill_manifests(plugin_root: Path, errors: list[str]) -> None:
219
+ skills_root = plugin_root / "skills"
220
+ if not skills_root.is_dir():
221
+ return
222
+ for skill_root in sorted(skills_root.iterdir(), key=lambda path: path.name):
223
+ if skill_root.name.startswith(".") or not skill_root.is_dir():
224
+ continue
225
+ validate_skill_manifest(skill_root, errors)
226
+
227
+
228
+ def validate_skill_manifest(skill_root: Path, errors: list[str]) -> None:
229
+ skill_md_path = skill_root / "SKILL.md"
230
+ if not skill_md_path.is_file():
231
+ errors.append(f"skill `{skill_root.name}` is missing `SKILL.md`")
232
+ return
233
+ try:
234
+ contents = skill_md_path.read_text(encoding="utf-8")
235
+ except OSError:
236
+ errors.append(f"unable to read skill `{skill_root.name}`")
237
+ return
238
+ frontmatter = parse_frontmatter(contents)
239
+ if frontmatter is None:
240
+ errors.append(f"skill `{skill_root.name}` must start with closed YAML frontmatter")
241
+ return
242
+ if not frontmatter_field(frontmatter, "name"):
243
+ errors.append(f"skill `{skill_root.name}` frontmatter field `name` must be non-empty")
244
+ if not frontmatter_field(frontmatter, "description"):
245
+ errors.append(f"skill `{skill_root.name}` frontmatter field `description` must be non-empty")
246
+
247
+
248
+ def parse_frontmatter(contents: str) -> str | None:
249
+ """Return the raw frontmatter block, or None when it is missing/unclosed.
250
+
251
+ Parsed by hand (no yaml dependency) — only the closed `---` fenced block is needed.
252
+ """
253
+ if not contents.startswith("---\n"):
254
+ return None
255
+ end = contents.find("\n---", 4)
256
+ if end == -1:
257
+ return None
258
+ return contents[4:end]
259
+
260
+
261
+ def frontmatter_field(frontmatter: str, key: str) -> bool:
262
+ """True when `key:` appears with a non-empty value on its own line."""
263
+ pattern = re.compile(rf"^{re.escape(key)}:\s*(.+?)\s*$", re.MULTILINE)
264
+ match = pattern.search(frontmatter)
265
+ return bool(match and match.group(1).strip())
266
+
267
+
268
+ if __name__ == "__main__":
269
+ main()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dimcode-darwin-arm64",
3
- "version": "0.2.11-beta.0",
3
+ "version": "0.2.12-beta.0",
4
4
  "description": "dimcode binary for macOS ARM64 (Apple Silicon)",
5
5
  "os": [
6
6
  "darwin"