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.
@@ -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,2 @@
1
+ """Zolvix Agent — local file agent for Zolvix AI."""
2
+ __version__ = "0.3.0"
@@ -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)