Sign In

dimagent-linux-x64

Package Overview
Dependencies
Maintainers
1
Versions
11
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

dimagent-linux-x64 - npm Package Compare versions

Comparing version
0.3.12
to
0.3.15
+125
bin/skills-assets/...-pet/tests/test_extract_strip_frames.py
import importlib.util
import sys
import unittest
from pathlib import Path
from PIL import Image, ImageDraw
SKILL_DIR = Path(__file__).resolve().parents[1]
EXTRACT_PATH = SKILL_DIR / "scripts" / "extract_strip_frames.py"
sys.path.insert(0, str(EXTRACT_PATH.parent))
SPEC = importlib.util.spec_from_file_location("extract_strip_frames", EXTRACT_PATH)
if SPEC is None or SPEC.loader is None:
raise RuntimeError(f"Unable to load {EXTRACT_PATH}")
EXTRACT = importlib.util.module_from_spec(SPEC)
SPEC.loader.exec_module(EXTRACT)
def _filled(draw: ImageDraw.ImageDraw, box: tuple[int, int, int, int]) -> None:
draw.rectangle(box, fill=(0, 0, 0, 255))
class ExtractStripFramesFrameBleedTest(unittest.TestCase):
"""A detached fragment must stay in the column that owns it.
The strip has two poses. Frame 0 owns a detached "arm" that sits inside its
own column slot but whose ``center_x`` is nearer frame 1's body center. The
old nearest-``center_x`` assignment pulled the arm across the boundary into
frame 1 (frame 0 lost it, frame 1 gained a stray). Slot-based assignment
keeps it in frame 0.
"""
def _strip(self) -> Image.Image:
# width 400, 2 frames -> slot_width 200; boundary at x=200.
strip = Image.new("RGBA", (400, 208), (0, 0, 0, 0))
draw = ImageDraw.Draw(strip)
_filled(draw, (20, 40, 100, 180)) # frame 0 body, center_x 60, slot 0
_filled(draw, (210, 40, 290, 180)) # frame 1 body, center_x 250, slot 1
# frame 0's detached arm: center_x 165 (slot 0) but nearer seed 1 (250)
# than seed 0 (60); separated from both bodies by a transparent gap.
_filled(draw, (150, 90, 180, 130))
return strip
def test_detached_fragment_stays_in_its_own_column(self) -> None:
strip = self._strip()
groups = EXTRACT.component_frame_groups(strip, 2)
self.assertIsNotNone(groups)
assert groups is not None
def has_arm(group: list[dict[str, object]]) -> bool:
return any(150 <= component["center_x"] <= 180 for component in group)
# Groups are ordered left-to-right by seed center_x.
self.assertTrue(has_arm(groups[0]), "frame 0 should keep its own arm")
self.assertFalse(has_arm(groups[1]), "frame 1 must not receive the stray arm")
def test_neither_frame_keeps_a_detached_fragment(self) -> None:
strip = self._strip()
frames = EXTRACT.extract_component_frames(strip, 2)
self.assertIsNotNone(frames)
assert frames is not None
# The arm is grouped into its own slot (frame 0, not frame 1) — proven by
# test_detached_fragment_stays_in_its_own_column — and then cleaned as a
# detached fragment, so neither frame carries a stray blob.
self.assertEqual(len(EXTRACT.connected_components(frames[0])), 1)
self.assertEqual(len(EXTRACT.connected_components(frames[1])), 1)
class ExtractStripFramesDebrisTest(unittest.TestCase):
"""A small free-floating island inside a frame's slot must be dropped.
Chroma/despill specks and detached fur wisps land in the strip beyond the
poses. When one sits in a frame's column it survives grouping (it is above
the noise floor and owns that slot) and would bake in as a stray pixel blob.
The finished cell must keep only the pose body.
"""
def _strip_with_speck(self) -> Image.Image:
strip = Image.new("RGBA", (400, 208), (0, 0, 0, 0))
draw = ImageDraw.Draw(strip)
_filled(draw, (20, 40, 100, 180)) # frame 0 body (slot 0)
_filled(draw, (210, 40, 290, 180)) # frame 1 body (slot 1)
# frame 0 debris: 10x10 = 100 px, above the noise floor but below the
# 128 px detached floor, separated from the body by a transparent gap.
_filled(draw, (150, 95, 160, 105))
return strip
def test_small_detached_island_is_removed(self) -> None:
frames = EXTRACT.extract_component_frames(self._strip_with_speck(), 2)
self.assertIsNotNone(frames)
assert frames is not None
# Both cells are body-only; the speck is gone.
self.assertEqual(len(EXTRACT.connected_components(frames[0])), 1)
self.assertEqual(len(EXTRACT.connected_components(frames[1])), 1)
@staticmethod
def _fitted(rects: list[tuple[int, int, int, int]]) -> Image.Image:
cell = Image.new("RGBA", (192, 208), (0, 0, 0, 0))
draw = ImageDraw.Draw(cell)
for rect in rects:
_filled(draw, rect)
return EXTRACT.fit_to_cell(cell)
def test_detached_medium_fragment_is_dropped(self) -> None:
# ~650 px fragment (well above 128) sitting ~29 px off a ~11k body, only
# ~6% of it — the baobao-style echo the 128 floor and 15% ratio both miss.
result = self._fitted([(40, 40, 120, 180), (150, 90, 170, 120)])
self.assertEqual(len(EXTRACT.connected_components(result)), 1)
def test_attached_part_is_kept(self) -> None:
# A >=128 px part with a 2 px gap to the body is a real attached detail.
result = self._fitted([(40, 40, 120, 180), (123, 90, 153, 130)])
self.assertEqual(len(EXTRACT.connected_components(result)), 2)
def test_large_second_mass_is_kept(self) -> None:
# A detached component that is >=15% of the main body is a genuine second
# mass; extraction keeps it and the row inspector flags the row instead.
result = self._fitted([(40, 40, 110, 170), (140, 40, 180, 120)])
self.assertEqual(len(EXTRACT.connected_components(result)), 2)
if __name__ == "__main__":
unittest.main()
import json
import subprocess
import sys
import tempfile
import unittest
from pathlib import Path
from PIL import Image, ImageDraw
SKILL_DIR = Path(__file__).resolve().parents[1]
INSPECT_PATH = SKILL_DIR / "scripts" / "inspect_frames.py"
def _cell(secondary: tuple[int, int, int, int] | None = None) -> Image.Image:
"""A 192x208 cell: one rectangular body (100x130 = 13000 px), optionally a
detached secondary blob well to its left."""
cell = Image.new("RGBA", (192, 208), (0, 0, 0, 0))
draw = ImageDraw.Draw(cell)
draw.rectangle((50, 40, 150, 170), fill=(200, 150, 120, 255)) # main body
if secondary is not None:
draw.rectangle(secondary, fill=(200, 150, 120, 255))
return cell
class InspectFramesSecondaryComponentTest(unittest.TestCase):
"""A frame with a second pose-sized component fails the row (crammed pose / stray)."""
def _run(self, cells: list[Image.Image]) -> tuple[int, dict]:
tmp = tempfile.TemporaryDirectory()
self.addCleanup(tmp.cleanup)
root = Path(tmp.name)
state_dir = root / "running-left"
state_dir.mkdir(parents=True)
for index, cell in enumerate(cells):
cell.save(state_dir / f"{index:02d}.png")
(root / "frames-manifest.json").write_text(
json.dumps(
{
"chroma_key": {"rgb": [255, 0, 255]},
"rows": [{"state": "running-left", "method": "components"}],
}
)
)
report_path = root / "report.json"
completed = subprocess.run(
[
sys.executable,
str(INSPECT_PATH),
"--frames-root",
str(root),
"--json-out",
str(report_path),
"--states",
"running-left",
"--require-components",
],
capture_output=True,
text=True,
)
return completed.returncode, json.loads(report_path.read_text())
def test_clean_row_passes(self) -> None:
code, report = self._run([_cell() for _ in range(8)])
self.assertEqual(code, 0)
self.assertTrue(report["ok"])
def test_second_pose_sized_component_fails_the_row(self) -> None:
cells = [_cell() for _ in range(8)]
# Frame 3 gains a detached 31x71 ~= 2200 px second blob (>128 and >15% of body).
cells[3] = _cell(secondary=(5, 70, 35, 140))
code, report = self._run(cells)
self.assertNotEqual(code, 0)
self.assertFalse(report["ok"])
self.assertTrue(
any("second pose-sized component" in error for error in report["errors"]),
report["errors"],
)
def test_tiny_second_component_is_ignored(self) -> None:
cells = [_cell() for _ in range(8)]
# A sub-floor speck (6x6 = 36 px < 128) must NOT fail the row.
cells[3] = _cell(secondary=(10, 90, 16, 96))
code, report = self._run(cells)
self.assertEqual(code, 0, report["errors"])
self.assertTrue(report["ok"])
if __name__ == "__main__":
unittest.main()

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

