Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ jobs:
- name: Install uv
uses: astral-sh/setup-uv@v4

- name: Check uv.lock is up to date
run: uv lock --check

- name: Install dependencies
run: uv sync --all-extras

Expand Down
7 changes: 4 additions & 3 deletions parallel_web_tools/cli/skills.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,8 @@ def skills_list(output_json: bool) -> None:
@click.option(
"--project",
is_flag=True,
help="Install to .agents/skills in detected project root (default is global install).",
help="Install into the detected project root — .agents/skills, plus .claude/skills "
"when Claude Code is present (default is global install).",
)
@click.option(
"--skill",
Expand Down Expand Up @@ -139,7 +140,7 @@ def skills_install(project: bool, skill_names: tuple[str, ...], output_json: boo
@click.option(
"--project",
is_flag=True,
help="Uninstall from .agents/skills in detected project root (default is global install).",
help="Uninstall from the detected project root's skill directories (default is global install).",
)
@click.option("--json", "output_json", is_flag=True, help="Output as JSON")
def skills_uninstall(project: bool, output_json: bool) -> None:
Expand Down Expand Up @@ -171,7 +172,7 @@ def skills_uninstall(project: bool, output_json: bool) -> None:
@click.option(
"--project",
is_flag=True,
help="Reinstall in .agents/skills in detected project root (default is global install).",
help="Reinstall in the detected project root's skill directories (default is global install).",
)
@click.option(
"--skill",
Expand Down
21 changes: 16 additions & 5 deletions parallel_web_tools/core/skills.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,8 +106,14 @@ def resolve_install_dirs(project: bool, start: Path | None = None) -> list[Path]

``.agents/skills`` is the canonical cross-agent location and always comes first.
Claude Code does not read it — it only discovers skills under ``.claude/skills`` —
so when a Claude Code configuration directory is present we install there too.
Agents that read both (Cursor, for example) de-duplicate by skill name.
so when Claude Code is present we install there too. Agents that read both
(Cursor, for example) de-duplicate by skill name.

Global installs mirror into the Claude Code configuration directory when it
exists; its absence means no Claude Code on this machine. Project installs
mirror into ``<root>/.claude/skills``, created if needed whenever the machine
has Claude Code — a repo missing ``.claude`` just hasn't stored Claude settings
yet, and skipping it would leave the skills invisible to Claude Code there.

An explicit ``PARALLEL_SKILLS_GLOBAL_DIR`` override targets exactly one directory,
on the assumption that a caller naming a path wants only that path written.
Expand All @@ -116,9 +122,14 @@ def resolve_install_dirs(project: bool, start: Path | None = None) -> list[Path]
if not project and os.environ.get(GLOBAL_SKILLS_DIR_ENV):
return [canonical]

claude_config_dir = canonical.parent.parent / ".claude" if project else get_claude_config_dir()
if not claude_config_dir.is_dir():
return [canonical]
if project:
claude_config_dir = canonical.parent.parent / ".claude"
if not claude_config_dir.is_dir() and not get_claude_config_dir().is_dir():
return [canonical]
else:
claude_config_dir = get_claude_config_dir()
if not claude_config_dir.is_dir():
return [canonical]

return _dedupe_dirs([canonical, claude_config_dir / "skills"])

Expand Down
9 changes: 7 additions & 2 deletions scripts/release.sh
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,9 @@ update_version_files() {
sedi "s/parallel-web-tools>=.*/parallel-web-tools>=$new_version/" \
"$PROJECT_ROOT/parallel_web_tools/integrations/bigquery/cloud_function/requirements.txt"

# 4. uv.lock (records this package's own version; stale locks dirty every working tree)
(cd "$PROJECT_ROOT" && uv lock)

# 5. npm/package.json
sedi "s/\"version\": \".*\"/\"version\": \"$npm_version\"/" "$PROJECT_ROOT/npm/package.json"
}
Expand Down Expand Up @@ -161,7 +164,8 @@ git add \
pyproject.toml \
parallel_web_tools/__init__.py \
parallel_web_tools/integrations/bigquery/cloud_function/requirements.txt \
npm/package.json
npm/package.json \
uv.lock

# Commit — if pre-commit hooks modify files (e.g. uv.lock), re-stage and retry
if ! git commit -m "chore: bump version to $NEW_VERSION"; then
Expand All @@ -171,7 +175,8 @@ if ! git commit -m "chore: bump version to $NEW_VERSION"; then
parallel_web_tools/__init__.py \
tests/test_cli.py \
parallel_web_tools/integrations/bigquery/cloud_function/requirements.txt \
npm/package.json
npm/package.json \
uv.lock
# Also stage any lock files updated by hooks
git diff --name-only | xargs -r git add
git commit -m "chore: bump version to $NEW_VERSION"
Expand Down
16 changes: 15 additions & 1 deletion tests/test_skills.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,14 +110,28 @@ def test_project_adds_claude_dir_when_present(self, tmp_path):
project_root / ".claude" / "skills",
]

def test_project_ignores_claude_config_dir_env(self, monkeypatch, tmp_path):
def test_project_mirrors_locally_when_machine_has_claude(self, monkeypatch, tmp_path):
"""A repo without .claude still gets one when Claude Code exists on the machine,
and the mirror stays project-local — never the global config directory."""
project_root = tmp_path / "repo"
project_root.mkdir()
(project_root / "pyproject.toml").write_text("[project]\nname='x'\n")
global_claude = tmp_path / "home" / ".claude"
global_claude.mkdir(parents=True)
monkeypatch.setenv(skills.CLAUDE_CONFIG_DIR_ENV, str(global_claude))

assert skills.resolve_install_dirs(project=True, start=project_root) == [
project_root / ".agents" / "skills",
project_root / ".claude" / "skills",
]

def test_project_skips_claude_dir_when_machine_has_no_claude(self, monkeypatch, tmp_path):
monkeypatch.delenv(skills.CLAUDE_CONFIG_DIR_ENV, raising=False)
monkeypatch.setattr("parallel_web_tools.core.skills.Path.home", lambda: tmp_path / "home")
project_root = tmp_path / "repo"
project_root.mkdir()
(project_root / "pyproject.toml").write_text("[project]\nname='x'\n")

assert skills.resolve_install_dirs(project=True, start=project_root) == [project_root / ".agents" / "skills"]

def test_symlinked_claude_dir_is_deduped(self, monkeypatch, tmp_path):
Expand Down
2 changes: 1 addition & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading