Skip to content

Commit bca7bcc

Browse files
kasperjungeclaude
andcommitted
Add global scope, config CLI, and Win locks
Introduce a structured config command namespace with tools and default-tool management while keeping `agr tools` as a deprecated alias. Add `--global/-g` support for add/remove/list/sync so users can manage skills in `~/.agr/agr.toml` and tool global directories outside git repositories. Extend fetcher/install helpers to support explicit skills directories for global installs and store global local dependencies as absolute paths for stable sync/remove behavior. Replace SDK cache file locking with a cross-platform backend that uses `msvcrt` on Windows and `fcntl` on POSIX. Update docs and changelog and add coverage for config commands, global flag flows, and cache lock backends. Co-Authored-By: Claude <noreply@anthropic.com>
1 parent eee3ca4 commit bca7bcc

17 files changed

Lines changed: 1117 additions & 234 deletions

File tree

CHANGELOG.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,24 @@
22

33
## [Unreleased]
44

5+
### Added
6+
- `agr config` command group with `tools` and `default-tool` subcommands
7+
- Global scope flags (`-g`, `--global`) for `agr add`, `agr remove`, `agr sync`, and `agr list`
8+
- Global config helpers for `~/.agr/agr.toml`
9+
- Cross-platform cache lock backends in SDK cache (`msvcrt` on Windows, `fcntl` on POSIX)
10+
- CLI tests for `agr config` commands and global flag flows
11+
- SDK cache tests for Windows and POSIX lock wrappers
12+
13+
### Changed
14+
- `agr tools` now behaves as a deprecated alias of `agr config tools` with a warning
15+
- Fetch/install helpers now support global installs via explicit skills directory overrides
16+
- README and reference docs now document `agr config` and global install/list/sync/remove commands
17+
- Docs tests now validate `agr config` command documentation
18+
19+
### Fixed
20+
- Global local dependencies are stored as absolute paths so `sync -g` and `remove -g` work reliably across directories
21+
- SDK cache no longer hard-depends on Unix-only `fcntl` lock calls
22+
523
## [0.7.4] - 2026-02-06
624
### Added
725
- Antigravity tool support with workspace `.agent/skills/` and global `~/.gemini/antigravity/skills/` paths

README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ That's it. The skill is now available in your configured tool (Claude Code, Code
5252

5353
```bash
5454
agr add anthropics/skills/frontend-design # Install a skill
55+
agr add -g anthropics/skills/frontend-design # Install globally for your user
5556
agr add anthropics/skills/pdf anthropics/skills/mcp-builder # Install multiple
5657
agr add anthropics/skills/pdf --source github # Install from an explicit source
5758
```
@@ -173,11 +174,17 @@ agr init --migrate
173174
| Command | Description |
174175
|---------|-------------|
175176
| `agr add <handle>` | Install a skill |
177+
| `agr add -g <handle>` | Install a skill globally |
176178
| `agr remove <handle>` | Uninstall a skill |
179+
| `agr remove -g <handle>` | Uninstall a global skill |
177180
| `agr sync` | Install all from agr.toml |
181+
| `agr sync -g` | Sync global dependencies |
178182
| `agr list` | Show installed skills |
183+
| `agr list -g` | Show global skills |
179184
| `agr init` | Create agr.toml |
180185
| `agr init <name>` | Create a new skill |
186+
| `agr config tools ...` | Manage configured tools |
187+
| `agr config default-tool ...` | Manage agrx default tool |
181188
| `agrx <handle>` | Run skill temporarily |
182189

183190
---

agr/commands/add.py

Lines changed: 40 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,15 @@
22

33
from rich.console import Console
44

5-
from agr.config import AgrConfig, Dependency, find_config, find_repo_root
5+
from pathlib import Path
6+
7+
from agr.config import (
8+
AgrConfig,
9+
Dependency,
10+
find_config,
11+
find_repo_root,
12+
get_or_create_global_config,
13+
)
614
from agr.exceptions import AgrError, InvalidHandleError
715
from agr.fetcher import fetch_and_install_to_tools
816
from agr.handle import parse_handle
@@ -11,31 +19,41 @@
1119

1220

1321
def run_add(
14-
refs: list[str], overwrite: bool = False, source: str | None = None
22+
refs: list[str],
23+
overwrite: bool = False,
24+
source: str | None = None,
25+
global_install: bool = False,
1526
) -> None:
1627
"""Run the add command.
1728
1829
Args:
1930
refs: List of handles or paths to add
2031
overwrite: Whether to overwrite existing skills
2132
"""
22-
# Find repo root
23-
repo_root = find_repo_root()
24-
if repo_root is None:
25-
console.print("[red]Error:[/red] Not in a git repository")
26-
raise SystemExit(1)
27-
28-
# Find or create config
29-
config_path = find_config()
30-
if config_path is None:
31-
config_path = repo_root / "agr.toml"
32-
config = AgrConfig()
33+
skills_dirs: dict[str, Path] | None = None
34+
if global_install:
35+
repo_root = None
36+
config_path, config = get_or_create_global_config()
3337
else:
34-
config = AgrConfig.load(config_path)
38+
# Find repo root
39+
repo_root = find_repo_root()
40+
if repo_root is None:
41+
console.print("[red]Error:[/red] Not in a git repository")
42+
raise SystemExit(1)
43+
44+
# Find or create config
45+
config_path = find_config()
46+
if config_path is None:
47+
config_path = repo_root / "agr.toml"
48+
config = AgrConfig()
49+
else:
50+
config = AgrConfig.load(config_path)
3551