"""Where installed skills live, and what they are actually called.
A skill's identity is the `name` in its SKILL.md frontmatter, not its directory
name: that is the name the runtime loads it under, and two directories claiming
the same name means only one of them is ever reachable. Everything here reads
that name, and reads it from every root the runtime scans — including skills
that arrive inside plugin bundles, which a scan of the loose skills directories
alone would miss.
"""
from __future__ import annotations
from dataclasses import dataclass
import os
def dimcode_home() -> str:
"""`DIMCODE_HOME` -> `$XDG_CONFIG_HOME/.dimcode/v2` -> `~/.dimcode/v2`."""
explicit = os.environ.get("DIMCODE_HOME", "").strip()
if explicit:
return os.path.abspath(os.path.expanduser(explicit))
xdg = os.environ.get("XDG_CONFIG_HOME", "").strip()
if xdg:
return os.path.abspath(os.path.join(os.path.expanduser(xdg), ".dimcode", "v2"))
return os.path.abspath(os.path.expanduser("~/.dimcode/v2"))
def agents_home() -> str:
return os.path.expanduser(os.environ.get("AGENTS_HOME", "~/.agents"))
def private_skills_root() -> str:
"""Where dimcode installs skills it manages."""
return os.path.join(dimcode_home(), "skills")
def shared_skills_root() -> str:
"""Cross-agent skills directory; read here, written only on explicit request."""
return os.path.join(agents_home(), "skills")
def private_plugins_root() -> str:
return os.path.join(dimcode_home(), "plugins")
def shared_plugins_root() -> str:
return os.path.join(agents_home(), "plugins")
def plugin_roots() -> list[str]:
return [private_plugins_root(), shared_plugins_root()]
@dataclass
class InstalledSkill:
"""A skill already on disk, identified the way the runtime identifies it."""
name: str
path: str
#: "private" | "shared" | "plugin"
origin: str
#: Owning plugin's directory name when origin == "plugin".
plugin: str | None = None
def describe(self) -> str:
if self.origin == "plugin":
return f"{self.name} (from plugin {self.plugin}, {self.path})"
where = "dimcode" if self.origin == "private" else "shared agents dir"
return f"{self.name} ({where}, {self.path})"
def read_skill_name(skill_dir: str) -> str | None:
"""The frontmatter `name`, or None when this isn't a readable skill dir.
Falls back to the directory name only when the file exists but declares no
name — the runtime does the same, and refusing to see such a skill would
make it invisible to the conflict check that exists to protect it.
"""
skill_md = os.path.join(skill_dir, "SKILL.md")
if not os.path.isfile(skill_md):
return None
try:
with open(skill_md, encoding="utf-8") as handle:
head = handle.read(4096)
except OSError:
return None
if not head.startswith("---"):
return os.path.basename(skill_dir.rstrip("/\\"))
end = head.find("\n---", 3)
frontmatter = head[3:end] if end != -1 else head[3:]
for line in frontmatter.splitlines():
key, sep, value = line.partition(":")
if sep and key.strip() == "name":
name = value.strip().strip("'\"")
if name:
return name
return os.path.basename(skill_dir.rstrip("/\\"))
def _scan_loose(root: str, origin: str) -> list[InstalledSkill]:
if not os.path.isdir(root):
return []
found: list[InstalledSkill] = []
for entry in sorted(os.listdir(root)):
path = os.path.join(root, entry)
if not os.path.isdir(path):
continue
name = read_skill_name(path)
if name:
found.append(InstalledSkill(name=name, path=path, origin=origin))
return found
def _scan_plugins() -> list[InstalledSkill]:
found: list[InstalledSkill] = []
for root in plugin_roots():
if not os.path.isdir(root):
continue
for plugin in sorted(os.listdir(root)):
skills_dir = os.path.join(root, plugin, "skills")
if not os.path.isdir(skills_dir):
continue
for entry in sorted(os.listdir(skills_dir)):
path = os.path.join(skills_dir, entry)
if not os.path.isdir(path):
continue
name = read_skill_name(path)
if name:
found.append(
InstalledSkill(name=name, path=path, origin="plugin", plugin=plugin)
)
return found
def installed_skills() -> list[InstalledSkill]:
"""Every skill the runtime can currently see, across every root."""
return [
*_scan_loose(private_skills_root(), "private"),
*_scan_loose(shared_skills_root(), "shared"),
*_scan_plugins(),
]
def installed_by_name() -> dict[str, list[InstalledSkill]]:
out: dict[str, list[InstalledSkill]] = {}
for skill in installed_skills():
out.setdefault(skill.name, []).append(skill)
return out
def installed_plugin_dirs() -> dict[str, str]:
"""Installed plugin directory name -> path, first root wins."""
out: dict[str, str] = {}
for root in plugin_roots():
if not os.path.isdir(root):
continue
for entry in sorted(os.listdir(root)):
path = os.path.join(root, entry)
if entry in out or not os.path.isdir(path):
continue
if ".installing-" in entry or ".backup-" in entry:
continue
out[entry] = path
return out

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

