echomodel 0.2.1__tar.gz
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.
- echomodel-0.2.1/PKG-INFO +12 -0
- echomodel-0.2.1/README.md +147 -0
- echomodel-0.2.1/pyproject.toml +31 -0
- echomodel-0.2.1/setup.cfg +4 -0
- echomodel-0.2.1/src/aicfg/__init__.py +0 -0
- echomodel-0.2.1/src/aicfg/cli/__init__.py +24 -0
- echomodel-0.2.1/src/aicfg/cli/commands.py +185 -0
- echomodel-0.2.1/src/aicfg/cli/context.py +183 -0
- echomodel-0.2.1/src/aicfg/cli/servers.py +143 -0
- echomodel-0.2.1/src/aicfg/cli/sessions.py +54 -0
- echomodel-0.2.1/src/aicfg/cli/settings.py +144 -0
- echomodel-0.2.1/src/aicfg/cli/skills.py +294 -0
- echomodel-0.2.1/src/aicfg/mcp/__init__.py +0 -0
- echomodel-0.2.1/src/aicfg/mcp/server.py +340 -0
- echomodel-0.2.1/src/aicfg/sdk/__init__.py +0 -0
- echomodel-0.2.1/src/aicfg/sdk/commands.py +171 -0
- echomodel-0.2.1/src/aicfg/sdk/config.py +91 -0
- echomodel-0.2.1/src/aicfg/sdk/context.py +475 -0
- echomodel-0.2.1/src/aicfg/sdk/mcp_setup.py +305 -0
- echomodel-0.2.1/src/aicfg/sdk/sessions.py +263 -0
- echomodel-0.2.1/src/aicfg/sdk/settings.py +175 -0
- echomodel-0.2.1/src/aicfg/sdk/skills.py +1210 -0
- echomodel-0.2.1/src/aicfg/sdk/utils.py +79 -0
- echomodel-0.2.1/src/echomodel.egg-info/PKG-INFO +12 -0
- echomodel-0.2.1/src/echomodel.egg-info/SOURCES.txt +42 -0
- echomodel-0.2.1/src/echomodel.egg-info/dependency_links.txt +1 -0
- echomodel-0.2.1/src/echomodel.egg-info/entry_points.txt +5 -0
- echomodel-0.2.1/src/echomodel.egg-info/requires.txt +8 -0
- echomodel-0.2.1/src/echomodel.egg-info/top_level.txt +1 -0
- echomodel-0.2.1/tests/test_allowed_tools_config.py +25 -0
- echomodel-0.2.1/tests/test_context_assist.py +242 -0
- echomodel-0.2.1/tests/test_context_eval.py +194 -0
- echomodel-0.2.1/tests/test_included_paths_config.py +17 -0
- echomodel-0.2.1/tests/test_isolated.py +9 -0
- echomodel-0.2.1/tests/test_mcp_add.py +134 -0
- echomodel-0.2.1/tests/test_mcp_remove.py +66 -0
- echomodel-0.2.1/tests/test_mcp_servers_list.py +19 -0
- echomodel-0.2.1/tests/test_mcp_servers_modify.py +114 -0
- echomodel-0.2.1/tests/test_settings_read.py +106 -0
- echomodel-0.2.1/tests/test_settings_set.py +63 -0
- echomodel-0.2.1/tests/test_skills.py +836 -0
- echomodel-0.2.1/tests/test_slash_commands_config.py +41 -0
- echomodel-0.2.1/tests/test_slash_commands_namespaced.py +40 -0
- echomodel-0.2.1/tests/test_slash_commands_register.py +24 -0
echomodel-0.2.1/PKG-INFO
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: echomodel
|
|
3
|
+
Version: 0.2.1
|
|
4
|
+
Summary: AI agent ecosystem — cross-platform skill management, MCP tools, and configuration
|
|
5
|
+
Requires-Python: >=3.11
|
|
6
|
+
Requires-Dist: click
|
|
7
|
+
Requires-Dist: toml
|
|
8
|
+
Requires-Dist: rich
|
|
9
|
+
Requires-Dist: pyyaml
|
|
10
|
+
Requires-Dist: mcp
|
|
11
|
+
Provides-Extra: dev
|
|
12
|
+
Requires-Dist: pytest; extra == "dev"
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# aicfg
|
|
2
|
+
|
|
3
|
+
Know at a glance which AI agent skills are installed on this machine,
|
|
4
|
+
for which platform, and which are missing — then fix it with one command.
|
|
5
|
+
|
|
6
|
+
aicfg is a unified tool for managing skills, MCP servers, and
|
|
7
|
+
configuration across Claude Code and Gemini CLI. Register git-hosted
|
|
8
|
+
skill marketplaces, browse what's available, and install to both
|
|
9
|
+
platforms without learning each tool's syntax.
|
|
10
|
+
|
|
11
|
+
## Why
|
|
12
|
+
|
|
13
|
+
The core problem: you track a set of skills you care about across one
|
|
14
|
+
or more marketplaces. On any given machine, some are installed for
|
|
15
|
+
Claude, some for Gemini, some for both, some for neither. Without
|
|
16
|
+
aicfg, answering "what's missing?" means checking two separate
|
|
17
|
+
directories and comparing manually.
|
|
18
|
+
|
|
19
|
+
`aicfg skills list` answers that question instantly — one table showing
|
|
20
|
+
every skill from your registered marketplaces, with install status per
|
|
21
|
+
platform. That's the central value. Everything else follows from it:
|
|
22
|
+
|
|
23
|
+
- **Git-hosted skill marketplaces** — register a repo, browse skills, install by name
|
|
24
|
+
- **One command to install a skill to both platforms** — no need to learn each tool's install syntax
|
|
25
|
+
- **Unified MCP server management** — register, health-check, and list servers across scopes
|
|
26
|
+
- **Context file unification** — keep CLAUDE.md and .gemini/settings.json in sync from shared source files
|
|
27
|
+
|
|
28
|
+
Skills use the [agentskills.io](https://agentskills.io/) open standard. Marketplace repos work natively with `gemini skills install` too.
|
|
29
|
+
|
|
30
|
+
## Quick Start
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
# Install
|
|
34
|
+
pipx install -e . --force
|
|
35
|
+
|
|
36
|
+
# Register a skills marketplace
|
|
37
|
+
aicfg skills marketplace register my/skills https://github.com/YOUR_USERNAME/skills.git
|
|
38
|
+
|
|
39
|
+
# See what's available
|
|
40
|
+
aicfg skills list
|
|
41
|
+
|
|
42
|
+
# Install a skill (to both Claude and Gemini)
|
|
43
|
+
aicfg skills install develop-unit-tests
|
|
44
|
+
|
|
45
|
+
# Install to one platform only
|
|
46
|
+
aicfg skills install nm --platform claude
|
|
47
|
+
|
|
48
|
+
# Publish a local skill to a marketplace
|
|
49
|
+
aicfg skills publish my-skill --marketplace my/skills
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Commands
|
|
53
|
+
|
|
54
|
+
### Skills
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
aicfg skills list # all skills across marketplaces + local
|
|
58
|
+
aicfg skills list --installed any # only installed skills
|
|
59
|
+
aicfg skills list --installed claude # installed on claude
|
|
60
|
+
aicfg skills list --installed none # not installed anywhere
|
|
61
|
+
aicfg skills list --refresh # force marketplace cache refresh
|
|
62
|
+
aicfg skills install <name> # install to all configured platforms
|
|
63
|
+
aicfg skills install <name> --platform claude # install to one platform
|
|
64
|
+
aicfg skills uninstall <name>
|
|
65
|
+
aicfg skills show <name> # full details + status per marketplace
|
|
66
|
+
aicfg skills publish <name> # publish to source marketplace
|
|
67
|
+
aicfg skills publish <name> --marketplace <alias>
|
|
68
|
+
aicfg skills publish <name> --source-path ~/ws/my-skill # from arbitrary dir
|
|
69
|
+
aicfg skills marketplace register <alias> <git-url>
|
|
70
|
+
aicfg skills marketplace list
|
|
71
|
+
aicfg skills marketplace remove <alias>
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Marketplace repos work natively with both aicfg and the Gemini CLI:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
# These are equivalent — same repo, same skill:
|
|
78
|
+
aicfg skills install develop-skill # via aicfg
|
|
79
|
+
gemini skills install <url> --path coding/develop-skill # via Gemini CLI
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Claude Code has no native skill CLI. aicfg copies SKILL.md to `~/.claude/skills/<name>/SKILL.md` directly.
|
|
83
|
+
|
|
84
|
+
### Claude Utilities
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
aicfg claude find-session "deploy" # search recent sessions for keywords
|
|
88
|
+
aicfg claude find-session "error" --most-recent=20
|
|
89
|
+
aicfg claude find-session "deploy" "cloud run" --all # AND match
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### MCP Servers
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
aicfg mcp add --self # register aicfg's own MCP server
|
|
96
|
+
aicfg mcp add --command some-mcp --name my-server
|
|
97
|
+
aicfg mcp add --path /path/to/repo # auto-discover from pyproject.toml
|
|
98
|
+
aicfg mcp list
|
|
99
|
+
aicfg mcp show aicfg # details + health check
|
|
100
|
+
aicfg mcp remove my-server
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### Gemini Slash Commands
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
aicfg cmds list # list with sync status
|
|
107
|
+
aicfg cmds add my-fix "Fix: {{context}}" # create locally
|
|
108
|
+
aicfg cmds publish my-fix # promote to repo registry
|
|
109
|
+
aicfg cmds install commitall # install from registry
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### Context Files
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
aicfg context status # check CLAUDE.md / GEMINI.md state
|
|
116
|
+
aicfg context unify --scope user # merge into shared CONTEXT.md
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### Settings
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
aicfg settings list
|
|
123
|
+
aicfg paths list # context.includeDirectories
|
|
124
|
+
aicfg allowed-tools list # tools.allowed
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## Architecture
|
|
128
|
+
|
|
129
|
+
- **SDK-first** — all logic in `src/aicfg/sdk/`. CLI and MCP server are thin wrappers.
|
|
130
|
+
- **Skills** — standard agentskills.io format, copied as-is from git-hosted marketplaces. No transformation.
|
|
131
|
+
- **Marketplace cache** — `~/.cache/ai-common/skills/marketplaces/`. Cloned without `.git`, 5-minute TTL.
|
|
132
|
+
- **Scope convention** — `user` = `~/.gemini/settings.json`, `project` = `./.gemini/settings.json`
|
|
133
|
+
- **Operational transparency** — operations like `publish` that perform multi-step git workflows include a full log of every command executed, with exit codes and output. You always get structured results for programmatic use, plus verifiable evidence of what actually happened under the hood.
|
|
134
|
+
|
|
135
|
+
## Development
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
make test # run unit tests (network blocked, isolated via env vars)
|
|
139
|
+
make install # pipx install in editable mode
|
|
140
|
+
make clean # remove build artifacts
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## Prerequisites
|
|
144
|
+
|
|
145
|
+
- Python 3.10+
|
|
146
|
+
- pipx
|
|
147
|
+
- Claude Code and/or Gemini CLI
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "echomodel"
|
|
7
|
+
version = "0.2.1"
|
|
8
|
+
description = "AI agent ecosystem — cross-platform skill management, MCP tools, and configuration"
|
|
9
|
+
requires-python = ">=3.11"
|
|
10
|
+
dependencies = [
|
|
11
|
+
"click",
|
|
12
|
+
"toml",
|
|
13
|
+
"rich",
|
|
14
|
+
"pyyaml",
|
|
15
|
+
"mcp",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
[project.optional-dependencies]
|
|
19
|
+
dev = ["pytest"]
|
|
20
|
+
|
|
21
|
+
[project.scripts]
|
|
22
|
+
em = "aicfg.cli:cli"
|
|
23
|
+
echomodel = "aicfg.cli:cli"
|
|
24
|
+
aicfg = "aicfg.cli:cli"
|
|
25
|
+
aicfg-mcp = "aicfg.mcp.server:run_server"
|
|
26
|
+
|
|
27
|
+
[tool.setuptools.packages.find]
|
|
28
|
+
where = ["src"]
|
|
29
|
+
|
|
30
|
+
[tool.setuptools.package-data]
|
|
31
|
+
aicfg = ["*.yaml"]
|
|
File without changes
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import click
|
|
2
|
+
from aicfg.cli.commands import cmds
|
|
3
|
+
from aicfg.cli.context import context_cli
|
|
4
|
+
from aicfg.cli.settings import paths, settings, allowed_tools
|
|
5
|
+
from aicfg.cli.servers import mcp_servers
|
|
6
|
+
from aicfg.cli.sessions import claude_cli
|
|
7
|
+
from aicfg.cli.skills import skills
|
|
8
|
+
|
|
9
|
+
@click.group()
|
|
10
|
+
def cli():
|
|
11
|
+
"""AI Config Manager (aicfg)"""
|
|
12
|
+
pass
|
|
13
|
+
|
|
14
|
+
cli.add_command(cmds)
|
|
15
|
+
cli.add_command(context_cli)
|
|
16
|
+
cli.add_command(paths)
|
|
17
|
+
cli.add_command(settings)
|
|
18
|
+
cli.add_command(allowed_tools)
|
|
19
|
+
cli.add_command(mcp_servers)
|
|
20
|
+
cli.add_command(claude_cli)
|
|
21
|
+
cli.add_command(skills)
|
|
22
|
+
|
|
23
|
+
if __name__ == "__main__":
|
|
24
|
+
cli()
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import click
|
|
2
|
+
import toml
|
|
3
|
+
import json
|
|
4
|
+
from rich.console import Console
|
|
5
|
+
from rich.table import Table
|
|
6
|
+
from rich import print as rprint
|
|
7
|
+
from aicfg.sdk import commands as sdk
|
|
8
|
+
|
|
9
|
+
console = Console()
|
|
10
|
+
|
|
11
|
+
@click.group()
|
|
12
|
+
def cmds():
|
|
13
|
+
"""Manage Gemini slash commands (TOML files)."""
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
@cmds.command()
|
|
17
|
+
@click.argument("name")
|
|
18
|
+
@click.argument("prompt", required=False)
|
|
19
|
+
@click.option("--desc", "-d", help="Description of the command")
|
|
20
|
+
@click.option("--scope", type=click.Choice(["user", "project"]),
|
|
21
|
+
default="user", help="Where to create the command (default: user)")
|
|
22
|
+
@click.option("--namespace", "-ns", help="Optional namespace (subdirectory) for the command")
|
|
23
|
+
def add(name, prompt, desc, scope, namespace):
|
|
24
|
+
"""Add a new command."""
|
|
25
|
+
if not prompt:
|
|
26
|
+
description = desc or "My custom command"
|
|
27
|
+
default_content = (
|
|
28
|
+
f'description = "{description}"\n'
|
|
29
|
+
'prompt = """\n'
|
|
30
|
+
'Write your prompt here...\n'
|
|
31
|
+
'"""'
|
|
32
|
+
)
|
|
33
|
+
edited = click.edit(default_content, extension=".toml")
|
|
34
|
+
if not edited:
|
|
35
|
+
rprint("[red]Aborted.[/red] No content provided.")
|
|
36
|
+
return
|
|
37
|
+
try:
|
|
38
|
+
data = toml.loads(edited)
|
|
39
|
+
prompt = data.get("prompt")
|
|
40
|
+
desc = data.get("description", desc)
|
|
41
|
+
except Exception as e:
|
|
42
|
+
rprint(f"[red]Invalid TOML:[/red] {e}")
|
|
43
|
+
return
|
|
44
|
+
|
|
45
|
+
path = sdk.add_command(name, prompt, desc, scope=scope, namespace=namespace)
|
|
46
|
+
rprint(f"[green]Created[/green] {path} (Scope: [bold]{scope.upper()}[/bold])")
|
|
47
|
+
|
|
48
|
+
@cmds.command("list")
|
|
49
|
+
@click.option("--json", "as_json", is_flag=True, help="Output as JSON.")
|
|
50
|
+
@click.option("--format", type=click.Choice(["text", "json"]),
|
|
51
|
+
default="text", help="Output format.")
|
|
52
|
+
@click.option("--filter", "filter_pattern", help="Filter by name (supports wildcards e.g. 'commit*')")
|
|
53
|
+
@click.option("--scope", "scopes", multiple=True, type=click.Choice(["user", "registry", "project"]),
|
|
54
|
+
help="Filter by scope (can specify multiple)")
|
|
55
|
+
def list_cmds(as_json, format, filter_pattern, scopes):
|
|
56
|
+
"""List all commands. Optional filters for name and scope."""
|
|
57
|
+
|
|
58
|
+
# Convert tuple to list for SDK, or None if empty (implies all)
|
|
59
|
+
scope_list = list(scopes) if scopes else None
|
|
60
|
+
|
|
61
|
+
results = sdk.list_commands(filter_pattern=filter_pattern, scopes=scope_list)
|
|
62
|
+
results = sorted(results, key=lambda x: x["name"])
|
|
63
|
+
|
|
64
|
+
if as_json or format == "json":
|
|
65
|
+
console.print_json(data=results)
|
|
66
|
+
return
|
|
67
|
+
|
|
68
|
+
table = Table(title="Custom Slash Commands")
|
|
69
|
+
table.add_column("Command", style="cyan")
|
|
70
|
+
|
|
71
|
+
# Determine which columns to show
|
|
72
|
+
show_user = not scope_list or "user" in scope_list
|
|
73
|
+
show_reg = not scope_list or "registry" in scope_list
|
|
74
|
+
show_proj = not scope_list or "project" in scope_list
|
|
75
|
+
|
|
76
|
+
if show_user: table.add_column("User", justify="center")
|
|
77
|
+
if show_reg: table.add_column("Registry", justify="center")
|
|
78
|
+
if show_proj: table.add_column("Project", justify="center")
|
|
79
|
+
|
|
80
|
+
for item in results:
|
|
81
|
+
# Define icons
|
|
82
|
+
# Green check if identical (synced), Yellow not-equal sign if diff
|
|
83
|
+
synced = item["synced"]
|
|
84
|
+
icon = "[green]✅[/green]" if synced else "[yellow] ≠ [/yellow]"
|
|
85
|
+
|
|
86
|
+
row = [item["name"]]
|
|
87
|
+
|
|
88
|
+
if show_user:
|
|
89
|
+
row.append(icon if item["user"]["exists"] else "[dim]- [/dim]")
|
|
90
|
+
if show_reg:
|
|
91
|
+
row.append(icon if item["registry"]["exists"] else "[dim]- [/dim]")
|
|
92
|
+
if show_proj:
|
|
93
|
+
row.append(icon if item["project"]["exists"] else "[dim]- [/dim]")
|
|
94
|
+
|
|
95
|
+
table.add_row(*row)
|
|
96
|
+
|
|
97
|
+
console.print(table)
|
|
98
|
+
|
|
99
|
+
@cmds.command()
|
|
100
|
+
@click.argument("name")
|
|
101
|
+
@click.option("--update", is_flag=True, help="Overwrite if command exists in registry and content differs.")
|
|
102
|
+
@click.option("--source-scope", type=click.Choice(["user", "project"]), help="Explicitly choose source scope for registration.")
|
|
103
|
+
def register(name, update, source_scope):
|
|
104
|
+
"""Register a command from user/project scope to the registry."""
|
|
105
|
+
try:
|
|
106
|
+
path = sdk.register_command(name, update=update, source_scope=source_scope)
|
|
107
|
+
rprint(f"[green]Registered[/green] '{name}' to {path}")
|
|
108
|
+
rprint(f"[blue]Note:[/blue] Changes to the registry repo ({path.parent}) will need to be committed and pushed to GitHub.")
|
|
109
|
+
except (ValueError, FileNotFoundError, FileExistsError) as e:
|
|
110
|
+
rprint(f"[red]Error:[/red] {e}")
|
|
111
|
+
exit(1)
|
|
112
|
+
|
|
113
|
+
@cmds.command()
|
|
114
|
+
@click.argument("name")
|
|
115
|
+
def show(name):
|
|
116
|
+
"""Show details of a command."""
|
|
117
|
+
data = sdk.get_command(name)
|
|
118
|
+
if not data:
|
|
119
|
+
rprint(f"[red]Error:[/red] Command '{name}' not found.")
|
|
120
|
+
return
|
|
121
|
+
|
|
122
|
+
rprint(f"[bold]Description:[/bold] {data.get('description')}")
|
|
123
|
+
rprint("[bold]Prompt:[/bold]")
|
|
124
|
+
rprint(data.get("prompt"))
|
|
125
|
+
|
|
126
|
+
@cmds.command()
|
|
127
|
+
@click.argument("name")
|
|
128
|
+
@click.option("--scope", type=click.Choice(["user", "project", "registry"]),
|
|
129
|
+
default="user", help="Scope to remove from (default: user)")
|
|
130
|
+
def remove(name, scope):
|
|
131
|
+
"""Remove a command from local, project, or registry scope."""
|
|
132
|
+
success = sdk.delete_command(name, scope=scope)
|
|
133
|
+
if success:
|
|
134
|
+
rprint(f"[green]Removed[/green] '{name}' from {scope} scope.")
|
|
135
|
+
else:
|
|
136
|
+
rprint(f"[red]Error:[/red] Command '{name}' not found in {scope} scope.")
|
|
137
|
+
|
|
138
|
+
@cmds.command()
|
|
139
|
+
@click.argument("name")
|
|
140
|
+
def publish(name):
|
|
141
|
+
"""Publish a command from User scope to the Registry."""
|
|
142
|
+
try:
|
|
143
|
+
path = sdk.publish_command(name)
|
|
144
|
+
rprint(f"[green]Published[/green] '{name}' to Registry ({path})")
|
|
145
|
+
except FileNotFoundError as e:
|
|
146
|
+
rprint(f"[red]Error:[/red] {e}")
|
|
147
|
+
|
|
148
|
+
@cmds.command()
|
|
149
|
+
@click.argument("name")
|
|
150
|
+
def install(name):
|
|
151
|
+
"""Install a command from Registry to User scope."""
|
|
152
|
+
try:
|
|
153
|
+
path = sdk.install_command(name)
|
|
154
|
+
rprint(f"[green]Installed[/green] '{name}' to User scope ({path})")
|
|
155
|
+
except FileNotFoundError as e:
|
|
156
|
+
rprint(f"[red]Error:[/red] {e}")
|
|
157
|
+
|
|
158
|
+
@cmds.command()
|
|
159
|
+
@click.argument("name")
|
|
160
|
+
def diff(name):
|
|
161
|
+
"""Show differences between User and Registry versions."""
|
|
162
|
+
result = sdk.get_diff(name)
|
|
163
|
+
if not result:
|
|
164
|
+
rprint(f"Command '{name}' cannot be diffed (must exist in both User and Registry).")
|
|
165
|
+
return
|
|
166
|
+
|
|
167
|
+
repo_lines, xdg_lines = result
|
|
168
|
+
import difflib
|
|
169
|
+
|
|
170
|
+
diff = difflib.unified_diff(
|
|
171
|
+
repo_lines, xdg_lines,
|
|
172
|
+
fromfile=f"Registry ({name})",
|
|
173
|
+
tofile=f"User ({name})",
|
|
174
|
+
lineterm=""
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
for line in diff:
|
|
178
|
+
if line.startswith("+"):
|
|
179
|
+
console.print(line, style="green", end="")
|
|
180
|
+
elif line.startswith("-"):
|
|
181
|
+
console.print(line, style="red", end="")
|
|
182
|
+
elif line.startswith("@"):
|
|
183
|
+
console.print(line, style="cyan", end="")
|
|
184
|
+
else:
|
|
185
|
+
print(line, end="")
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
"""CLI commands for managing AI assistant context files."""
|
|
2
|
+
import json
|
|
3
|
+
import click
|
|
4
|
+
from rich import print as rprint
|
|
5
|
+
from rich.table import Table
|
|
6
|
+
from aicfg.sdk import context as context_sdk
|
|
7
|
+
from aicfg.sdk import settings as settings_sdk
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@click.group(name="context")
|
|
11
|
+
def context_cli():
|
|
12
|
+
"""Manage AI assistant context files."""
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@context_cli.command("status")
|
|
17
|
+
@click.option("--scope", type=click.Choice(["user", "project"]), default=None,
|
|
18
|
+
help="Filter to specific scope (default: show all)")
|
|
19
|
+
@click.option("--format", "output_format", type=click.Choice(["table", "json"]), default="table")
|
|
20
|
+
def status(scope, output_format):
|
|
21
|
+
"""Show the current state of context files for both user and project scopes."""
|
|
22
|
+
result = context_sdk.get_context_status(scope)
|
|
23
|
+
|
|
24
|
+
if output_format == "json":
|
|
25
|
+
rprint(json.dumps(result, indent=2))
|
|
26
|
+
return
|
|
27
|
+
|
|
28
|
+
rprint(f"[dim]Working directory:[/dim] {result['working_directory']}")
|
|
29
|
+
if result.get("git_root"):
|
|
30
|
+
rprint(f"[dim]Git root:[/dim] {result['git_root']}")
|
|
31
|
+
rprint()
|
|
32
|
+
|
|
33
|
+
for scope_name, scope_data in result["scopes"].items():
|
|
34
|
+
table = Table(title=f"{scope_name.title()} Scope")
|
|
35
|
+
table.add_column("File", style="cyan")
|
|
36
|
+
table.add_column("Status", style="green")
|
|
37
|
+
table.add_column("Details", style="dim")
|
|
38
|
+
|
|
39
|
+
for file_name, info in scope_data["files"].items():
|
|
40
|
+
if not info["exists"]:
|
|
41
|
+
file_status = "[yellow]missing[/yellow]"
|
|
42
|
+
details = info["path"]
|
|
43
|
+
elif info["is_symlink"]:
|
|
44
|
+
if info.get("points_to_unified"):
|
|
45
|
+
file_status = "[green]symlink (unified)[/green]"
|
|
46
|
+
else:
|
|
47
|
+
file_status = "[red]symlink (other)[/red]"
|
|
48
|
+
details = f"-> {info['symlink_target']}"
|
|
49
|
+
else:
|
|
50
|
+
file_status = "present"
|
|
51
|
+
details = info["path"]
|
|
52
|
+
|
|
53
|
+
table.add_row(file_name, file_status, details)
|
|
54
|
+
|
|
55
|
+
rprint(table)
|
|
56
|
+
rprint(f"[dim]State: {scope_data['state']}[/dim]\n")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@context_cli.command("unify")
|
|
60
|
+
@click.option("--scope", type=click.Choice(["user", "project"]), default="user",
|
|
61
|
+
help="Scope to unify (default: user)")
|
|
62
|
+
def unify(scope):
|
|
63
|
+
"""
|
|
64
|
+
Unify CLAUDE.md and GEMINI.md into a single shared CONTEXT.md.
|
|
65
|
+
|
|
66
|
+
Combines context files into a unified location, then creates symlinks
|
|
67
|
+
so both tools read from the same file.
|
|
68
|
+
|
|
69
|
+
This operation is idempotent - running it multiple times is safe.
|
|
70
|
+
"""
|
|
71
|
+
result = context_sdk.unify_context(scope)
|
|
72
|
+
|
|
73
|
+
if result["success"]:
|
|
74
|
+
rprint(f"[green]Success![/green]\n")
|
|
75
|
+
rprint(result["message"])
|
|
76
|
+
|
|
77
|
+
if result.get("backups"):
|
|
78
|
+
rprint(f"\n[dim]Backups created:[/dim]")
|
|
79
|
+
for backup in result["backups"]:
|
|
80
|
+
rprint(f" [dim]{backup}[/dim]")
|
|
81
|
+
|
|
82
|
+
if result.get("symlinks_created"):
|
|
83
|
+
rprint(f"\n[dim]Symlinks created:[/dim]")
|
|
84
|
+
for symlink in result["symlinks_created"]:
|
|
85
|
+
rprint(f" [dim]{symlink}[/dim]")
|
|
86
|
+
else:
|
|
87
|
+
rprint(f"[red]Failed![/red]\n")
|
|
88
|
+
rprint(result.get("message", result.get("error", "Unknown error")))
|
|
89
|
+
raise SystemExit(1)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@context_cli.command("analyze")
|
|
93
|
+
@click.argument("scope", type=click.Choice(["user", "project", "all"]))
|
|
94
|
+
@click.argument("prompt")
|
|
95
|
+
@click.option("--model", default=None, help="Gemini model override")
|
|
96
|
+
@click.option("--format", "output_format", type=click.Choice(["text", "json"]), default="text")
|
|
97
|
+
def analyze(scope, prompt, model, output_format):
|
|
98
|
+
"""
|
|
99
|
+
Analyze context files using Gemini.
|
|
100
|
+
|
|
101
|
+
SCOPE: 'user', 'project', or 'all' to analyze both scopes together.
|
|
102
|
+
|
|
103
|
+
PROMPT: Your question or analysis request about the context files.
|
|
104
|
+
"""
|
|
105
|
+
result = context_sdk.analyze_context(scope, prompt, model)
|
|
106
|
+
|
|
107
|
+
if output_format == "json":
|
|
108
|
+
rprint(json.dumps(result, indent=2))
|
|
109
|
+
return
|
|
110
|
+
|
|
111
|
+
if result.get("success"):
|
|
112
|
+
rprint(result["response"])
|
|
113
|
+
else:
|
|
114
|
+
rprint(f"[red]Error:[/red] {result.get('error', 'Unknown error')}")
|
|
115
|
+
raise SystemExit(1)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@context_cli.command("revise")
|
|
119
|
+
@click.argument("scope", type=click.Choice(["user", "project"]))
|
|
120
|
+
@click.argument("prompt")
|
|
121
|
+
@click.option("--model", default=None, help="Gemini model override")
|
|
122
|
+
@click.option("--format", "output_format", type=click.Choice(["text", "json"]), default="text")
|
|
123
|
+
def revise(scope, prompt, model, output_format):
|
|
124
|
+
"""
|
|
125
|
+
Revise context file using Gemini.
|
|
126
|
+
|
|
127
|
+
SCOPE: 'user' or 'project' (cannot revise both at once).
|
|
128
|
+
|
|
129
|
+
PROMPT: Instructions for how to modify the context file.
|
|
130
|
+
"""
|
|
131
|
+
result = context_sdk.revise_context(scope, prompt, model)
|
|
132
|
+
|
|
133
|
+
if output_format == "json":
|
|
134
|
+
rprint(json.dumps(result, indent=2))
|
|
135
|
+
return
|
|
136
|
+
|
|
137
|
+
if result.get("success"):
|
|
138
|
+
rprint(f"[green]Success![/green] {result['message']}")
|
|
139
|
+
rprint(f"[dim]Backup: {result['backup']}[/dim]")
|
|
140
|
+
else:
|
|
141
|
+
rprint(f"[red]Error:[/red] {result.get('error', 'Unknown error')}")
|
|
142
|
+
raise SystemExit(1)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
# Subgroup for file-names management (Gemini-specific setting)
|
|
146
|
+
@context_cli.group(name="file-names")
|
|
147
|
+
def file_names():
|
|
148
|
+
"""Manage context.fileName setting (Gemini-specific)."""
|
|
149
|
+
pass
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
@file_names.command("list")
|
|
153
|
+
def list_file_names():
|
|
154
|
+
"""List configured context file names."""
|
|
155
|
+
path, files = settings_sdk.get_context_files()
|
|
156
|
+
rprint(f"[bold]Config:[/bold] {path}")
|
|
157
|
+
if not files:
|
|
158
|
+
rprint("[yellow]No context files configured.[/yellow]")
|
|
159
|
+
return
|
|
160
|
+
table = Table(show_header=True, header_style="bold cyan")
|
|
161
|
+
table.add_column("File Name")
|
|
162
|
+
for f in sorted(files):
|
|
163
|
+
table.add_row(f)
|
|
164
|
+
rprint(table)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
@file_names.command("add")
|
|
168
|
+
@click.argument("filename")
|
|
169
|
+
def add_file_name(filename):
|
|
170
|
+
"""Add a context file name."""
|
|
171
|
+
path = settings_sdk.add_context_file(filename)
|
|
172
|
+
rprint(f"[green]Added[/green] '{filename}' to {path}")
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
@file_names.command("remove")
|
|
176
|
+
@click.argument("filename")
|
|
177
|
+
def remove_file_name(filename):
|
|
178
|
+
"""Remove a context file name."""
|
|
179
|
+
path, removed = settings_sdk.remove_context_file(filename)
|
|
180
|
+
if removed:
|
|
181
|
+
rprint(f"[green]Removed[/green] '{filename}' from {path}")
|
|
182
|
+
else:
|
|
183
|
+
rprint(f"[red]File '{filename}' not found in {path}[/red]")
|