From 7b4b01ca5da0264a6bda715d3fa3937097685e1b Mon Sep 17 00:00:00 2001 From: Yuchen Xiao Date: Mon, 4 May 2026 13:27:29 -0400 Subject: [PATCH 1/3] feat: deploy cmi agent skills via `diffpy.app agentify` --- news/agentify.rst | 23 +++++++ src/diffpy/apps/app_agentify.py | 35 ++++++++++ src/diffpy/apps/apps.py | 25 ++++++++ tests/test_agentify.py | 110 ++++++++++++++++++++++++++++++++ 4 files changed, 193 insertions(+) create mode 100644 news/agentify.rst create mode 100644 src/diffpy/apps/app_agentify.py create mode 100644 tests/test_agentify.py diff --git a/news/agentify.rst b/news/agentify.rst new file mode 100644 index 0000000..d02b81b --- /dev/null +++ b/news/agentify.rst @@ -0,0 +1,23 @@ +**Added:** + +* Add "agentify" app to deploy ``diffpy.cmi`` agent skills. + +**Changed:** + +* + +**Deprecated:** + +* + +**Removed:** + +* + +**Fixed:** + +* + +**Security:** + +* diff --git a/src/diffpy/apps/app_agentify.py b/src/diffpy/apps/app_agentify.py new file mode 100644 index 0000000..4695111 --- /dev/null +++ b/src/diffpy/apps/app_agentify.py @@ -0,0 +1,35 @@ +import shutil +import subprocess +import tempfile +from pathlib import Path + +REPO_URL = "https://github.com/diffpy/cmi-agent-skills" +DIR_NAME = "cmi-skill" + + +def agentify(args): + agent = args.agent + system_flag = args.system + if agent == "claude": + skills_dir = ".claude/skills" + elif agent == "codex": + skills_dir = ".codex/skills" + if system_flag: + destination = Path().home() / skills_dir / DIR_NAME + else: + destination = Path().cwd() / skills_dir / DIR_NAME + if destination.exists() and not args.update: + raise FileExistsError( + f"Agentic skill {DIR_NAME} already exists at {destination}. " + "To overwrite, pass '--update' flag to update the skill" + ) + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + subprocess.run( + ["git", "clone", REPO_URL, str(tmp_path)], + check=True, + ) + if destination.exists(): + shutil.rmtree(destination) + shutil.copytree(tmp_path / DIR_NAME, destination, dirs_exist_ok=True) + print(f"Agentic skill {DIR_NAME} has been deployed to {destination}") diff --git a/src/diffpy/apps/apps.py b/src/diffpy/apps/apps.py index 95ae493..9a77d63 100644 --- a/src/diffpy/apps/apps.py +++ b/src/diffpy/apps/apps.py @@ -1,5 +1,6 @@ import argparse +from diffpy.apps.app_agentify import agentify from diffpy.apps.app_runmacro import runmacro from diffpy.apps.version import __version__ # noqa @@ -36,6 +37,7 @@ def main(): title="Available applications", dest="application", ) + # runmacro application runmacro_parser = apps_parsers.add_parser( "runmacro", help="Run a macro `<.dp-in>` file", @@ -46,6 +48,29 @@ def main(): help="Path to the `<.dp-in>` macro file to be run", ) runmacro_parser.set_defaults(func=runmacro) + # agent application + agentify_parser = apps_parsers.add_parser( + "agentify", + help="Deploy diffpy.cmi agentic skills in the local environment.", + ) + agentify_parser.add_argument( + "--agent", + "-a", + help="The agent to use for the agentic skill.", + default="claude", + choices=["claude", "codex"], + ) + agentify_parser.add_argument( + "--update", + action="store_true", + help="When set, update the existing agentic skill.", + ) + agentify_parser.add_argument( + "--system", + action="store_true", + help="When set, deploy the agentic skill to the system directory.", + ) + agentify_parser.set_defaults(func=agentify) args = parser.parse_args() if args.application is None: parser.print_help() diff --git a/tests/test_agentify.py b/tests/test_agentify.py new file mode 100644 index 0000000..cb29115 --- /dev/null +++ b/tests/test_agentify.py @@ -0,0 +1,110 @@ +import re +import tempfile +from pathlib import Path +from types import SimpleNamespace +from unittest import mock + +import pytest + +from diffpy.apps.app_agentify import agentify + + +@pytest.mark.parametrize( + "args, expected_scope, expected_skill_dir", + [ + # C1: diffpy.apps agentify + # Deploys workspace claude skill. + # Expect skill folder is created in the current working directory. + ( + SimpleNamespace( + agent="claude", + system=False, + update=False, + ), + "cwd", + ".claude/skills/cmi-skill", + ), + # C2: diffpy.apps agentify --system + # Deploys system claude skill. + # Expect skill folder is created in the user's home directory. + ( + SimpleNamespace( + agent="claude", + system=True, + update=False, + ), + "home", + ".claude/skills/cmi-skill", + ), + # C3: diffpy.apps agentify --agent codex + # Deploys workspace codex skill. + # Expect skill folder is created in the current working directory. + ( + SimpleNamespace( + agent="codex", + system=False, + update=False, + ), + "cwd", + ".codex/skills/cmi-skill", + ), + # C4: diffpy.apps agentify --agent codex --system + # Deploys system codex skill. + # Expect skill folder is created in the user's home directory. + ( + SimpleNamespace( + agent="codex", + system=True, + update=False, + ), + "home", + ".codex/skills/cmi-skill", + ), + ], +) +def test_agentify(args, expected_scope, expected_skill_dir): + with tempfile.TemporaryDirectory() as tmp: + with ( + mock.patch.object(Path, "home", return_value=Path(tmp) / "home"), + mock.patch.object(Path, "cwd", return_value=Path(tmp) / "cwd"), + ): + agentify(args) + expected_path = Path(tmp) / expected_scope / expected_skill_dir + assert expected_path.exists() + + +def test_agentify_update(): + with tempfile.TemporaryDirectory() as tmp: + with ( + mock.patch.object(Path, "home", return_value=Path(tmp) / "home"), + mock.patch.object(Path, "cwd", return_value=Path(tmp) / "cwd"), + ): + # C1: Deploy again without --update flag when skill already exists. + # Expect FileExistsError to be raised, and the error message + # matches. + args = SimpleNamespace( + agent="claude", + system=False, + update=False, + ) + agentify(args) + skill_path = Path(tmp) / "cwd" / ".claude" / "skills" / "cmi-skill" + assert skill_path.exists() + args.update = True + agentify(args) + pytest.raises( + FileExistsError, + match=re.escape( + f"Agentic skill cmi-skill already exists at {skill_path}. " + "To overwrite, pass '--update' flag to update the skill" + ), + ) + # C1: Deploy again with --update flag when skill already exists + # with a dummy file in the skill directory. + # Expect no error to be raised, and the skill is updated. + dummy_file = skill_path / "dummy.txt" + dummy_file.touch() + assert dummy_file.exists() + args.update = True + agentify(args) + assert not dummy_file.exists() From f6da7ca5b63fe2d6a48a664f5fbd89e728bf924c Mon Sep 17 00:00:00 2001 From: Yuchen Xiao Date: Mon, 4 May 2026 13:37:57 -0400 Subject: [PATCH 2/3] docs: update instructions for `diffpy.app agentify` --- README.rst | 1 + docs/source/getting-started.rst | 31 +++++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/README.rst b/README.rst index 856ac0f..5249337 100644 --- a/README.rst +++ b/README.rst @@ -40,6 +40,7 @@ User applications to help with tasks using diffpy packages. Currently it contains - `runmacro`: A runner for DiffPy macro files. +- `agentify`: A deployer for diffpy.cmi agentic skills. For more information about the diffpy.apps library, please consult our `online documentation `_. diff --git a/docs/source/getting-started.rst b/docs/source/getting-started.rst index 077bc0e..b699cd8 100644 --- a/docs/source/getting-started.rst +++ b/docs/source/getting-started.rst @@ -13,6 +13,7 @@ diffpy packages. This page contains the instructions for all applications available, including: - :ref:`runmacro` +- :ref:`agentify` .. _runmacro: @@ -149,3 +150,33 @@ starting point for the refinement. constraint. e.g. Here, lattice parameter ``a=b=c``, and ``Usio_0=Uiso_i, i=1,2,3``, ``a`` and ``Uiso_0`` are used as the reference variables. + +.. _agentify: +Use ``agentify`` to deploy agent skills ``diffpy.cmi`` +------------------------------------------------------ + +The ``agentify`` application allows users to deploy agentic skills in the +local environment. To use this application, run: + +.. code-block:: bash + + diffpy.app agentify + +``claude`` and ``codex`` agent skills are supported, and ``claude`` is used +by default. To specify the agent skill, use the ``--agent`` option: + +.. code-block:: bash + + diffpy.app agentify --agent codex + +To deploy the agentic skill to the system directory, use the ``--system`` flag: + +.. code-block:: bash + + diffpy.app agentify --system + +To update the existing ``diffpy.cmi`` agentic skill, use the ``--update`` flag: + +.. code-block:: bash + + diffpy.app agentify --update From 52a35d444165238638a29d0d0a2ed7b37f1049da Mon Sep 17 00:00:00 2001 From: Yuchen Xiao Date: Mon, 4 May 2026 13:43:44 -0400 Subject: [PATCH 3/3] chore: correct typos --- src/diffpy/apps/apps.py | 3 +-- tests/test_agentify.py | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/diffpy/apps/apps.py b/src/diffpy/apps/apps.py index 9a77d63..253ede7 100644 --- a/src/diffpy/apps/apps.py +++ b/src/diffpy/apps/apps.py @@ -48,14 +48,13 @@ def main(): help="Path to the `<.dp-in>` macro file to be run", ) runmacro_parser.set_defaults(func=runmacro) - # agent application + # agentify application agentify_parser = apps_parsers.add_parser( "agentify", help="Deploy diffpy.cmi agentic skills in the local environment.", ) agentify_parser.add_argument( "--agent", - "-a", help="The agent to use for the agentic skill.", default="claude", choices=["claude", "codex"], diff --git a/tests/test_agentify.py b/tests/test_agentify.py index cb29115..ef2ea6f 100644 --- a/tests/test_agentify.py +++ b/tests/test_agentify.py @@ -99,7 +99,7 @@ def test_agentify_update(): "To overwrite, pass '--update' flag to update the skill" ), ) - # C1: Deploy again with --update flag when skill already exists + # C2: Deploy again with --update flag when skill already exists # with a dummy file in the skill directory. # Expect no error to be raised, and the skill is updated. dummy_file = skill_path / "dummy.txt"