"""Importing skills: where they land, and when the install must refuse.
Everything after the fetch is exercised here — classifying what the path holds,
refusing a name that is already taken, packaging a collection into one plugin —
by pointing the script at a local fixture instead of GitHub. The transport
itself (zip download, git sparse checkout) is untouched by these tests.
"""
import json
import os
import subprocess
import sys
import tempfile
import textwrap
import unittest
from pathlib import Path
SKILL_DIR = Path(__file__).resolve().parents[1]
SCRIPTS_DIR = SKILL_DIR / "scripts"
def write_skill(directory: Path, name: str, description: str = "does things") -> Path:
directory.mkdir(parents=True, exist_ok=True)
(directory / "SKILL.md").write_text(
f"---\nname: {name}\ndescription: {description}\n---\nbody\n", encoding="utf-8"
)
return directory
# Runs the script's main() with the fetch stubbed out to a local directory, and
# with `dim` replaced by a stub that behaves like the real plugin installer:
# it copies the staged bundle into the plugins root and prints the JSON record.
DRIVER = textwrap.dedent(
"""
import json, os, shutil, sys
sys.path.insert(0, os.environ["SCRIPTS_DIR"])
import importlib.util
spec = importlib.util.spec_from_file_location(
"installer", os.path.join(os.environ["SCRIPTS_DIR"], "install-skill-from-github.py")
)
installer = importlib.util.module_from_spec(spec)
sys.modules["installer"] = installer
spec.loader.exec_module(installer)
installer._prepare_repo = lambda source, method, tmp_dir: os.environ["REPO_ROOT"]
def fake_install_bundle(bundle_root, bundle_name, provenance_source, ref, overwrite, source_paths=None):
import skill_roots
dest = os.path.join(skill_roots.private_plugins_root(), bundle_name)
os.makedirs(os.path.dirname(dest), exist_ok=True)
if os.path.exists(dest):
if not overwrite:
raise installer.InstallError("already installed")
shutil.rmtree(dest)
shutil.copytree(bundle_root, dest)
with open(os.path.join(dest, "dim-install.json"), "w", encoding="utf-8") as fh:
json.dump(
{
"source": provenance_source,
"ref": ref,
"generated": True,
"generatedPaths": source_paths or [],
"generatedPluginName": bundle_name,
}, fh
)
return dest
installer._install_bundle = fake_install_bundle
raise SystemExit(installer.main(json.loads(os.environ["ARGV"])))
"""
)
class InstallSkillTest(unittest.TestCase):
def setUp(self) -> None:
self._tmp = tempfile.TemporaryDirectory()
root = Path(self._tmp.name)
self.home = root / "home"
self.dimcode_home = self.home / ".dimcode" / "v2"
self.repo = root / "repo"
(self.home / ".agents" / "skills").mkdir(parents=True)
(self.dimcode_home / "skills").mkdir(parents=True)
self.addCleanup(self._tmp.cleanup)
def run_installer(self, argv: list[str]) -> subprocess.CompletedProcess:
env = {
**os.environ,
"HOME": str(self.home),
"AGENTS_HOME": str(self.home / ".agents"),
"DIMCODE_HOME": str(self.dimcode_home),
"SCRIPTS_DIR": str(SCRIPTS_DIR),
"REPO_ROOT": str(self.repo),
"ARGV": json.dumps(argv),
}
env.pop("XDG_CONFIG_HOME", None)
return subprocess.run(
[sys.executable, "-c", DRIVER],
capture_output=True,
text=True,
env=env,
)
def test_installs_into_dimcode_own_root_by_default(self) -> None:
write_skill(self.repo / "pdf", "pdf")
result = self.run_installer(["--repo", "acme/skills", "--path", "pdf"])
self.assertEqual(result.returncode, 0, result.stderr)
self.assertTrue((self.dimcode_home / "skills" / "pdf" / "SKILL.md").is_file())
self.assertFalse((self.home / ".agents" / "skills" / "pdf").exists())
def test_shared_flag_installs_into_the_cross_agent_directory(self) -> None:
write_skill(self.repo / "pdf", "pdf")
result = self.run_installer(["--repo", "acme/skills", "--path", "pdf", "--shared"])
self.assertEqual(result.returncode, 0, result.stderr)
self.assertTrue((self.home / ".agents" / "skills" / "pdf" / "SKILL.md").is_file())
self.assertFalse((self.dimcode_home / "skills" / "pdf").exists())
def test_directory_name_is_not_the_skill_identity(self) -> None:
# The runtime resolves the frontmatter name, so that is what the install
# is named after — not the folder it happened to sit in upstream.
write_skill(self.repo / "folder-name", "actual-name")
result = self.run_installer(["--repo", "acme/skills", "--path", "folder-name"])
self.assertEqual(result.returncode, 0, result.stderr)
self.assertTrue((self.dimcode_home / "skills" / "actual-name").is_dir())
def test_lists_candidates_for_a_collection_directory(self) -> None:
write_skill(self.repo / "web" / "better-ui", "better-ui")
write_skill(self.repo / "web" / "better-colors", "better-colors")
result = self.run_installer(
["--repo", "acme/skills", "--path", "web", "--list-candidates", "--format", "json"]
)
self.assertEqual(result.returncode, 0, result.stderr)
payload = json.loads(result.stdout)
names = sorted(skill["name"] for skill in payload[0]["skills"])
self.assertEqual(names, ["better-colors", "better-ui"])
def test_refuses_a_name_already_taken_by_a_loose_skill(self) -> None:
write_skill(self.repo / "pdf", "pdf")
write_skill(self.home / ".agents" / "skills" / "pdf", "pdf")
result = self.run_installer(["--repo", "acme/skills", "--path", "pdf"])
self.assertEqual(result.returncode, 1)
self.assertIn("already installed under the same name", result.stderr)
self.assertFalse((self.dimcode_home / "skills" / "pdf").exists())
def test_refuses_a_name_already_taken_by_a_plugin_bundled_skill(self) -> None:
# A bundled skill occupies the name just as a loose one does; scanning
# only the skills directories would miss it and install a dead copy.
write_skill(self.repo / "better-ui", "better-ui")
write_skill(self.dimcode_home / "plugins" / "web-pack" / "skills" / "better-ui", "better-ui")
result = self.run_installer(["--repo", "acme/skills", "--path", "better-ui"])
self.assertEqual(result.returncode, 1)
self.assertIn("from plugin web-pack", result.stderr)
def test_packages_a_collection_into_one_plugin(self) -> None:
write_skill(self.repo / "web" / "better-ui", "better-ui")
write_skill(self.repo / "web" / "better-colors", "better-colors")
result = self.run_installer(
[
"--repo", "acme/skills",
"--path", "web/better-ui", "web/better-colors",
"--as-plugin", "--plugin-name", "web-pack",
]
)
self.assertEqual(result.returncode, 0, result.stderr)
bundle = self.dimcode_home / "plugins" / "web-pack"
manifest = json.loads((bundle / ".codex-plugin" / "plugin.json").read_text())
self.assertEqual(manifest["name"], "web-pack")
self.assertEqual(manifest["description"], "")
self.assertEqual(manifest["skills"], "./skills")
self.assertTrue((bundle / "skills" / "better-ui" / "SKILL.md").is_file())
self.assertTrue((bundle / "skills" / "better-colors" / "SKILL.md").is_file())
# Provenance records the origin so a later reinstall can rebuild it.
provenance = json.loads((bundle / "dim-install.json").read_text())
self.assertEqual(provenance["source"], "acme/skills")
self.assertTrue(provenance["generated"])
# Nothing leaks into the loose skills directories.
self.assertFalse((self.dimcode_home / "skills" / "better-ui").exists())
def test_rejects_dest_combined_with_as_plugin(self) -> None:
write_skill(self.repo / "pdf", "pdf")
result = self.run_installer(
["--repo", "acme/skills", "--path", "pdf", "--as-plugin", "--dest", "/tmp/x"]
)
self.assertEqual(result.returncode, 1)
self.assertIn("--dest cannot be combined with --as-plugin", result.stderr)
if __name__ == "__main__":
unittest.main()
---
name: skill-migrator
description: Migrate existing Codex, Claude, or npx skills into Dim. Use when a user wants to bring skills from ~/.codex/skills, ~/.claude/skills, ~/.agents/skills, project skill directories, or another local skill folder into Dim, especially when several skills should be managed together as one plugin.
metadata:
short-description: Migrate Codex and Claude skills into Dim
---
# Skill Migrator
Move existing skills into Dim's managed installation model without losing the
source files, metadata, scripts, references, or assets.
This skill is for migration, not for silently copying a directory. Always inspect
the source first, explain what will happen, and preserve the original until the
Dim installation has been verified.
## Supported sources
Check these locations in this order when the user says "my Codex skills",
"my Claude skills", or "skills installed with npx skills":
1. `~/.agents/skills/` — the usual shared user-level skills directory.
2. `~/.codex/skills/` — Codex-specific user skills, when present.
3. `~/.claude/skills/` — Claude user-level skills, when present.
4. `<project>/.agents/skills/` — shared project-scoped skills.
5. `<project>/.claude/skills/` — Claude project-scoped skills.
6. A local directory or repository path explicitly supplied by the user.
Do not assume that a directory name is the skill identity. Read each
`SKILL.md` frontmatter and use its canonical `name`. A valid skill directory
must contain a readable `SKILL.md` with non-empty `name` and `description`
fields.
## Classify before migrating
Inspect the source root and classify it:
- A directory containing `SKILL.md` is one skill.
- A directory without `SKILL.md` whose child directories contain two or more
valid `SKILL.md` files is a skill collection.
- A root containing `.codex-plugin/plugin.json` or
`.claude-plugin/plugin.json` is already a plugin. Do not wrap it in another
plugin; use the existing plugin install flow.
- A repository containing a marketplace catalog is a plugin marketplace source,
not a loose skill collection.
If the source is ambiguous, show the discovered paths and ask the user to
choose. Never guess a nested directory or silently skip invalid entries.
## Choose the destination
For one skill, install it as a standalone Dim skill unless the user requests a
plugin.
For two or more skills from the same source, recommend one plugin:
```text
Found N skills from <source>.
Recommended: import them as one plugin
- one enable/disable switch
- one uninstall boundary
- individual skill switches on the plugin detail page
Alternative: import them as standalone skills
- each skill is managed separately in Skills
```
If the user chooses a plugin, derive a stable lower-case hyphenated plugin name
from the source or collection directory. Ask for a name when the derived name
would be empty, ambiguous, or already installed. Use the real collection name
as the plugin display name when it is available.
Do not offer `~/.agents/skills` as a Dim installation destination by default.
That directory is shared with other agents and should only be written when the
user explicitly asks for cross-agent sharing. Dim-managed output belongs in its
private skills or plugins root.
## Migration procedure
1. Discover candidate directories and read their `SKILL.md` files.
2. Report the canonical names, source paths, and any invalid or duplicate names.
3. Check all skill discovery roots for canonical-name conflicts, including
existing Dim skills, shared skills, project skills, and skills inside plugins.
4. Stop on a conflict. Report both paths and ask the user to rename, remove, or
exclude one. Never silently rename a skill because the runtime resolves
skills by canonical name.
5. Preserve the complete skill directory. Copy `SKILL.md`, `scripts/`,
`references/`, `assets/`, `agents/`, and other files unless the user
explicitly asks for a minimal import.
6. Install through the existing Dim skill/plugin installer. Do not implement a
second ad-hoc copy protocol in the conversation.
7. Verify the installed result by rescanning Dim's skills and plugins, checking
canonical names and the plugin component list.
8. Only after verification, offer to archive or remove the original. Never
delete `~/.codex/skills`, `~/.claude/skills`, `~/.agents/skills`, or a
project directory without explicit confirmation.
## Plugin layout
When importing a collection as one plugin, the resulting bundle should have
this shape:
```text
<plugin>/
├── .codex-plugin/plugin.json
└── skills/
├── <skill-a>/SKILL.md
├── <skill-b>/SKILL.md
└── ...
```
For a skills-only migration, this is the complete Dim manifest contract. Use it
directly; do not pause to research or reconfirm the format after the user has
already selected the skills and the plugin installation mode:
```json
{
"name": "<lower-case-hyphenated-plugin-id>",
"version": "0.0.0",
"description": "",
"interface": {
"displayName": "<real collection or user-facing title>"
},
"skills": "./skills"
}
```
Manifest rules:
- Write the file at `<bundle>/.codex-plugin/plugin.json`.
- `name` is the stable internal id and plugin directory name. It must be
non-empty, lower-case hyphen-case, and contain only letters, digits, `.`, `_`,
or `-`.
- `interface.displayName` is the real user-facing collection title. It does not
need to match `name`.
- Use `version: "0.0.0"` for a generated migration bundle unless the source
provides a meaningful bundle version.
- Keep `description: ""` when no collection description exists; do not insert
placeholder text.
- Use exactly `skills: "./skills"` and place every migrated skill at
`<bundle>/skills/<directory>/SKILL.md`.
- Do not add `hooks`, `mcpServers`, `agents`, or other manifest fields for a
skills-only migration.
- Keep each skill's canonical frontmatter name unchanged.
After creating the temporary bundle, install it through:
```bash
dim plugin install <bundle-path> --generated --provenance-source <source-path>
```
Use `--overwrite` only when the user explicitly chose to replace an existing
plugin with the same internal name. The plugin installer owns final validation,
conflict checks, provenance, and destination placement. Do not manually copy the
finished bundle into Dim's plugins directory.
Once the user has selected the skills and chosen "install as one plugin", proceed
with bundle creation and installation. Do not answer with "let me confirm the
manifest format" or ask the same installation-mode question again.
## npx skills guidance
When the user says they installed skills with `npx skills`, first inspect the
user and project skill roots above. The CLI may have installed a single skill,
several skills from a collection, or a plugin-shaped source. Use the files that
are actually present as the source of truth rather than reconstructing the
repository URL from memory.
If only a repository URL is available, use the existing `skill-installer` flow
to fetch and classify it. If the user asks for a plugin result, pass the
collection's skill directories through the plugin bundle path instead of
installing them loose and then copying them a second time.
## Communication
Tell the user:
- what was found and where;
- whether the result is a standalone skill or a plugin;
- the Dim-managed destination;
- which original files remain untouched;
- whether a new session or reload is needed before the migrated skill can be
used by the agent.
If migration is blocked, report the exact conflict or invalid file and stop.
+2
-2

