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.
Files changed (44) hide show
  1. echomodel-0.2.1/PKG-INFO +12 -0
  2. echomodel-0.2.1/README.md +147 -0
  3. echomodel-0.2.1/pyproject.toml +31 -0
  4. echomodel-0.2.1/setup.cfg +4 -0
  5. echomodel-0.2.1/src/aicfg/__init__.py +0 -0
  6. echomodel-0.2.1/src/aicfg/cli/__init__.py +24 -0
  7. echomodel-0.2.1/src/aicfg/cli/commands.py +185 -0
  8. echomodel-0.2.1/src/aicfg/cli/context.py +183 -0
  9. echomodel-0.2.1/src/aicfg/cli/servers.py +143 -0
  10. echomodel-0.2.1/src/aicfg/cli/sessions.py +54 -0
  11. echomodel-0.2.1/src/aicfg/cli/settings.py +144 -0
  12. echomodel-0.2.1/src/aicfg/cli/skills.py +294 -0
  13. echomodel-0.2.1/src/aicfg/mcp/__init__.py +0 -0
  14. echomodel-0.2.1/src/aicfg/mcp/server.py +340 -0
  15. echomodel-0.2.1/src/aicfg/sdk/__init__.py +0 -0
  16. echomodel-0.2.1/src/aicfg/sdk/commands.py +171 -0
  17. echomodel-0.2.1/src/aicfg/sdk/config.py +91 -0
  18. echomodel-0.2.1/src/aicfg/sdk/context.py +475 -0
  19. echomodel-0.2.1/src/aicfg/sdk/mcp_setup.py +305 -0
  20. echomodel-0.2.1/src/aicfg/sdk/sessions.py +263 -0
  21. echomodel-0.2.1/src/aicfg/sdk/settings.py +175 -0
  22. echomodel-0.2.1/src/aicfg/sdk/skills.py +1210 -0
  23. echomodel-0.2.1/src/aicfg/sdk/utils.py +79 -0
  24. echomodel-0.2.1/src/echomodel.egg-info/PKG-INFO +12 -0
  25. echomodel-0.2.1/src/echomodel.egg-info/SOURCES.txt +42 -0
  26. echomodel-0.2.1/src/echomodel.egg-info/dependency_links.txt +1 -0
  27. echomodel-0.2.1/src/echomodel.egg-info/entry_points.txt +5 -0
  28. echomodel-0.2.1/src/echomodel.egg-info/requires.txt +8 -0
  29. echomodel-0.2.1/src/echomodel.egg-info/top_level.txt +1 -0
  30. echomodel-0.2.1/tests/test_allowed_tools_config.py +25 -0
  31. echomodel-0.2.1/tests/test_context_assist.py +242 -0
  32. echomodel-0.2.1/tests/test_context_eval.py +194 -0
  33. echomodel-0.2.1/tests/test_included_paths_config.py +17 -0
  34. echomodel-0.2.1/tests/test_isolated.py +9 -0
  35. echomodel-0.2.1/tests/test_mcp_add.py +134 -0
  36. echomodel-0.2.1/tests/test_mcp_remove.py +66 -0
  37. echomodel-0.2.1/tests/test_mcp_servers_list.py +19 -0
  38. echomodel-0.2.1/tests/test_mcp_servers_modify.py +114 -0
  39. echomodel-0.2.1/tests/test_settings_read.py +106 -0
  40. echomodel-0.2.1/tests/test_settings_set.py +63 -0
  41. echomodel-0.2.1/tests/test_skills.py +836 -0
  42. echomodel-0.2.1/tests/test_slash_commands_config.py +41 -0
  43. echomodel-0.2.1/tests/test_slash_commands_namespaced.py +40 -0
  44. echomodel-0.2.1/tests/test_slash_commands_register.py +24 -0
@@ -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"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
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]")