zolvix-agent 0.3.0__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.
- zolvix_agent-0.3.0/PKG-INFO +68 -0
- zolvix_agent-0.3.0/README.md +42 -0
- zolvix_agent-0.3.0/agent/__init__.py +2 -0
- zolvix_agent-0.3.0/agent/config.py +123 -0
- zolvix_agent-0.3.0/agent/main.py +304 -0
- zolvix_agent-0.3.0/agent/patterns.py +28 -0
- zolvix_agent-0.3.0/agent/tools.py +451 -0
- zolvix_agent-0.3.0/agent/watcher.py +94 -0
- zolvix_agent-0.3.0/agent/ws_client.py +181 -0
- zolvix_agent-0.3.0/pyproject.toml +43 -0
- zolvix_agent-0.3.0/setup.cfg +4 -0
- zolvix_agent-0.3.0/tests/test_cli_onboarding.py +45 -0
- zolvix_agent-0.3.0/tests/test_config.py +16 -0
- zolvix_agent-0.3.0/tests/test_patch.py +33 -0
- zolvix_agent-0.3.0/tests/test_patterns.py +43 -0
- zolvix_agent-0.3.0/tests/test_project_map.py +13 -0
- zolvix_agent-0.3.0/tests/test_run_command.py +24 -0
- zolvix_agent-0.3.0/tests/test_tools.py +93 -0
- zolvix_agent-0.3.0/tests/test_version.py +12 -0
- zolvix_agent-0.3.0/zolvix_agent.egg-info/PKG-INFO +68 -0
- zolvix_agent-0.3.0/zolvix_agent.egg-info/SOURCES.txt +23 -0
- zolvix_agent-0.3.0/zolvix_agent.egg-info/dependency_links.txt +1 -0
- zolvix_agent-0.3.0/zolvix_agent.egg-info/entry_points.txt +2 -0
- zolvix_agent-0.3.0/zolvix_agent.egg-info/requires.txt +12 -0
- zolvix_agent-0.3.0/zolvix_agent.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: zolvix-agent
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Zolvix local file agent — connects your codebase to Zolvix AI
|
|
5
|
+
Author: Zolvix
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://zolvix.app
|
|
8
|
+
Keywords: zolvix,ai,agent,rag,cli
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Environment :: Console
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Classifier: Topic :: Software Development
|
|
13
|
+
Requires-Python: >=3.10
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
Requires-Dist: websockets>=12.0
|
|
16
|
+
Requires-Dist: watchdog>=4.0
|
|
17
|
+
Requires-Dist: httpx>=0.27
|
|
18
|
+
Requires-Dist: click>=8.1
|
|
19
|
+
Requires-Dist: rich>=13.0
|
|
20
|
+
Requires-Dist: pydantic>=2.0
|
|
21
|
+
Requires-Dist: python-dotenv>=1.0
|
|
22
|
+
Requires-Dist: aiofiles>=23.0
|
|
23
|
+
Requires-Dist: pathspec>=0.12
|
|
24
|
+
Provides-Extra: dev
|
|
25
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
26
|
+
|
|
27
|
+
# Zolvix Agent
|
|
28
|
+
|
|
29
|
+
Command-line agent that lets Zolvix AI work with your local files.
|
|
30
|
+
|
|
31
|
+
## Install
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pip install zolvix-agent
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Usage
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
# 1. Log in (saves your API key)
|
|
41
|
+
zolvix login
|
|
42
|
+
|
|
43
|
+
# 2. Watch a project folder and connect it to Zolvix
|
|
44
|
+
zolvix watch ./src
|
|
45
|
+
|
|
46
|
+
# 3. Check your status
|
|
47
|
+
zolvix status
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Run `zolvix` with no command to see a quick getting-started guide, or
|
|
51
|
+
`zolvix --help` for the full command reference.
|
|
52
|
+
|
|
53
|
+
`zolvix watch` shows local file-change activity; the server-side change feed is
|
|
54
|
+
not implemented yet.
|
|
55
|
+
|
|
56
|
+
Use `--confirm-writes` to be asked before each file write.
|
|
57
|
+
|
|
58
|
+
## Security
|
|
59
|
+
|
|
60
|
+
- The agent only operates inside the folder you point it at (sandboxed).
|
|
61
|
+
- `.env`, `*.key`, `*.pem`, `.git/`, `node_modules/`, `dist/`, and `build/` are
|
|
62
|
+
excluded from both reads and writes.
|
|
63
|
+
- Every action is recorded in an audit log.
|
|
64
|
+
|
|
65
|
+
## Requirements
|
|
66
|
+
|
|
67
|
+
- Python 3.10+
|
|
68
|
+
- A Business or Enterprise plan
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Zolvix Agent
|
|
2
|
+
|
|
3
|
+
Command-line agent that lets Zolvix AI work with your local files.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install zolvix-agent
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
# 1. Log in (saves your API key)
|
|
15
|
+
zolvix login
|
|
16
|
+
|
|
17
|
+
# 2. Watch a project folder and connect it to Zolvix
|
|
18
|
+
zolvix watch ./src
|
|
19
|
+
|
|
20
|
+
# 3. Check your status
|
|
21
|
+
zolvix status
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Run `zolvix` with no command to see a quick getting-started guide, or
|
|
25
|
+
`zolvix --help` for the full command reference.
|
|
26
|
+
|
|
27
|
+
`zolvix watch` shows local file-change activity; the server-side change feed is
|
|
28
|
+
not implemented yet.
|
|
29
|
+
|
|
30
|
+
Use `--confirm-writes` to be asked before each file write.
|
|
31
|
+
|
|
32
|
+
## Security
|
|
33
|
+
|
|
34
|
+
- The agent only operates inside the folder you point it at (sandboxed).
|
|
35
|
+
- `.env`, `*.key`, `*.pem`, `.git/`, `node_modules/`, `dist/`, and `build/` are
|
|
36
|
+
excluded from both reads and writes.
|
|
37
|
+
- Every action is recorded in an audit log.
|
|
38
|
+
|
|
39
|
+
## Requirements
|
|
40
|
+
|
|
41
|
+
- Python 3.10+
|
|
42
|
+
- A Business or Enterprise plan
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Agent configuration.
|
|
3
|
+
Reads from ~/.zolvix/config or env vars or .zolvix file in project root.
|
|
4
|
+
"""
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
import os
|
|
7
|
+
import json
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
CONFIG_DIR = Path.home() / ".zolvix"
|
|
13
|
+
CONFIG_FILE = CONFIG_DIR / "config.json"
|
|
14
|
+
DOTENV_FILE = Path(".zolvix")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class AgentConfig:
|
|
19
|
+
api_key: str = ""
|
|
20
|
+
base_url: str = "https://zolvix.app" # prod default
|
|
21
|
+
watch_path: str = "."
|
|
22
|
+
# Hangi pattern'ler izlensin
|
|
23
|
+
include_patterns: list[str] = field(default_factory=lambda: [
|
|
24
|
+
"**/*.py", "**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx",
|
|
25
|
+
"**/*.java", "**/*.kt", "**/*.go", "**/*.rs",
|
|
26
|
+
"**/*.sql", "**/*.jrxml", "**/*.xml",
|
|
27
|
+
"**/*.json", "**/*.yaml", "**/*.yml",
|
|
28
|
+
"**/*.md", "**/*.txt",
|
|
29
|
+
])
|
|
30
|
+
# Asla dokunulmasın
|
|
31
|
+
exclude_patterns: list[str] = field(default_factory=lambda: [
|
|
32
|
+
"**/.git/**", "**/__pycache__/**", "**/node_modules/**",
|
|
33
|
+
"**/.venv/**", "**/venv/**", "**/.env*",
|
|
34
|
+
"**/*.pyc", "**/*.pyo", "**/.next/**",
|
|
35
|
+
"**/dist/**", "**/build/**", "**/*.secret",
|
|
36
|
+
"**/*.key", "**/*.pem", "**/*.p12",
|
|
37
|
+
])
|
|
38
|
+
# Maksimum dosya boyutu (byte) — büyük binary'leri atla
|
|
39
|
+
max_file_size: int = 512 * 1024 # 512 KB
|
|
40
|
+
# write_file için onay gereksin mi?
|
|
41
|
+
require_write_confirm: bool = False
|
|
42
|
+
# Optional callback invoked before a write when require_write_confirm is set.
|
|
43
|
+
# Returns True to proceed. Not loaded from disk/env (runtime-only).
|
|
44
|
+
confirm_cb: object = None
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def ws_url(self) -> str:
|
|
48
|
+
base = self.base_url.replace("https://", "wss://").replace("http://", "ws://")
|
|
49
|
+
return f"{base}/ws/agent"
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def api_url(self) -> str:
|
|
53
|
+
return self.base_url.rstrip("/")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def load_config(
|
|
57
|
+
api_key: str | None = None,
|
|
58
|
+
base_url: str | None = None,
|
|
59
|
+
watch_path: str | None = None,
|
|
60
|
+
) -> AgentConfig:
|
|
61
|
+
"""
|
|
62
|
+
Config yükleme önceliği:
|
|
63
|
+
1. CLI argümanları (en yüksek)
|
|
64
|
+
2. Ortam değişkenleri
|
|
65
|
+
3. Proje .zolvix dosyası
|
|
66
|
+
4. ~/.zolvix/config.json
|
|
67
|
+
5. Defaults
|
|
68
|
+
"""
|
|
69
|
+
cfg: dict = {}
|
|
70
|
+
|
|
71
|
+
# 4. Global config
|
|
72
|
+
if CONFIG_FILE.exists():
|
|
73
|
+
try:
|
|
74
|
+
cfg.update(json.loads(CONFIG_FILE.read_text()))
|
|
75
|
+
except Exception:
|
|
76
|
+
pass
|
|
77
|
+
|
|
78
|
+
# field-name aliases shared by .zolvix and env vars
|
|
79
|
+
_alias = {"zolvix_api_key": "api_key", "zolvix_base_url": "base_url", "zolvix_watch": "watch_path"}
|
|
80
|
+
|
|
81
|
+
# 3. Proje .zolvix
|
|
82
|
+
if DOTENV_FILE.exists():
|
|
83
|
+
try:
|
|
84
|
+
for line in DOTENV_FILE.read_text().splitlines():
|
|
85
|
+
line = line.strip()
|
|
86
|
+
if "=" in line and not line.startswith("#"):
|
|
87
|
+
k, v = line.split("=", 1)
|
|
88
|
+
key = k.strip().lower()
|
|
89
|
+
cfg[_alias.get(key, key)] = v.strip()
|
|
90
|
+
except Exception:
|
|
91
|
+
pass
|
|
92
|
+
|
|
93
|
+
# 2. Env vars
|
|
94
|
+
env_map = {
|
|
95
|
+
"ZOLVIX_API_KEY": "api_key",
|
|
96
|
+
"ZOLVIX_BASE_URL": "base_url",
|
|
97
|
+
"ZOLVIX_WATCH": "watch_path",
|
|
98
|
+
}
|
|
99
|
+
for env_key, cfg_key in env_map.items():
|
|
100
|
+
val = os.environ.get(env_key)
|
|
101
|
+
if val:
|
|
102
|
+
cfg[cfg_key] = val
|
|
103
|
+
|
|
104
|
+
# 1. CLI args
|
|
105
|
+
if api_key: cfg["api_key"] = api_key
|
|
106
|
+
if base_url: cfg["base_url"] = base_url
|
|
107
|
+
if watch_path: cfg["watch_path"] = watch_path
|
|
108
|
+
|
|
109
|
+
return AgentConfig(**{k: v for k, v in cfg.items() if hasattr(AgentConfig, k)})
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def save_config(api_key: str, base_url: str = "https://zolvix.app") -> None:
|
|
113
|
+
"""~/.zolvix/config.json'a kaydet."""
|
|
114
|
+
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
|
115
|
+
existing = {}
|
|
116
|
+
if CONFIG_FILE.exists():
|
|
117
|
+
try:
|
|
118
|
+
existing = json.loads(CONFIG_FILE.read_text())
|
|
119
|
+
except Exception:
|
|
120
|
+
pass
|
|
121
|
+
existing.update({"api_key": api_key, "base_url": base_url})
|
|
122
|
+
CONFIG_FILE.write_text(json.dumps(existing, indent=2))
|
|
123
|
+
CONFIG_FILE.chmod(0o600) # sadece kullanıcı okusun
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Zolvix Agent CLI
|
|
3
|
+
zolvix watch ./src
|
|
4
|
+
zolvix login
|
|
5
|
+
zolvix status
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
import asyncio
|
|
9
|
+
import sys
|
|
10
|
+
import os
|
|
11
|
+
import json
|
|
12
|
+
import signal
|
|
13
|
+
import logging
|
|
14
|
+
from datetime import datetime
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
import click
|
|
18
|
+
import httpx
|
|
19
|
+
from rich.console import Console
|
|
20
|
+
from rich.panel import Panel
|
|
21
|
+
from rich.text import Text
|
|
22
|
+
from rich.live import Live
|
|
23
|
+
from rich.table import Table
|
|
24
|
+
from rich import print as rprint
|
|
25
|
+
|
|
26
|
+
from agent import __version__
|
|
27
|
+
from agent.config import AgentConfig, load_config, save_config
|
|
28
|
+
from agent.tools import ToolExecutor
|
|
29
|
+
from agent.ws_client import AgentWebSocket
|
|
30
|
+
from agent.watcher import FileWatcher
|
|
31
|
+
|
|
32
|
+
console = Console()
|
|
33
|
+
log = logging.getLogger("zolvix")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# ── CLI ────────────────────────────────────────────────────────────────────────
|
|
37
|
+
|
|
38
|
+
@click.group(invoke_without_command=True)
|
|
39
|
+
@click.option("--debug", is_flag=True, help="Enable debug logging.")
|
|
40
|
+
@click.pass_context
|
|
41
|
+
def cli(ctx: click.Context, debug: bool):
|
|
42
|
+
"""Zolvix Agent - connect your local code to Zolvix AI."""
|
|
43
|
+
level = logging.DEBUG if debug else logging.WARNING
|
|
44
|
+
logging.basicConfig(level=level, format="%(message)s")
|
|
45
|
+
if ctx.invoked_subcommand is None:
|
|
46
|
+
_print_welcome()
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@cli.command()
|
|
50
|
+
@click.option("--api-key", envvar="ZOLVIX_API_KEY", help="Your Zolvix API key")
|
|
51
|
+
@click.option("--base-url", default="https://zolvix.app", help="Zolvix server URL")
|
|
52
|
+
def login(api_key: str, base_url: str):
|
|
53
|
+
"""Save your Zolvix API key."""
|
|
54
|
+
if not api_key:
|
|
55
|
+
api_key = click.prompt("Zolvix API Key", hide_input=True)
|
|
56
|
+
|
|
57
|
+
console.print("[dim]Testing connection...[/]")
|
|
58
|
+
try:
|
|
59
|
+
r = httpx.get(
|
|
60
|
+
f"{base_url.rstrip('/')}/api/auth/me",
|
|
61
|
+
headers={"Authorization": f"Bearer {api_key}"},
|
|
62
|
+
timeout=10,
|
|
63
|
+
)
|
|
64
|
+
if r.status_code == 401:
|
|
65
|
+
console.print("[red]Invalid API key.[/]")
|
|
66
|
+
sys.exit(1)
|
|
67
|
+
r.raise_for_status()
|
|
68
|
+
data = r.json()
|
|
69
|
+
save_config(api_key, base_url)
|
|
70
|
+
console.print(Panel(
|
|
71
|
+
f"[green]Logged in![/]\n\n"
|
|
72
|
+
f"[dim]Email:[/] {data.get('email', '?')}\n"
|
|
73
|
+
f"[dim]Plan:[/] {data.get('plan', '?')}\n"
|
|
74
|
+
f"[dim]Config:[/] ~/.zolvix/config.json",
|
|
75
|
+
title="[bold]Zolvix Agent[/]",
|
|
76
|
+
border_style="green",
|
|
77
|
+
))
|
|
78
|
+
except httpx.RequestError as e:
|
|
79
|
+
console.print(f"[red]Connection error: {e}[/]")
|
|
80
|
+
sys.exit(1)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@cli.command()
|
|
84
|
+
@click.argument("path", default=".", type=click.Path(exists=True))
|
|
85
|
+
@click.option("--api-key", envvar="ZOLVIX_API_KEY")
|
|
86
|
+
@click.option("--base-url", envvar="ZOLVIX_BASE_URL")
|
|
87
|
+
@click.option("--confirm-writes", is_flag=True, help="Ask for confirmation before each file write.")
|
|
88
|
+
def watch(path: str, api_key: str | None, base_url: str | None, confirm_writes: bool):
|
|
89
|
+
"""Watch a folder and connect it to Zolvix."""
|
|
90
|
+
config = load_config(
|
|
91
|
+
api_key=api_key,
|
|
92
|
+
base_url=base_url,
|
|
93
|
+
watch_path=str(Path(path).resolve()),
|
|
94
|
+
)
|
|
95
|
+
config.require_write_confirm = confirm_writes
|
|
96
|
+
|
|
97
|
+
if not config.api_key:
|
|
98
|
+
console.print("[red]No API key found. Run 'zolvix login' first.[/]")
|
|
99
|
+
sys.exit(1)
|
|
100
|
+
|
|
101
|
+
_check_plan(config)
|
|
102
|
+
|
|
103
|
+
console.print(Panel(
|
|
104
|
+
f"[bold cyan]Zolvix Agent[/] [dim]v{__version__}[/]\n\n"
|
|
105
|
+
f"[dim]Watching:[/] [green]{config.watch_path}[/]\n"
|
|
106
|
+
f"[dim]Server:[/] {config.base_url}\n\n"
|
|
107
|
+
f"[dim]Stop with Ctrl+C[/]",
|
|
108
|
+
border_style="cyan",
|
|
109
|
+
))
|
|
110
|
+
|
|
111
|
+
executor = ToolExecutor(config)
|
|
112
|
+
|
|
113
|
+
if config.require_write_confirm:
|
|
114
|
+
def _confirm(path: str, diff: str) -> bool:
|
|
115
|
+
console.print(f"[yellow]write_file -> {path}[/]")
|
|
116
|
+
return click.confirm("Allow this file write?", default=False)
|
|
117
|
+
config.confirm_cb = _confirm
|
|
118
|
+
|
|
119
|
+
activity: list[dict] = []
|
|
120
|
+
|
|
121
|
+
def on_status(level: str, msg: str):
|
|
122
|
+
ts = datetime.now().strftime("%H:%M:%S")
|
|
123
|
+
color_map = {
|
|
124
|
+
"info": "dim",
|
|
125
|
+
"success": "green",
|
|
126
|
+
"warn": "yellow",
|
|
127
|
+
"error": "red",
|
|
128
|
+
"tool": "cyan",
|
|
129
|
+
"ok": "green",
|
|
130
|
+
"agent": "purple",
|
|
131
|
+
}
|
|
132
|
+
color = color_map.get(level, "white")
|
|
133
|
+
icon_map = {
|
|
134
|
+
"info": "●",
|
|
135
|
+
"success": "✓",
|
|
136
|
+
"warn": "⚠",
|
|
137
|
+
"error": "✗",
|
|
138
|
+
"tool": "⚙",
|
|
139
|
+
"ok": "✓",
|
|
140
|
+
"agent": "🤖",
|
|
141
|
+
}
|
|
142
|
+
icon = icon_map.get(level, "·")
|
|
143
|
+
console.print(f"[dim]{ts}[/] [{color}]{icon}[/] {msg}")
|
|
144
|
+
activity.append({"ts": ts, "level": level, "msg": msg})
|
|
145
|
+
|
|
146
|
+
def on_file_change(event: str, src: str, dst: str | None):
|
|
147
|
+
on_status("info", f"File {event}: {src}")
|
|
148
|
+
|
|
149
|
+
watcher = FileWatcher(config, on_file_change)
|
|
150
|
+
watcher.start()
|
|
151
|
+
|
|
152
|
+
ws = AgentWebSocket(config, executor, on_status)
|
|
153
|
+
loop = asyncio.new_event_loop()
|
|
154
|
+
asyncio.set_event_loop(loop)
|
|
155
|
+
stop_event = asyncio.Event()
|
|
156
|
+
|
|
157
|
+
async def _run():
|
|
158
|
+
runner = loop.create_task(ws.run())
|
|
159
|
+
await stop_event.wait()
|
|
160
|
+
await ws.stop()
|
|
161
|
+
runner.cancel()
|
|
162
|
+
try:
|
|
163
|
+
await runner
|
|
164
|
+
except asyncio.CancelledError:
|
|
165
|
+
pass
|
|
166
|
+
|
|
167
|
+
def _shutdown(sig, frame):
|
|
168
|
+
console.print("\n[dim]Shutting down...[/]")
|
|
169
|
+
loop.call_soon_threadsafe(stop_event.set)
|
|
170
|
+
|
|
171
|
+
signal.signal(signal.SIGINT, _shutdown)
|
|
172
|
+
try:
|
|
173
|
+
signal.signal(signal.SIGTERM, _shutdown)
|
|
174
|
+
except (ValueError, AttributeError):
|
|
175
|
+
pass # SIGTERM not available on this platform
|
|
176
|
+
|
|
177
|
+
try:
|
|
178
|
+
loop.run_until_complete(_run())
|
|
179
|
+
finally:
|
|
180
|
+
watcher.stop()
|
|
181
|
+
loop.close()
|
|
182
|
+
console.print("[dim]Agent stopped.[/]")
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
@cli.command()
|
|
186
|
+
@click.option("--api-key", envvar="ZOLVIX_API_KEY")
|
|
187
|
+
@click.option("--base-url", envvar="ZOLVIX_BASE_URL")
|
|
188
|
+
def status(api_key: str | None, base_url: str | None):
|
|
189
|
+
"""Show your connection and plan status."""
|
|
190
|
+
config = load_config(api_key=api_key, base_url=base_url)
|
|
191
|
+
|
|
192
|
+
if not config.api_key:
|
|
193
|
+
console.print("[red]No API key found. Run 'zolvix login'.[/]")
|
|
194
|
+
return
|
|
195
|
+
|
|
196
|
+
try:
|
|
197
|
+
r = httpx.get(
|
|
198
|
+
f"{config.api_url}/api/auth/me",
|
|
199
|
+
headers={"Authorization": f"Bearer {config.api_key}"},
|
|
200
|
+
timeout=10,
|
|
201
|
+
)
|
|
202
|
+
r.raise_for_status()
|
|
203
|
+
data = r.json()
|
|
204
|
+
|
|
205
|
+
table = Table(show_header=False, box=None, padding=(0, 2))
|
|
206
|
+
table.add_row("[dim]Email[/]", data.get("email", "?"))
|
|
207
|
+
table.add_row("[dim]Plan[/]", data.get("plan", "?"))
|
|
208
|
+
table.add_row("[dim]Server[/]", config.base_url)
|
|
209
|
+
table.add_row("[dim]Config[/]", str(Path.home() / ".zolvix" / "config.json"))
|
|
210
|
+
|
|
211
|
+
plan = data.get("plan", "")
|
|
212
|
+
if plan in ("business", "enterprise"):
|
|
213
|
+
status_text = "[green]✓ Agent mode available[/]"
|
|
214
|
+
else:
|
|
215
|
+
status_text = f"[yellow]⚠ Agent mode requires a Business or Enterprise plan (current: {plan})[/]"
|
|
216
|
+
|
|
217
|
+
console.print(Panel(table, title="[bold]Zolvix Agent Status[/]", border_style="cyan"))
|
|
218
|
+
console.print(status_text)
|
|
219
|
+
|
|
220
|
+
except httpx.RequestError as e:
|
|
221
|
+
console.print(f"[red]Connection error: {e}[/]")
|
|
222
|
+
except httpx.HTTPStatusError as e:
|
|
223
|
+
console.print(f"[red]HTTP error: {e.response.status_code}[/]")
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
@cli.command()
|
|
227
|
+
@click.argument("path", default=".", type=click.Path(exists=True))
|
|
228
|
+
@click.option("--api-key", envvar="ZOLVIX_API_KEY")
|
|
229
|
+
def init(path: str, api_key: str | None):
|
|
230
|
+
"""Create a .zolvix config file in a project folder."""
|
|
231
|
+
config = load_config(api_key=api_key, watch_path=path)
|
|
232
|
+
dotenv = Path(path) / ".zolvix"
|
|
233
|
+
|
|
234
|
+
content = f"""# Zolvix Agent config - add this file to .gitignore!
|
|
235
|
+
ZOLVIX_API_KEY={config.api_key or 'your-api-key-here'}
|
|
236
|
+
ZOLVIX_BASE_URL={config.base_url}
|
|
237
|
+
"""
|
|
238
|
+
dotenv.write_text(content)
|
|
239
|
+
|
|
240
|
+
gitignore = Path(path) / ".gitignore"
|
|
241
|
+
if gitignore.exists():
|
|
242
|
+
gi = gitignore.read_text()
|
|
243
|
+
if ".zolvix" not in gi:
|
|
244
|
+
gitignore.write_text(gi + "\n.zolvix\n")
|
|
245
|
+
console.print("[green]✓ Added .zolvix to .gitignore[/]")
|
|
246
|
+
|
|
247
|
+
console.print(f"[green]✓ Created {dotenv}[/]")
|
|
248
|
+
console.print("[dim]Add your API key to the file, then run 'zolvix watch'.[/]")
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
# ── Helpers ────────────────────────────────────────────────────────────────────
|
|
252
|
+
|
|
253
|
+
def _print_welcome() -> None:
|
|
254
|
+
"""Friendly getting-started banner for a bare `zolvix` invocation.
|
|
255
|
+
Local-only: adapts to whether the user has logged in (config exists)."""
|
|
256
|
+
from agent.config import CONFIG_FILE
|
|
257
|
+
logged_in = CONFIG_FILE.exists()
|
|
258
|
+
step1_tag = "[green]logged in[/]" if logged_in else "[yellow](start here)[/]"
|
|
259
|
+
step2_tag = " [yellow]<- next[/]" if logged_in else ""
|
|
260
|
+
body = Text.from_markup(
|
|
261
|
+
"Connect your local code to Zolvix AI.\n\n"
|
|
262
|
+
"[bold]Get started:[/]\n"
|
|
263
|
+
f" [cyan]1[/] [bold]zolvix login[/] Save your API key {step1_tag}\n"
|
|
264
|
+
f" [cyan]2[/] [bold]zolvix watch .[/] Watch this folder and connect{step2_tag}\n"
|
|
265
|
+
" [cyan]3[/] Open Zolvix in your browser, turn on Agent mode, and chat\n\n"
|
|
266
|
+
"[bold]More:[/]\n"
|
|
267
|
+
" [dim]zolvix status[/] Show connection and plan\n"
|
|
268
|
+
" [dim]zolvix init[/] Create a .zolvix config file here\n"
|
|
269
|
+
" [dim]zolvix --help[/] Full command reference\n\n"
|
|
270
|
+
"[dim]Docs: https://zolvix.app - Needs a Business or Enterprise plan[/]"
|
|
271
|
+
)
|
|
272
|
+
console.print(Panel(
|
|
273
|
+
body,
|
|
274
|
+
title=f"[bold cyan]Zolvix Agent[/] [dim]v{__version__}[/]",
|
|
275
|
+
border_style="cyan",
|
|
276
|
+
))
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def _check_plan(config: AgentConfig) -> None:
|
|
280
|
+
"""Business+ plan check."""
|
|
281
|
+
try:
|
|
282
|
+
r = httpx.get(
|
|
283
|
+
f"{config.api_url}/api/auth/me",
|
|
284
|
+
headers={"Authorization": f"Bearer {config.api_key}"},
|
|
285
|
+
timeout=10,
|
|
286
|
+
)
|
|
287
|
+
if r.status_code == 401:
|
|
288
|
+
console.print("[red]Invalid API key.[/]")
|
|
289
|
+
sys.exit(1)
|
|
290
|
+
data = r.json()
|
|
291
|
+
plan = data.get("plan", "")
|
|
292
|
+
if plan not in ("business", "enterprise"):
|
|
293
|
+
console.print(
|
|
294
|
+
f"[yellow]⚠ Agent mode requires a Business or Enterprise plan.[/]\n"
|
|
295
|
+
f"[dim]Current plan: {plan}[/]\n"
|
|
296
|
+
f"Upgrade at: {config.base_url}/pricing"
|
|
297
|
+
)
|
|
298
|
+
sys.exit(1)
|
|
299
|
+
except httpx.RequestError:
|
|
300
|
+
console.print("[yellow]⚠ Could not verify plan, continuing...[/]")
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
if __name__ == "__main__":
|
|
304
|
+
cli()
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Single source of include/exclude path matching.
|
|
2
|
+
|
|
3
|
+
Uses gitignore semantics (pathspec) so patterns like ``**/*.py`` match files at
|
|
4
|
+
every depth INCLUDING the watch root, and ``**/.git/**`` / ``*.key`` exclusions
|
|
5
|
+
fire at any depth. fnmatch could not express ``**`` and silently broke both.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import pathspec
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class PathMatcher:
|
|
13
|
+
def __init__(self, include: list[str], exclude: list[str]):
|
|
14
|
+
self._include = pathspec.PathSpec.from_lines("gitwildmatch", include)
|
|
15
|
+
self._exclude = pathspec.PathSpec.from_lines("gitwildmatch", exclude)
|
|
16
|
+
|
|
17
|
+
def excluded(self, rel: str) -> bool:
|
|
18
|
+
rel = rel.replace("\\", "/")
|
|
19
|
+
return self._exclude.match_file(rel)
|
|
20
|
+
|
|
21
|
+
def allowed(self, rel: str) -> bool:
|
|
22
|
+
"""Visible unless excluded. Include patterns are NOT a hard gate — file
|
|
23
|
+
discovery must surface everything that exists, extensioned or not
|
|
24
|
+
(e.g. ``Makefile``, ``Dockerfile``, a bare ``mehmet``). Secrets/junk
|
|
25
|
+
stay blocked by the exclude list; ``read_file`` still guards by size and
|
|
26
|
+
decodes with ``errors='replace'`` so binaries can't crash a read."""
|
|
27
|
+
rel = rel.replace("\\", "/")
|
|
28
|
+
return not self._exclude.match_file(rel)
|