3652
# Get configured tools
3753
tools = config.get_tools()
3854
resolver = config.get_source_resolver()
55+
if global_install:
56+
skills_dirs = {tool.name: tool.get_global_skills_dir() for tool in tools}
3957

4058
# Track results for summary
4159
results: list[tuple[str, bool, str]] = [] # (ref, success, message)
@@ -60,17 +78,24 @@ def run_add(
6078
overwrite,
6179
resolver=resolver,
6280
source=source,
81+
skills_dirs=skills_dirs,
6382
)
6483
installed_paths = [
6584
f"{name}: {path}" for name, path in installed_paths_dict.items()
6685
]
6786

6887
# Add to config
6988
if handle.is_local:
89+
path_value = ref
90+
if global_install and handle.local_path is not None:
91+
if handle.local_path.is_absolute():
92+
path_value = str(handle.local_path.resolve())
93+
else:
94+
path_value = str((Path.cwd() / handle.local_path).resolve())
7095
config.add_dependency(
7196
Dependency(
7297
type="skill",
73-
path=ref,
98+
path=path_value,
7499
)
75100
)
76101
else:

agr/commands/list.py

Lines changed: 37 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from rich.console import Console
66
from rich.table import Table
77

8-
from agr.config import AgrConfig, find_config, find_repo_root
8+
from agr.config import AgrConfig, find_config, find_repo_root, get_global_config_path
99
from agr.fetcher import is_skill_installed
1010
from agr.handle import ParsedHandle, parse_handle
1111
from agr.tool import ToolConfig
@@ -15,9 +15,10 @@
1515