@@ -106,4 +106,4 @@ {

"path": "facade/node_modules/sharp/node_modules/.bin/semver",
"size": 1842,
"sha256": "b35180e0d9290fc2b3a8a50abf59bf7e19086b5a5df6dc1473ed9c0598520ba4"
"size": 2009,
"sha256": "83d7a17b81c23c373e3e5d7740d59a7d9832e8713e9b235cabc805efae3f9e4b"
},

@@ -110,0 +110,0 @@ {

@@ -12,7 +12,27 @@ ---

If the `chrome` tool is not available, Chrome Control is off. Tell the user to
enable it from the Desktop sidebar: **Skills / Plugins → Plugins → My Plugins →
Chrome Control**, and stop — do not try to reach Chrome another way. It is not
in Settings.
## Setup triage
Chrome Control needs two things the user controls: the feature toggle in
Desktop, and the DimAgent extension installed in their Chrome. Both default to
off/absent, so on a fresh install neither is there. Diagnose before acting, tell
the user the one next step, and stop — do not try to reach Chrome another way.
1. **No `chrome` tool at all** → the feature is off. Tell the user to turn it on
at **Skills / Plugins → Plugins → My Plugins → Chrome Control** (sidebar, not
Settings). Then tell them step 2 is next, so they do not have to come back
for it.
2. **`chrome status` reports not connected** → the extension is missing or
Chrome is not running. The same Chrome Control page has **Setup Chrome
Extension** → *Open Chrome Web Store*; the user installs **DimAgent Chrome
Control** there and keeps Chrome open. Desktop connects on its own within a
few seconds — no restart of DimAgent needed.
3. **Connected before, not connected now** → Chrome was quit, the extension was
disabled or removed, or another DimAgent instance took the connection over.
Ask the user to check Chrome is running with the extension enabled, then use
**Troubleshoot → Re-register Native Host** on the same page.
Do not claim Chrome Control is broken or unavailable in this build, and do not
guess between these three — `chrome status` distinguishes 2 from 3, and the tool
being absent entirely distinguishes 1.
## When to use Chrome

@@ -19,0 +39,0 @@

@@ -16,2 +16,17 @@ #!/usr/bin/env python3

CELL_HEIGHT = 208
# Detached-component floor for a finished cell. A connected component smaller
# than this that is not the frame's main mass is treated as debris (chroma/
# despill speck, detached fur wisp) and dropped. Mirrors the look-row assembler
# (assemble_extended_atlas.MIN_DETACHED_COMPONENT_PIXELS); a follow-up task will
# unify the two and add proximity-based attach / fail-fast handling.
MIN_DETACHED_COMPONENT_PIXELS = 128
# A non-main component is kept only if it is a genuine second mass (at least
# DETACHED_SECOND_ELEMENT_FRACTION of the main body — the row inspector then
# flags the row) or is attached to the body within ATTACH_GAP_PX. Everything
# else is debris. This closes the gap between the absolute floor above and the
# row-inspection ratio: a medium fragment (e.g. a 400-700 px echo of a
# neighbouring pose sitting 10-15 px off the body) is neither tiny nor large but
# is still debris.
DETACHED_SECOND_ELEMENT_FRACTION = 0.15
ATTACH_GAP_PX = 3
ROW_FRAME_COUNTS = {

@@ -83,2 +98,5 @@ "idle": 6,

def fit_to_cell(image: Image.Image) -> Image.Image:
# Clean debris before measuring so the pose is centered on its own body, not
# skewed by a floating speck that is about to be removed anyway.
image = remove_small_detached_components(image)
bbox = image.getbbox()

@@ -105,2 +123,3 @@ target = Image.new("RGBA", (CELL_WIDTH, CELL_HEIGHT), (0, 0, 0, 0))

def fit_viewport_to_cell(image: Image.Image) -> Image.Image:
image = remove_small_detached_components(image)
target = Image.new("RGBA", (CELL_WIDTH, CELL_HEIGHT), (0, 0, 0, 0))

@@ -187,2 +206,46 @@ if image.getbbox() is None:

def _bbox_gap(
a: tuple[int, int, int, int], b: tuple[int, int, int, int]
) -> int:
"""Smallest axis separation between two bboxes; 0 when they touch or overlap."""
dx = max(0, a[0] - b[2], b[0] - a[2])
dy = max(0, a[1] - b[3], b[1] - a[3])
return max(dx, dy)
def remove_small_detached_components(image: Image.Image) -> Image.Image:
"""Drop free-floating debris from a finished cell.
The generated strip carries stray islands beyond the poses: chroma/despill
specks, fur wisps, or a medium echo of a neighbouring pose the model drew a
few pixels off the body. When one lands in a frame's column it survives
grouping and bakes in as a detached blob. Keep the largest connected mass;
keep a genuine second mass (>= ``DETACHED_SECOND_ELEMENT_FRACTION`` of it,
which the row inspector then flags) and a part touching the body within
``ATTACH_GAP_PX``; clear everything else.
"""
rgba = image.convert("RGBA")
components = sorted(
connected_components(rgba), key=lambda component: component["area"], reverse=True
)
if not components:
return rgba
main = components[0]
main_area = main["area"]
main_bbox = main["bbox"]
width = rgba.width
pixels = rgba.load()
for component in components[1:]:
area = component["area"]
attached = (
area >= MIN_DETACHED_COMPONENT_PIXELS
and _bbox_gap(main_bbox, component["bbox"]) <= ATTACH_GAP_PX
)
if area >= main_area * DETACHED_SECOND_ELEMENT_FRACTION or attached:
continue
for index in component["pixels"]:
pixels[index % width, index // width] = (0, 0, 0, 0)
return rgba
def component_group_image(

@@ -236,7 +299,24 @@ source: Image.Image,

# Assign non-seed fragments (arms, feet, marks) by the equal-width column slot
# that owns their center, not by nearest seed center_x. A detached part of a
# walking pose stays in its own column instead of being pulled across a frame
# boundary when a neighbour's seed happens to sit closer (frame bleed).
slot_width = strip.width / frame_count
def slot_of(center_x: float) -> int:
return min(frame_count - 1, max(0, int(center_x // slot_width)))
seed_slots = [slot_of(seed["center_x"]) for seed in seeds]
for component in components:
if id(component) in seed_ids or component["area"] < noise_threshold:
continue
target_slot = slot_of(component["center_x"])
candidates = [index for index, slot in enumerate(seed_slots) if slot == target_slot]
if not candidates:
# Degenerate strip where no seed occupies this column: fall back to the
# old nearest-center behaviour so the fragment is not dropped.
candidates = list(range(len(seeds)))
nearest_index = min(
range(len(seeds)),
candidates,
key=lambda index: abs(seeds[index]["center_x"] - component["center_x"]),

@@ -243,0 +323,0 @@ )

@@ -45,2 +45,40 @@ #!/usr/bin/env python3

def two_largest_component_areas(image: Image.Image) -> tuple[int, int]:
"""Pixel areas of the two largest opaque (alpha > 16) 4-connected components.
A clean pose is a single connected mass, so the second area is ~0. A large
second area means the cell crammed more than one pose or kept a pose-sized
stray — the failure that concentrates on the leftward row when it is
generated independently instead of mirrored from the clean rightward row.
"""
alpha = image.getchannel("A")
width, height = alpha.size
data = alpha.tobytes()
total = width * height
visited = bytearray(total)
areas: list[int] = []
for start in range(total):
if data[start] <= 16 or visited[start]:
continue
stack = [start]
visited[start] = 1
count = 0
while stack:
current = stack.pop()
count += 1
x = current % width
for neighbour, in_bounds in (
(current - 1, x > 0),
(current + 1, x + 1 < width),
(current - width, current - width >= 0),
(current + width, current + width < total),
):
if in_bounds and not visited[neighbour] and data[neighbour] > 16:
visited[neighbour] = 1
stack.append(neighbour)
areas.append(count)
areas.sort(reverse=True)
return (areas[0] if areas else 0, areas[1] if len(areas) > 1 else 0)
def edge_alpha_count(image: Image.Image, margin: int) -> int:

@@ -165,2 +203,3 @@ alpha = image.getchannel("A")

)
largest_component, second_component = two_largest_component_areas(frame)
info = {

@@ -175,2 +214,4 @@ "index": index,

"chroma_adjacent_pixels": chroma_adjacent_pixels,
"largest_component_pixels": largest_component,
"second_component_pixels": second_component,
}

@@ -196,2 +237,13 @@ frames.append(info)

)
if (
largest_component > 0
and second_component >= args.secondary_component_min_pixels
and second_component >= largest_component * args.secondary_component_ratio
):
row_errors.append(
f"{state} frame {index:02d} has a second pose-sized component "
f"({second_component} px, {second_component / largest_component:.0%} of the main body); "
"the row crammed more than one pose or kept a stray — regenerate the row, or mirror it "
"from the opposite direction when that is identity-safe"
)

@@ -235,2 +287,14 @@ if areas:

parser.add_argument(
"--secondary-component-ratio",
type=float,
default=0.15,
help="Fail a frame whose second connected component is at least this fraction of the main body.",
)
parser.add_argument(
"--secondary-component-min-pixels",
type=int,
default=128,
help="Ignore second components below this size (matches the extractor's detached-debris floor).",
)
parser.add_argument(
"--require-components",

@@ -237,0 +301,0 @@ action="store_true",

@@ -8,2 +8,3 @@ #!/usr/bin/env python3

from dataclasses import dataclass
import json
import os

@@ -19,2 +20,11 @@ import shutil

from github_utils import github_request
from skill_roots import (
installed_by_name,
installed_plugin_dirs,
private_plugins_root,
private_skills_root,
read_skill_name,
shared_skills_root,
)
DEFAULT_REF = "main"

@@ -32,2 +42,8 @@

method: str = "auto"
as_plugin: bool = False
plugin_name: str | None = None
shared: bool = False
overwrite: bool = False
list_candidates: bool = False
format: str = "text"

@@ -48,6 +64,2 @@

def _agents_home() -> str:
return os.path.expanduser("~/.agents")
def _tmp_root() -> str:

@@ -247,6 +259,151 @@ base = os.path.join(tempfile.gettempdir(), "agents")

def _default_dest() -> str:
return os.path.join(_agents_home(), "skills")
def _default_bundle_name(source: Source) -> str:
"""Name the bundle after the directory the skills came from.
A collection lives under a category path (`.../web`), which reads better as
a plugin name than the repo alone; a repo-root install falls back to the
repo name.
"""
if len(source.paths) == 1:
base = os.path.basename(source.paths[0].rstrip("/"))
if base:
return f"{source.repo}-{base}"
common = os.path.commonpath([p.rstrip("/") for p in source.paths]) if source.paths else ""
base = os.path.basename(common)
return f"{source.repo}-{base}" if base else source.repo
def _default_dest(shared: bool) -> str:
"""Dimcode's own skills root, unless the caller explicitly asked to share.
Installing into `~/.agents/skills` makes the skill visible to other agents
but takes it out of dimcode's update path, so it is never the default —
only an explicit `--shared`.
"""
return shared_skills_root() if shared else private_skills_root()
def _find_candidates(path: str) -> list[dict[str, str]]:
"""Skills one level under `path`, for a directory that is not itself a skill.
A collection repo points at a category directory, not at a skill, so the
caller needs the list before it can ask the user how to import them.
"""
if not os.path.isdir(path):
raise InstallError(f"Path not found: {path}")
found = []
for entry in sorted(os.listdir(path)):
child = os.path.join(path, entry)
if not os.path.isdir(child):
continue
name = read_skill_name(child)
if name:
found.append({"dir": entry, "name": name})
return found
def _assert_no_name_conflict(names: list[str]) -> None:
"""Refuse to install a skill whose name is already taken.
Skills are resolved by name and deduped across roots, so a second skill
under an existing name is never the one that loads — installing it would
report success and leave the user with a skill that silently does nothing.
The user has to resolve it (remove, rename, or skip) before we write.
"""
existing = installed_by_name()
clashes = []
for name in names:
for owner in existing.get(name, []):
clashes.append(f" {name}: already installed as {owner.describe()}")
if clashes:
raise InstallError(
"These skills are already installed under the same name:\n"
+ "\n".join(clashes)
+ "\nRemove or rename the existing one, or drop it from this install, then retry."
)
def _synthesize_bundle(
staged: list[tuple[str, str]],
bundle_name: str,
tmp_dir: str,
) -> str:
"""Package skills into a plugin bundle laid out for the plugin installer.
Producing a real bundle — rather than writing into the plugins directory
directly — is what lets the shared installer do the manifest validation,
conflict check, provenance and atomic swap exactly once.
"""
bundle_root = os.path.join(tmp_dir, "bundle", bundle_name)
manifest_dir = os.path.join(bundle_root, ".codex-plugin")
os.makedirs(manifest_dir, exist_ok=True)
manifest = {
"name": bundle_name,
"version": "0.0.0",
# Required by the manifest summary; a synthesized bundle has no
# upstream description to carry, so it is explicitly empty.
"description": "",
"skills": "./skills",
"interface": {"displayName": bundle_name},
}
with open(os.path.join(manifest_dir, "plugin.json"), "w", encoding="utf-8") as handle:
json.dump(manifest, handle, indent=2)
for skill_name, skill_src in staged:
_validate_skill_name(skill_name)
shutil.copytree(skill_src, os.path.join(bundle_root, "skills", skill_name))
return bundle_root
def _dim_binary() -> str:
return os.environ.get("DIMCODE_CLI_BIN", "dim")
def _install_bundle(
bundle_root: str,
bundle_name: str,
provenance_source: str,
ref: str,
overwrite: bool,
source_paths: list[str] | None = None,
) -> str:
"""Hand the staged bundle to the shared plugin installer."""
installed_plugins = installed_plugin_dirs()
if not overwrite and bundle_name in installed_plugins:
raise InstallError(
f'A plugin named "{bundle_name}" is already installed at '
f"{installed_plugins[bundle_name]}.\n"
"Pass --plugin-name to install under another name, or --overwrite to replace it."
)
cmd = [
_dim_binary(), "plugin", "install", bundle_root,
"--provenance-source", provenance_source,
"--ref", ref,
"--generated",
"--generated-plugin-name", bundle_name,
"--json",
]
for source_path in source_paths or []:
cmd.extend(["--generated-path", source_path])
if overwrite:
cmd.append("--overwrite")
try:
result = subprocess.run(
cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True
)
except OSError as exc:
raise InstallError(
f"Cannot run '{_dim_binary()} plugin install': {exc}. "
"Set DIMCODE_CLI_BIN if the CLI is not on PATH."
) from exc
if result.returncode != 0:
message = (result.stderr or result.stdout).strip()
raise InstallError(f"Plugin install failed: {message or 'unknown error'}")
fallback = os.path.join(private_plugins_root(), bundle_name)
try:
installed = json.loads(result.stdout.strip().splitlines()[-1])
except (ValueError, IndexError):
return fallback
return str(installed.get("path") or fallback)
def _parse_args(argv: list[str]) -> Args:

@@ -271,8 +428,74 @@ parser = argparse.ArgumentParser(description="Install a skill from GitHub.")

)
parser.add_argument(
"--as-plugin",
action="store_true",
help="Package the selected skills into one plugin instead of installing them loose",
)
parser.add_argument(
"--plugin-name",
help="Plugin name for --as-plugin (defaults to the path or repo name)",
)
parser.add_argument(
"--shared",
action="store_true",
help="Install into ~/.agents/skills so other agents see it (dimcode stops managing updates)",
)
parser.add_argument(
"--overwrite",
action="store_true",
help="Replace an existing plugin of the same name (--as-plugin only)",
)
parser.add_argument(
"--list-candidates",
action="store_true",
help="List the skills under --path instead of installing, for a collection directory",
)
parser.add_argument("--format", choices=["text", "json"], default="text")
parser.add_argument("--generated-path", action="append", default=[], help=argparse.SUPPRESS)
parser.add_argument("--generated-plugin-name", help=argparse.SUPPRESS)
return parser.parse_args(argv, namespace=Args())
def _report_candidates(paths: list[str], repo_root: str, fmt: str) -> int:
"""Print what lives under a directory that is not itself a skill."""
payload = []
for path in paths:
target = os.path.join(repo_root, path)
payload.append({"path": path, "skills": _find_candidates(target)})
if fmt == "json":
print(json.dumps(payload))
return 0
for entry in payload:
skills = entry["skills"]
if not skills:
print(f"{entry['path']}: no skills found")
continue
print(f"{entry['path']}: {len(skills)} skills")
for skill in skills:
print(f" {skill['name']}")
return 0
def _stage_skills(source: Source, repo_root: str, args: Args) -> list[tuple[str, str]]:
"""Resolve each requested path to (skill name, source directory)."""
staged: list[tuple[str, str]] = []
for path in source.paths:
skill_src = os.path.join(repo_root, path)
_validate_skill(skill_src)
override = args.name if len(source.paths) == 1 else None
skill_name = override or read_skill_name(skill_src) or os.path.basename(path.rstrip("/"))
_validate_skill_name(skill_name)
staged.append((skill_name, skill_src))
return staged
def main(argv: list[str]) -> int:
args = _parse_args(argv)
try:
if args.as_plugin and args.dest:
raise InstallError("--dest cannot be combined with --as-plugin.")
if args.as_plugin and args.shared:
raise InstallError(
"--shared cannot be combined with --as-plugin; a bundle installs as a plugin."
)
source = _resolve_source(args)

@@ -284,18 +507,36 @@ source.ref = source.ref or args.ref

_validate_relative_path(path)
dest_root = args.dest or _default_dest()
tmp_dir = tempfile.mkdtemp(prefix="skill-install-", dir=_tmp_root())
try:
repo_root = _prepare_repo(source, args.method, tmp_dir)
if args.list_candidates:
return _report_candidates(source.paths, repo_root, args.format)
staged = _stage_skills(source, repo_root, args)
# Names are how the runtime resolves skills, so a clash makes the
# new copy unreachable. Check before writing anything, whichever
# mode we are in.
_assert_no_name_conflict([name for name, _ in staged])
if args.as_plugin:
bundle_name = args.plugin_name or _default_bundle_name(source)
_validate_skill_name(bundle_name)
bundle_root = _synthesize_bundle(staged, bundle_name, tmp_dir)
path = _install_bundle(
bundle_root,
bundle_name,
f"{source.owner}/{source.repo}",
source.ref,
args.overwrite,
source.paths,
)
print(f"Installed plugin {bundle_name} ({len(staged)} skills) to {path}")
return 0
dest_root = args.dest or _default_dest(args.shared)
installed = []
for path in source.paths:
skill_name = args.name if len(source.paths) == 1 else None
skill_name = skill_name or os.path.basename(path.rstrip("/"))
_validate_skill_name(skill_name)
if not skill_name:
raise InstallError("Unable to derive skill name.")
for skill_name, skill_src in staged:
dest_dir = os.path.join(dest_root, skill_name)
if os.path.exists(dest_dir):
raise InstallError(f"Destination already exists: {dest_dir}")
skill_src = os.path.join(repo_root, path)
_validate_skill(skill_src)
_copy_skill(skill_src, dest_dir)

@@ -302,0 +543,0 @@ installed.append((skill_name, dest_dir))

@@ -13,2 +13,3 @@ #!/usr/bin/env python3

from github_utils import github_api_contents_url, github_request
from skill_roots import installed_by_name

@@ -35,18 +36,17 @@ DEFAULT_REPO = "openai/skills"

def _agents_home() -> str:
return os.path.expanduser("~/.agents")
def _installed_skills() -> dict[str, str]:
"""Installed skill name -> where it came from.
Keyed by the SKILL.md `name` rather than the directory name (that is what
the runtime resolves), and covering every root it scans — including skills
that arrive inside plugin bundles, which would otherwise be reported as not
installed and then collide on install.
"""
return {
name: owners[0].describe()
for name, owners in installed_by_name().items()
if owners
}
def _installed_skills() -> set[str]:
root = os.path.join(_agents_home(), "skills")
if not os.path.isdir(root):
return set()
entries = set()
for name in os.listdir(root):
path = os.path.join(root, name)
if os.path.isdir(path):
entries.add(name)
return entries
def _list_skills(repo: str, path: str, ref: str) -> list[str]:

@@ -95,3 +95,8 @@ api_url = github_api_contents_url(repo, path, ref)

payload = [
{"name": name, "installed": name in installed} for name in skills
{
"name": name,
"installed": name in installed,
**({"installedAs": installed[name]} if name in installed else {}),
}
for name in skills
]

@@ -101,3 +106,3 @@ print(json.dumps(payload))

for idx, name in enumerate(skills, start=1):
suffix = " (already installed)" if name in installed else ""
suffix = f" (already installed — {installed[name]})" if name in installed else ""
print(f"{idx}. {name}{suffix}")

@@ -104,0 +109,0 @@ return 0

---
name: skill-installer
description: Install Agents skills into $AGENTS_HOME/skills from a curated list or a GitHub repo path. Use when a user asks to list installable skills, install a curated skill, or install a skill from another repo (including private repos).
description: Install Agents skills from a curated list or a GitHub repo, either loose or packaged as one plugin. Use when a user asks to list installable skills, install a curated skill, install a skill from another repo (including private repos), or import a whole collection of skills.
metadata:

@@ -17,4 +17,68 @@ short-description: Install curated skills from openai/skills or other repos

## Step 1: Classify the source before installing
Not every path a user points at is a single skill. Check what it actually is:
| What is at the path | How to tell | What to do |
| --- | --- | --- |
| One skill | the directory contains `SKILL.md` | install it directly, no questions |
| A collection / category | no `SKILL.md`, but subdirectories have one | **ask how to import** (step 2) |
| A plugin | repo root has `.codex-plugin/plugin.json` or `.claude-plugin/plugin.json` | not this skill's job — use `dim plugin install <source>` |
| A marketplace | repo has a `marketplace.json` catalog | not this skill's job — use `dim plugin marketplace add <source>` |
`scripts/install-skill-from-github.py --list-candidates --path <dir>` prints the
skills under a directory (add `--format json` to read it back reliably). Use it
when the path is not itself a skill.
## Step 2: Ask how to import a collection
When two or more skills come from the same place, ask once — the answer changes
how the user manages them afterwards, and it can't be inferred. Use a structured
question tool if one is available; otherwise ask in plain text:
```
Found a skill collection at vercel-labs/agent-skills (web/, 5 skills):
better-ui, better-colors, better-layout, better-typography, better-writing
How should these be installed?
1. As one plugin "agent-skills-web" (recommended)
Appears under Plugins: enable, disable or remove all five together,
with per-skill switches on its detail page.
2. Loose
Appears as five separate entries under Skills, managed one by one.
3. Loose, into the shared ~/.agents/skills directory
Other agents (Codex and so on) can see them, but Dim no longer
manages their updates.
```
When to ask:
| Situation | What to do |
| --- | --- |
| One skill | install loose, don't ask |
| Two or more skills from one source | ask once, then reuse that answer for the rest of the conversation |
| The user already said how they want it | don't ask |
| No one is there to answer | install loose and mention `--as-plugin` in the result |
## Step 3: Install
Install skills with the helper scripts.
Loose (default — installs into Dim's own skills directory):
```
scripts/install-skill-from-github.py --repo <owner>/<repo> --path <path/to/skill> [<path/to/skill> ...]
```
As one plugin:
```
scripts/install-skill-from-github.py --repo <owner>/<repo> --path <dir>/<skill-a> <dir>/<skill-b> --as-plugin [--plugin-name <name>]
```
`--as-plugin` packages the skills into a plugin bundle and hands it to
`dim plugin install`, so the bundle goes through the same manifest check,
conflict check and provenance the plugin market uses. Upgrading it later is a
reinstall: `dim plugin install <source> --overwrite`.
## Communication

@@ -43,2 +107,3 @@

- Example (experimental skill): `scripts/install-skill-from-github.py --repo openai/skills --path skills/.experimental/<skill-name>`
- Example (collection as a plugin): `scripts/install-skill-from-github.py --repo vercel-labs/agent-skills --path web/better-ui web/better-colors --as-plugin`

@@ -50,4 +115,5 @@ ## Behavior and Options

- Aborts if the destination skill directory already exists.
- Installs into `$AGENTS_HOME/skills/<skill-name>` (defaults to `~/.agents/skills`).
- Multiple `--path` values install multiple skills in one run, each named from the path basename unless `--name` is supplied.
- Installs into `<dimcodeHome>/skills/<skill-name>` (default `~/.dimcode/v2/skills`), which is where Dim keeps the skills it manages. `--shared` installs into `~/.agents/skills` instead, so other agents can see the skill — at the cost of Dim no longer managing its updates.
- `--as-plugin` installs one plugin into `<dimcodeHome>/plugins/<name>` instead of loose skills; `--plugin-name` overrides the derived name and `--overwrite` replaces an existing plugin of that name.
- Multiple `--path` values install multiple skills in one run, each named from its `SKILL.md` unless `--name` is supplied.
- Options: `--ref <ref>` (default `main`), `--dest <path>`, `--method auto|download|git`.

@@ -61,2 +127,3 @@

- The skills at https://github.com/openai/skills/tree/main/skills/.system are preinstalled, so no need to help users install those. If they ask, just explain this. If they insist, you can download and overwrite.
- Installed annotations come from `$AGENTS_HOME/skills`.
- Installed annotations come from every directory the runtime scans — Dim's own skills dir, the shared `~/.agents/skills`, and skills carried by installed plugins — and match on the `name` in `SKILL.md`, not the directory name.
- **A name clash aborts the install.** Skills resolve by name, so a second skill under an existing name would never load. The script reports which skill already owns the name and where it lives; the user removes or renames it (or drops it from the install) before retrying. Do not work around this by renaming silently.
{
"name": "dimagent-linux-x64",
"version": "0.3.12",
"version": "0.3.15",
"description": "dimagent binary for Linux x64",

@@ -5,0 +5,0 @@ "os": [

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is too big to display