1616
def _get_installation_status(
1717
handle: ParsedHandle,
18-
repo_root: Path,
18+
repo_root: Path | None,
1919
tools: list[ToolConfig],
2020
source: str | None = None,
21+
skills_dirs: dict[str, Path] | None = None,
2122
) -> str:
2223
"""Get installation status across all configured tools.
2324
@@ -32,7 +33,13 @@ def _get_installation_status(
3233
installed_tools = [
3334
tool.name
3435
for tool in tools
35-
if is_skill_installed(handle, repo_root, tool, source)
36+
if is_skill_installed(
37+
handle,
38+
repo_root,
39+
tool,
40+
source,
41+
skills_dir=skills_dirs.get(tool.name) if skills_dirs is not None else None,
42+
)
3643
]
3744

3845
if len(installed_tools) == len(tools):
@@ -43,26 +50,37 @@ def _get_installation_status(
4350
return "[yellow]not synced[/yellow]"
4451

4552

46-
def run_list() -> None:
53+
def run_list(global_install: bool = False) -> None:
4754
"""Run the list command.
4855
4956
Lists all dependencies from agr.toml with their sync status.
5057
"""
51-
# Find repo root
52-
repo_root = find_repo_root()
53-
if repo_root is None:
54-
console.print("[red]Error:[/red] Not in a git repository")
55-
raise SystemExit(1)
56-
57-
# Find config
58-
config_path = find_config()
59-
if config_path is None:
60-
console.print("[yellow]No agr.toml found.[/yellow]")
61-
console.print("[dim]Run 'agr init' to create one.[/dim]")
62-
return
58+
skills_dirs: dict[str, Path] | None = None
59+
if global_install:
60+
repo_root = None
61+
config_path = get_global_config_path()
62+
if not config_path.exists():
63+
console.print("[yellow]No global agr.toml found.[/yellow]")
64+
console.print("[dim]Run 'agr add -g <handle>' to create one.[/dim]")
65+
return
66+
else:
67+
# Find repo root
68+
repo_root = find_repo_root()
69+
if repo_root is None:
70+
console.print("[red]Error:[/red] Not in a git repository")
71+
raise SystemExit(1)
72+
73+
# Find config
74+
config_path = find_config()
75+
if config_path is None:
76+
console.print("[yellow]No agr.toml found.[/yellow]")
77+
console.print("[dim]Run 'agr init' to create one.[/dim]")
78+
return
6379

6480
config = AgrConfig.load(config_path)
6581
tools = config.get_tools()
82+
if global_install:
83+
skills_dirs = {tool.name: tool.get_global_skills_dir() for tool in tools}
6684

6785
if not config.dependencies:
6886
console.print("[yellow]No dependencies in agr.toml.[/yellow]")
@@ -96,7 +114,9 @@ def run_list() -> None:
96114
source_name = (
97115
None if dep.is_local else (dep.source or config.default_source)
98116
)
99-
status = _get_installation_status(handle, repo_root, tools, source_name)
117+
status = _get_installation_status(
118+
handle, repo_root, tools, source_name, skills_dirs
119+
)
100120
except Exception:
101121
status = "[red]invalid[/red]"
102122

agr/commands/remove.py

Lines changed: 65 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,54 @@
11
"""agr remove command implementation."""
22

3+
from pathlib import Path
4+
35
from rich.console import Console
46

5-
from agr.config import AgrConfig, find_config, find_repo_root
7+
from agr.config import (
8+
AgrConfig,
9+
find_config,
10+
find_repo_root,
11+
get_global_config_path,
12+
)
613
from agr.fetcher import uninstall_skill
714
from agr.handle import parse_handle
815

916
console = Console()
1017

1118

12-
def run_remove(refs: list[str]) -> None:
19+
def run_remove(refs: list[str], global_install: bool = False) -> None:
1320
"""Run the remove command.
1421
1522
Args:
1623
refs: List of handles or paths to remove
1724
"""
18-
# Find repo root
19-
repo_root = find_repo_root()
20-
if repo_root is None:
21-
console.print("[red]Error:[/red] Not in a git repository")
22-
raise SystemExit(1)
23-
24-
# Find config
25-
config_path = find_config()
26-
if config_path is None:
27-
console.print("[red]Error:[/red] No agr.toml found")
28-
raise SystemExit(1)
25+
skills_dirs: dict[str, Path] | None = None
26+
if global_install:
27+
repo_root = None
28+
config_path = get_global_config_path()
29+
if not config_path.exists():
30+
console.print("[red]Error:[/red] No global agr.toml found")
31+
console.print("[dim]Run 'agr add -g <handle>' first.[/dim]")
32+
raise SystemExit(1)
33+
else:
34+
# Find repo root
35+
repo_root = find_repo_root()
36+
if repo_root is None:
37+
console.print("[red]Error:[/red] Not in a git repository")
38+
raise SystemExit(1)
39+
40+
# Find config
41+
config_path = find_config()
42+
if config_path is None:
43+
console.print("[red]Error:[/red] No agr.toml found")
44+
raise SystemExit(1)
2945

3046
config = AgrConfig.load(config_path)
3147

3248
# Get configured tools
3349
tools = config.get_tools()
50+
if global_install:
51+
skills_dirs = {tool.name: tool.get_global_skills_dir() for tool in tools}
3452

3553
# Track results
3654
results: list[tuple[str, bool, str]] = []
@@ -43,6 +61,18 @@ def run_remove(refs: list[str]) -> None:
4361
dep = config.get_by_identifier(ref)
4462
if dep is None and handle.is_local:
4563
dep = config.get_by_identifier(str(handle.local_path))
64+
if (
65+
dep is None
66+
and global_install
67+
and handle.is_local
68+
and handle.local_path is not None
69+
):
70+
absolute_path = (
71+
handle.local_path.resolve()
72+
if handle.local_path.is_absolute()
73+
else (Path.cwd() / handle.local_path).resolve()
74+
)
75+
dep = config.get_by_identifier(str(absolute_path))
4676
if dep is None and not handle.is_local:
4777
dep = config.get_by_identifier(handle.to_toml_handle())
4878

@@ -53,7 +83,16 @@ def run_remove(refs: list[str]) -> None:
5383
# Remove from filesystem for all configured tools
5484
removed_fs = False
5585
for tool in tools:
56-
if uninstall_skill(handle, repo_root, tool, source_name):
86+
target_skills_dir = (
87+
skills_dirs.get(tool.name) if skills_dirs is not None else None
88+
)
89+
if uninstall_skill(
90+
handle,
91+
repo_root,
92+
tool,
93+
source_name,
94+
skills_dir=target_skills_dir,
95+
):
5796
removed_fs = True
5897

5998
# Remove from config
@@ -62,6 +101,18 @@ def run_remove(refs: list[str]) -> None:
62101
if not removed_config and handle.is_local:
63102
# Try with the path
64103
removed_config = config.remove_dependency(str(handle.local_path))
104+
if (
105+
not removed_config
106+
and global_install
107+
and handle.is_local
108+
and handle.local_path is not None
109+
):
110+
absolute_path = (
111+
handle.local_path.resolve()
112+
if handle.local_path.is_absolute()
113+
else (Path.cwd() / handle.local_path).resolve()
114+
)
115+
removed_config = config.remove_dependency(str(absolute_path))
65116
if not removed_config:
66117
# Try with toml handle
67118
removed_config = config.remove_dependency(handle.to_toml_handle())

0 commit comments

Comments
 (0)