agent-cli 0.70.5__py3-none-any.whl
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.
- agent_cli/__init__.py +5 -0
- agent_cli/__main__.py +6 -0
- agent_cli/_extras.json +14 -0
- agent_cli/_requirements/.gitkeep +0 -0
- agent_cli/_requirements/audio.txt +79 -0
- agent_cli/_requirements/faster-whisper.txt +215 -0
- agent_cli/_requirements/kokoro.txt +425 -0
- agent_cli/_requirements/llm.txt +183 -0
- agent_cli/_requirements/memory.txt +355 -0
- agent_cli/_requirements/mlx-whisper.txt +222 -0
- agent_cli/_requirements/piper.txt +176 -0
- agent_cli/_requirements/rag.txt +402 -0
- agent_cli/_requirements/server.txt +154 -0
- agent_cli/_requirements/speed.txt +77 -0
- agent_cli/_requirements/vad.txt +155 -0
- agent_cli/_requirements/wyoming.txt +71 -0
- agent_cli/_tools.py +368 -0
- agent_cli/agents/__init__.py +23 -0
- agent_cli/agents/_voice_agent_common.py +136 -0
- agent_cli/agents/assistant.py +383 -0
- agent_cli/agents/autocorrect.py +284 -0
- agent_cli/agents/chat.py +496 -0
- agent_cli/agents/memory/__init__.py +31 -0
- agent_cli/agents/memory/add.py +190 -0
- agent_cli/agents/memory/proxy.py +160 -0
- agent_cli/agents/rag_proxy.py +128 -0
- agent_cli/agents/speak.py +209 -0
- agent_cli/agents/transcribe.py +671 -0
- agent_cli/agents/transcribe_daemon.py +499 -0
- agent_cli/agents/voice_edit.py +291 -0
- agent_cli/api.py +22 -0
- agent_cli/cli.py +106 -0
- agent_cli/config.py +503 -0
- agent_cli/config_cmd.py +307 -0
- agent_cli/constants.py +27 -0
- agent_cli/core/__init__.py +1 -0
- agent_cli/core/audio.py +461 -0
- agent_cli/core/audio_format.py +299 -0
- agent_cli/core/chroma.py +88 -0
- agent_cli/core/deps.py +191 -0
- agent_cli/core/openai_proxy.py +139 -0
- agent_cli/core/process.py +195 -0
- agent_cli/core/reranker.py +120 -0
- agent_cli/core/sse.py +87 -0
- agent_cli/core/transcription_logger.py +70 -0
- agent_cli/core/utils.py +526 -0
- agent_cli/core/vad.py +175 -0
- agent_cli/core/watch.py +65 -0
- agent_cli/dev/__init__.py +14 -0
- agent_cli/dev/cli.py +1588 -0
- agent_cli/dev/coding_agents/__init__.py +19 -0
- agent_cli/dev/coding_agents/aider.py +24 -0
- agent_cli/dev/coding_agents/base.py +167 -0
- agent_cli/dev/coding_agents/claude.py +39 -0
- agent_cli/dev/coding_agents/codex.py +24 -0
- agent_cli/dev/coding_agents/continue_dev.py +15 -0
- agent_cli/dev/coding_agents/copilot.py +24 -0
- agent_cli/dev/coding_agents/cursor_agent.py +48 -0
- agent_cli/dev/coding_agents/gemini.py +28 -0
- agent_cli/dev/coding_agents/opencode.py +15 -0
- agent_cli/dev/coding_agents/registry.py +49 -0
- agent_cli/dev/editors/__init__.py +19 -0
- agent_cli/dev/editors/base.py +89 -0
- agent_cli/dev/editors/cursor.py +15 -0
- agent_cli/dev/editors/emacs.py +46 -0
- agent_cli/dev/editors/jetbrains.py +56 -0
- agent_cli/dev/editors/nano.py +31 -0
- agent_cli/dev/editors/neovim.py +33 -0
- agent_cli/dev/editors/registry.py +59 -0
- agent_cli/dev/editors/sublime.py +20 -0
- agent_cli/dev/editors/vim.py +42 -0
- agent_cli/dev/editors/vscode.py +15 -0
- agent_cli/dev/editors/zed.py +20 -0
- agent_cli/dev/project.py +568 -0
- agent_cli/dev/registry.py +52 -0
- agent_cli/dev/skill/SKILL.md +141 -0
- agent_cli/dev/skill/examples.md +571 -0
- agent_cli/dev/terminals/__init__.py +19 -0
- agent_cli/dev/terminals/apple_terminal.py +82 -0
- agent_cli/dev/terminals/base.py +56 -0
- agent_cli/dev/terminals/gnome.py +51 -0
- agent_cli/dev/terminals/iterm2.py +84 -0
- agent_cli/dev/terminals/kitty.py +77 -0
- agent_cli/dev/terminals/registry.py +48 -0
- agent_cli/dev/terminals/tmux.py +58 -0
- agent_cli/dev/terminals/warp.py +132 -0
- agent_cli/dev/terminals/zellij.py +78 -0
- agent_cli/dev/worktree.py +856 -0
- agent_cli/docs_gen.py +417 -0
- agent_cli/example-config.toml +185 -0
- agent_cli/install/__init__.py +5 -0
- agent_cli/install/common.py +89 -0
- agent_cli/install/extras.py +174 -0
- agent_cli/install/hotkeys.py +48 -0
- agent_cli/install/services.py +87 -0
- agent_cli/memory/__init__.py +7 -0
- agent_cli/memory/_files.py +250 -0
- agent_cli/memory/_filters.py +63 -0
- agent_cli/memory/_git.py +157 -0
- agent_cli/memory/_indexer.py +142 -0
- agent_cli/memory/_ingest.py +408 -0
- agent_cli/memory/_persistence.py +182 -0
- agent_cli/memory/_prompt.py +91 -0
- agent_cli/memory/_retrieval.py +294 -0
- agent_cli/memory/_store.py +169 -0
- agent_cli/memory/_streaming.py +44 -0
- agent_cli/memory/_tasks.py +48 -0
- agent_cli/memory/api.py +113 -0
- agent_cli/memory/client.py +272 -0
- agent_cli/memory/engine.py +361 -0
- agent_cli/memory/entities.py +43 -0
- agent_cli/memory/models.py +112 -0
- agent_cli/opts.py +433 -0
- agent_cli/py.typed +0 -0
- agent_cli/rag/__init__.py +3 -0
- agent_cli/rag/_indexer.py +67 -0
- agent_cli/rag/_indexing.py +226 -0
- agent_cli/rag/_prompt.py +30 -0
- agent_cli/rag/_retriever.py +156 -0
- agent_cli/rag/_store.py +48 -0
- agent_cli/rag/_utils.py +218 -0
- agent_cli/rag/api.py +175 -0
- agent_cli/rag/client.py +299 -0
- agent_cli/rag/engine.py +302 -0
- agent_cli/rag/models.py +55 -0
- agent_cli/scripts/.runtime/.gitkeep +0 -0
- agent_cli/scripts/__init__.py +1 -0
- agent_cli/scripts/check_plugin_skill_sync.py +50 -0
- agent_cli/scripts/linux-hotkeys/README.md +63 -0
- agent_cli/scripts/linux-hotkeys/toggle-autocorrect.sh +45 -0
- agent_cli/scripts/linux-hotkeys/toggle-transcription.sh +58 -0
- agent_cli/scripts/linux-hotkeys/toggle-voice-edit.sh +58 -0
- agent_cli/scripts/macos-hotkeys/README.md +45 -0
- agent_cli/scripts/macos-hotkeys/skhd-config-example +5 -0
- agent_cli/scripts/macos-hotkeys/toggle-autocorrect.sh +12 -0
- agent_cli/scripts/macos-hotkeys/toggle-transcription.sh +37 -0
- agent_cli/scripts/macos-hotkeys/toggle-voice-edit.sh +37 -0
- agent_cli/scripts/nvidia-asr-server/README.md +99 -0
- agent_cli/scripts/nvidia-asr-server/pyproject.toml +27 -0
- agent_cli/scripts/nvidia-asr-server/server.py +255 -0
- agent_cli/scripts/nvidia-asr-server/shell.nix +32 -0
- agent_cli/scripts/nvidia-asr-server/uv.lock +4654 -0
- agent_cli/scripts/run-openwakeword.sh +11 -0
- agent_cli/scripts/run-piper-windows.ps1 +30 -0
- agent_cli/scripts/run-piper.sh +24 -0
- agent_cli/scripts/run-whisper-linux.sh +40 -0
- agent_cli/scripts/run-whisper-macos.sh +6 -0
- agent_cli/scripts/run-whisper-windows.ps1 +51 -0
- agent_cli/scripts/run-whisper.sh +9 -0
- agent_cli/scripts/run_faster_whisper_server.py +136 -0
- agent_cli/scripts/setup-linux-hotkeys.sh +72 -0
- agent_cli/scripts/setup-linux.sh +108 -0
- agent_cli/scripts/setup-macos-hotkeys.sh +61 -0
- agent_cli/scripts/setup-macos.sh +76 -0
- agent_cli/scripts/setup-windows.ps1 +63 -0
- agent_cli/scripts/start-all-services-windows.ps1 +53 -0
- agent_cli/scripts/start-all-services.sh +178 -0
- agent_cli/scripts/sync_extras.py +138 -0
- agent_cli/server/__init__.py +3 -0
- agent_cli/server/cli.py +721 -0
- agent_cli/server/common.py +222 -0
- agent_cli/server/model_manager.py +288 -0
- agent_cli/server/model_registry.py +225 -0
- agent_cli/server/proxy/__init__.py +3 -0
- agent_cli/server/proxy/api.py +444 -0
- agent_cli/server/streaming.py +67 -0
- agent_cli/server/tts/__init__.py +3 -0
- agent_cli/server/tts/api.py +335 -0
- agent_cli/server/tts/backends/__init__.py +82 -0
- agent_cli/server/tts/backends/base.py +139 -0
- agent_cli/server/tts/backends/kokoro.py +403 -0
- agent_cli/server/tts/backends/piper.py +253 -0
- agent_cli/server/tts/model_manager.py +201 -0
- agent_cli/server/tts/model_registry.py +28 -0
- agent_cli/server/tts/wyoming_handler.py +249 -0
- agent_cli/server/whisper/__init__.py +3 -0
- agent_cli/server/whisper/api.py +413 -0
- agent_cli/server/whisper/backends/__init__.py +89 -0
- agent_cli/server/whisper/backends/base.py +97 -0
- agent_cli/server/whisper/backends/faster_whisper.py +225 -0
- agent_cli/server/whisper/backends/mlx.py +270 -0
- agent_cli/server/whisper/languages.py +116 -0
- agent_cli/server/whisper/model_manager.py +157 -0
- agent_cli/server/whisper/model_registry.py +28 -0
- agent_cli/server/whisper/wyoming_handler.py +203 -0
- agent_cli/services/__init__.py +343 -0
- agent_cli/services/_wyoming_utils.py +64 -0
- agent_cli/services/asr.py +506 -0
- agent_cli/services/llm.py +228 -0
- agent_cli/services/tts.py +450 -0
- agent_cli/services/wake_word.py +142 -0
- agent_cli-0.70.5.dist-info/METADATA +2118 -0
- agent_cli-0.70.5.dist-info/RECORD +196 -0
- agent_cli-0.70.5.dist-info/WHEEL +4 -0
- agent_cli-0.70.5.dist-info/entry_points.txt +4 -0
- agent_cli-0.70.5.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
"""Install optional extras at runtime with pinned versions."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import shutil
|
|
6
|
+
import subprocess
|
|
7
|
+
import sys
|
|
8
|
+
import tomllib
|
|
9
|
+
from importlib.metadata import version as get_version
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Annotated
|
|
12
|
+
|
|
13
|
+
import typer
|
|
14
|
+
|
|
15
|
+
from agent_cli.cli import app
|
|
16
|
+
from agent_cli.core.deps import EXTRAS as _EXTRAS_META
|
|
17
|
+
from agent_cli.core.utils import console, print_error_message
|
|
18
|
+
|
|
19
|
+
# Extract descriptions from the centralized EXTRAS metadata
|
|
20
|
+
EXTRAS: dict[str, str] = {name: desc for name, (desc, _) in _EXTRAS_META.items()}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _requirements_dir() -> Path:
|
|
24
|
+
return Path(__file__).parent.parent / "_requirements"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _available_extras() -> list[str]:
|
|
28
|
+
"""List available extras based on requirements files."""
|
|
29
|
+
req_dir = _requirements_dir()
|
|
30
|
+
if not req_dir.exists():
|
|
31
|
+
return []
|
|
32
|
+
return sorted(p.stem for p in req_dir.glob("*.txt"))
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _requirements_path(extra: str) -> Path:
|
|
36
|
+
return _requirements_dir() / f"{extra}.txt"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _in_virtualenv() -> bool:
|
|
40
|
+
"""Check if running inside a virtual environment."""
|
|
41
|
+
return sys.prefix != sys.base_prefix
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _is_uv_tool_install() -> bool:
|
|
45
|
+
"""Check if running from a uv tool environment."""
|
|
46
|
+
receipt = Path(sys.prefix) / "uv-receipt.toml"
|
|
47
|
+
return receipt.exists()
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _get_current_uv_tool_extras() -> list[str]:
|
|
51
|
+
"""Get extras currently configured in uv-receipt.toml."""
|
|
52
|
+
receipt = Path(sys.prefix) / "uv-receipt.toml"
|
|
53
|
+
if not receipt.exists():
|
|
54
|
+
return []
|
|
55
|
+
data = tomllib.loads(receipt.read_text())
|
|
56
|
+
requirements = data.get("tool", {}).get("requirements", [])
|
|
57
|
+
for req in requirements:
|
|
58
|
+
if req.get("name") == "agent-cli":
|
|
59
|
+
return req.get("extras", [])
|
|
60
|
+
return []
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _install_via_uv_tool(extras: list[str], *, quiet: bool = False) -> bool:
|
|
64
|
+
"""Reinstall agent-cli via uv tool with the specified extras."""
|
|
65
|
+
current_version = get_version("agent-cli").split("+")[0] # Strip local version
|
|
66
|
+
extras_str = ",".join(extras)
|
|
67
|
+
package_spec = f"agent-cli[{extras_str}]=={current_version}"
|
|
68
|
+
python_version = f"{sys.version_info.major}.{sys.version_info.minor}"
|
|
69
|
+
cmd = ["uv", "tool", "install", package_spec, "--force", "--python", python_version]
|
|
70
|
+
if quiet:
|
|
71
|
+
cmd.append("-q")
|
|
72
|
+
console.print(f"Running: [cyan]{' '.join(cmd)}[/]")
|
|
73
|
+
result = subprocess.run(cmd, check=False)
|
|
74
|
+
return result.returncode == 0
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _install_cmd() -> list[str]:
|
|
78
|
+
"""Build the install command with appropriate flags."""
|
|
79
|
+
in_venv = _in_virtualenv()
|
|
80
|
+
if shutil.which("uv"):
|
|
81
|
+
cmd = ["uv", "pip", "install", "--python", sys.executable]
|
|
82
|
+
if not in_venv:
|
|
83
|
+
# Allow installing to system Python when not in a venv
|
|
84
|
+
cmd.append("--system")
|
|
85
|
+
return cmd
|
|
86
|
+
cmd = [sys.executable, "-m", "pip", "install"]
|
|
87
|
+
if not in_venv:
|
|
88
|
+
# Install to user site-packages when not in a venv
|
|
89
|
+
cmd.append("--user")
|
|
90
|
+
return cmd
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _install_extras_impl(extras: list[str], *, quiet: bool = False) -> bool:
|
|
94
|
+
"""Install extras. Returns True on success, False on failure."""
|
|
95
|
+
if _is_uv_tool_install():
|
|
96
|
+
current_extras = _get_current_uv_tool_extras()
|
|
97
|
+
new_extras = sorted(set(current_extras) | set(extras))
|
|
98
|
+
return _install_via_uv_tool(new_extras, quiet=quiet)
|
|
99
|
+
|
|
100
|
+
cmd = _install_cmd()
|
|
101
|
+
for extra in extras:
|
|
102
|
+
req_file = _requirements_path(extra)
|
|
103
|
+
if not quiet:
|
|
104
|
+
console.print(f"Installing [cyan]{extra}[/]...")
|
|
105
|
+
result = subprocess.run(
|
|
106
|
+
[*cmd, "-r", str(req_file)],
|
|
107
|
+
check=False,
|
|
108
|
+
capture_output=quiet,
|
|
109
|
+
)
|
|
110
|
+
if result.returncode != 0:
|
|
111
|
+
return False
|
|
112
|
+
return True
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def install_extras_programmatic(extras: list[str], *, quiet: bool = False) -> bool:
|
|
116
|
+
"""Install extras programmatically (for auto-install feature)."""
|
|
117
|
+
available = _available_extras()
|
|
118
|
+
valid = [e for e in extras if e in available]
|
|
119
|
+
invalid = [e for e in extras if e not in available]
|
|
120
|
+
if invalid:
|
|
121
|
+
console.print(f"[yellow]Unknown extras (skipped): {', '.join(invalid)}[/]")
|
|
122
|
+
return bool(valid) and _install_extras_impl(valid, quiet=quiet)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
@app.command("install-extras", rich_help_panel="Installation", no_args_is_help=True)
|
|
126
|
+
def install_extras(
|
|
127
|
+
extras: Annotated[list[str] | None, typer.Argument(help="Extras to install")] = None,
|
|
128
|
+
list_extras: Annotated[
|
|
129
|
+
bool,
|
|
130
|
+
typer.Option("--list", "-l", help="List available extras"),
|
|
131
|
+
] = False,
|
|
132
|
+
all_extras: Annotated[
|
|
133
|
+
bool,
|
|
134
|
+
typer.Option("--all", "-a", help="Install all available extras"),
|
|
135
|
+
] = False,
|
|
136
|
+
) -> None:
|
|
137
|
+
"""Install optional extras (rag, memory, vad, etc.) with pinned versions.
|
|
138
|
+
|
|
139
|
+
Examples:
|
|
140
|
+
- `agent-cli install-extras rag` # Install RAG dependencies
|
|
141
|
+
- `agent-cli install-extras memory vad` # Install multiple extras
|
|
142
|
+
- `agent-cli install-extras --list` # Show available extras
|
|
143
|
+
- `agent-cli install-extras --all` # Install all extras
|
|
144
|
+
|
|
145
|
+
"""
|
|
146
|
+
available = _available_extras()
|
|
147
|
+
|
|
148
|
+
if list_extras:
|
|
149
|
+
console.print("[bold]Available extras:[/]")
|
|
150
|
+
for name in available:
|
|
151
|
+
desc = EXTRAS.get(name, "")
|
|
152
|
+
console.print(f" [cyan]{name}[/]: {desc}")
|
|
153
|
+
return
|
|
154
|
+
|
|
155
|
+
if all_extras:
|
|
156
|
+
extras = available
|
|
157
|
+
|
|
158
|
+
if not extras:
|
|
159
|
+
print_error_message("No extras specified. Use --list to see available, or --all.")
|
|
160
|
+
raise typer.Exit(1)
|
|
161
|
+
|
|
162
|
+
invalid = [e for e in extras if e not in available]
|
|
163
|
+
if invalid:
|
|
164
|
+
print_error_message(f"Unknown extras: {invalid}. Use --list to see available.")
|
|
165
|
+
raise typer.Exit(1)
|
|
166
|
+
|
|
167
|
+
if not _install_extras_impl(extras):
|
|
168
|
+
print_error_message("Failed to install extras")
|
|
169
|
+
raise typer.Exit(1)
|
|
170
|
+
|
|
171
|
+
if _is_uv_tool_install():
|
|
172
|
+
console.print("[green]Done! Extras will persist across uv tool upgrade.[/]")
|
|
173
|
+
else:
|
|
174
|
+
console.print("[green]Done![/]")
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""Hotkey installation commands."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import platform
|
|
6
|
+
|
|
7
|
+
from agent_cli.cli import app
|
|
8
|
+
from agent_cli.core.utils import print_with_style
|
|
9
|
+
from agent_cli.install.common import execute_installation_script, get_platform_script
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@app.command("install-hotkeys", rich_help_panel="Installation")
|
|
13
|
+
def install_hotkeys() -> None:
|
|
14
|
+
"""Install system-wide hotkeys for agent-cli commands.
|
|
15
|
+
|
|
16
|
+
Sets up the following hotkeys:
|
|
17
|
+
|
|
18
|
+
macOS:
|
|
19
|
+
- Cmd+Shift+R: Toggle voice transcription
|
|
20
|
+
- Cmd+Shift+A: Autocorrect clipboard text
|
|
21
|
+
- Cmd+Shift+V: Voice edit clipboard text
|
|
22
|
+
|
|
23
|
+
Linux:
|
|
24
|
+
- Super+Shift+R: Toggle voice transcription
|
|
25
|
+
- Super+Shift+A: Autocorrect clipboard text
|
|
26
|
+
- Super+Shift+V: Voice edit clipboard text
|
|
27
|
+
|
|
28
|
+
Note: On macOS, you may need to grant Accessibility permissions to skhd
|
|
29
|
+
in System Settings → Privacy & Security → Accessibility.
|
|
30
|
+
"""
|
|
31
|
+
script_name = get_platform_script("setup-macos-hotkeys.sh", "setup-linux-hotkeys.sh")
|
|
32
|
+
system = platform.system().lower()
|
|
33
|
+
|
|
34
|
+
execute_installation_script(
|
|
35
|
+
script_name=script_name,
|
|
36
|
+
operation_name="Set up hotkeys",
|
|
37
|
+
success_message="Hotkeys installed successfully!",
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
# Post-installation steps for macOS
|
|
41
|
+
if system == "darwin":
|
|
42
|
+
print_with_style("\n⚠️ Important:", "yellow")
|
|
43
|
+
print_with_style("If hotkeys don't work, grant Accessibility permissions:", "yellow")
|
|
44
|
+
print_with_style(
|
|
45
|
+
" 1. Open System Settings → Privacy & Security → Accessibility",
|
|
46
|
+
"cyan",
|
|
47
|
+
)
|
|
48
|
+
print_with_style(" 2. Add and enable 'skhd'", "cyan")
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""Service installation and management commands."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import subprocess
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
|
|
10
|
+
from agent_cli.cli import app
|
|
11
|
+
from agent_cli.core.utils import console, print_error_message, print_with_style
|
|
12
|
+
from agent_cli.install.common import (
|
|
13
|
+
execute_installation_script,
|
|
14
|
+
get_platform_script,
|
|
15
|
+
get_script_path,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@app.command("install-services", rich_help_panel="Installation")
|
|
20
|
+
def install_services() -> None:
|
|
21
|
+
"""Install all required services (Ollama, Whisper, Piper, OpenWakeWord).
|
|
22
|
+
|
|
23
|
+
This command installs:
|
|
24
|
+
- Ollama (local LLM server)
|
|
25
|
+
- Wyoming Faster Whisper (speech-to-text)
|
|
26
|
+
- Wyoming Piper (text-to-speech)
|
|
27
|
+
- Wyoming OpenWakeWord (wake word detection)
|
|
28
|
+
|
|
29
|
+
The appropriate installation method is used based on your operating system.
|
|
30
|
+
"""
|
|
31
|
+
script_name = get_platform_script("setup-macos.sh", "setup-linux.sh")
|
|
32
|
+
|
|
33
|
+
execute_installation_script(
|
|
34
|
+
script_name=script_name,
|
|
35
|
+
operation_name="Install services",
|
|
36
|
+
success_message="Services installed successfully!",
|
|
37
|
+
next_steps=[
|
|
38
|
+
"Start services: agent-cli start-services",
|
|
39
|
+
"Set up hotkeys: agent-cli install-hotkeys",
|
|
40
|
+
],
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@app.command("start-services", rich_help_panel="Service Management")
|
|
45
|
+
def start_services(
|
|
46
|
+
attach: bool = typer.Option(
|
|
47
|
+
True, # noqa: FBT003
|
|
48
|
+
"--attach/--no-attach",
|
|
49
|
+
help="Attach to Zellij session after starting",
|
|
50
|
+
),
|
|
51
|
+
) -> None:
|
|
52
|
+
"""Start all agent-cli services in a Zellij session.
|
|
53
|
+
|
|
54
|
+
This starts:
|
|
55
|
+
- Ollama (LLM server)
|
|
56
|
+
- Wyoming Faster Whisper (speech-to-text)
|
|
57
|
+
- Wyoming Piper (text-to-speech)
|
|
58
|
+
- Wyoming OpenWakeWord (wake word detection)
|
|
59
|
+
|
|
60
|
+
Services run in a Zellij terminal multiplexer session named 'agent-cli'.
|
|
61
|
+
Use Ctrl-Q to quit or Ctrl-O d to detach from the session.
|
|
62
|
+
"""
|
|
63
|
+
try:
|
|
64
|
+
script_path = get_script_path("start-all-services.sh")
|
|
65
|
+
except FileNotFoundError as e:
|
|
66
|
+
print_error_message("Service scripts not found")
|
|
67
|
+
console.print(str(e))
|
|
68
|
+
raise typer.Exit(1) from None
|
|
69
|
+
|
|
70
|
+
env = os.environ.copy()
|
|
71
|
+
if not attach:
|
|
72
|
+
env["AGENT_CLI_NO_ATTACH"] = "true"
|
|
73
|
+
|
|
74
|
+
try:
|
|
75
|
+
subprocess.run([str(script_path)], check=True, env=env)
|
|
76
|
+
if not attach:
|
|
77
|
+
print_with_style("✅ Services started in background.", "green")
|
|
78
|
+
print_with_style("Run 'zellij attach agent-cli' to view the session.", "yellow")
|
|
79
|
+
else:
|
|
80
|
+
# If we get here with attach=True, user likely detached
|
|
81
|
+
print_with_style("\n👋 Detached from Zellij session.")
|
|
82
|
+
print_with_style(
|
|
83
|
+
"Services are still running. Use 'zellij attach agent-cli' to reattach.",
|
|
84
|
+
)
|
|
85
|
+
except subprocess.CalledProcessError as e:
|
|
86
|
+
print_error_message(f"Failed to start services. Exit code: {e.returncode}")
|
|
87
|
+
raise typer.Exit(e.returncode) from None
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
"""File-backed memory helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import logging
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import TYPE_CHECKING
|
|
10
|
+
from uuid import uuid4
|
|
11
|
+
|
|
12
|
+
from pydantic import ValidationError
|
|
13
|
+
|
|
14
|
+
from agent_cli.core.utils import atomic_write_text
|
|
15
|
+
from agent_cli.memory.models import MemoryMetadata
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from collections.abc import Iterable
|
|
19
|
+
|
|
20
|
+
LOGGER = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
_ENTRIES_DIRNAME = "entries"
|
|
23
|
+
_SNAPSHOT_FILENAME = "memory_index.json"
|
|
24
|
+
_DELETED_DIRNAME = "deleted"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class MemoryFileRecord:
|
|
29
|
+
"""Materialized memory file on disk."""
|
|
30
|
+
|
|
31
|
+
id: str
|
|
32
|
+
path: Path
|
|
33
|
+
metadata: MemoryMetadata
|
|
34
|
+
content: str
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def ensure_store_dirs(root: Path) -> tuple[Path, Path]:
|
|
38
|
+
"""Ensure base folders exist and return (entries_dir, snapshot_path)."""
|
|
39
|
+
entries_dir = root / _ENTRIES_DIRNAME
|
|
40
|
+
entries_dir.mkdir(parents=True, exist_ok=True)
|
|
41
|
+
snapshot_path = root / _SNAPSHOT_FILENAME
|
|
42
|
+
return entries_dir, snapshot_path
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _safe_timestamp(value: str) -> str:
|
|
46
|
+
"""Return a filesystem-safe timestamp-ish token."""
|
|
47
|
+
return "".join(ch if ch.isalnum() or ch in "-._" else "-" for ch in value) or "ts"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def soft_delete_memory_file(
|
|
51
|
+
path: Path,
|
|
52
|
+
entries_dir: Path,
|
|
53
|
+
replaced_by: str | None = None,
|
|
54
|
+
) -> None:
|
|
55
|
+
"""Move a memory file to the deleted directory, updating metadata if replaced."""
|
|
56
|
+
try:
|
|
57
|
+
rel_path = path.relative_to(entries_dir)
|
|
58
|
+
except ValueError:
|
|
59
|
+
# Not in entries_dir? Just unlink.
|
|
60
|
+
path.unlink(missing_ok=True)
|
|
61
|
+
return
|
|
62
|
+
|
|
63
|
+
# Prepare destination
|
|
64
|
+
dest_path = entries_dir / _DELETED_DIRNAME / rel_path
|
|
65
|
+
dest_path.parent.mkdir(parents=True, exist_ok=True)
|
|
66
|
+
|
|
67
|
+
# If we need to update metadata, read-modify-write
|
|
68
|
+
if replaced_by:
|
|
69
|
+
record = read_memory_file(path)
|
|
70
|
+
if record:
|
|
71
|
+
record.metadata.replaced_by = replaced_by
|
|
72
|
+
front_matter = _render_front_matter(record.id, record.metadata)
|
|
73
|
+
body = front_matter + "\n" + record.content + "\n"
|
|
74
|
+
atomic_write_text(dest_path, body)
|
|
75
|
+
path.unlink(missing_ok=True)
|
|
76
|
+
return
|
|
77
|
+
|
|
78
|
+
# Simple move if no metadata change
|
|
79
|
+
path.rename(dest_path)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def write_memory_file(
|
|
83
|
+
root: Path,
|
|
84
|
+
*,
|
|
85
|
+
conversation_id: str,
|
|
86
|
+
role: str,
|
|
87
|
+
created_at: str,
|
|
88
|
+
content: str,
|
|
89
|
+
summary_kind: str | None = None,
|
|
90
|
+
doc_id: str | None = None,
|
|
91
|
+
source_id: str | None = None,
|
|
92
|
+
) -> MemoryFileRecord:
|
|
93
|
+
"""Render and persist a memory document to disk."""
|
|
94
|
+
entries_dir, _ = ensure_store_dirs(root)
|
|
95
|
+
safe_conversation = _slugify(conversation_id)
|
|
96
|
+
doc_id = doc_id or str(uuid4())
|
|
97
|
+
safe_ts = _safe_timestamp(created_at)
|
|
98
|
+
|
|
99
|
+
# Route by role/category for readability
|
|
100
|
+
if summary_kind:
|
|
101
|
+
subdir = Path("summaries")
|
|
102
|
+
filename = "summary.md"
|
|
103
|
+
elif role == "user":
|
|
104
|
+
subdir = Path("turns") / "user"
|
|
105
|
+
filename = f"{safe_ts}__{doc_id}.md"
|
|
106
|
+
elif role == "assistant":
|
|
107
|
+
subdir = Path("turns") / "assistant"
|
|
108
|
+
filename = f"{safe_ts}__{doc_id}.md"
|
|
109
|
+
elif role == "memory":
|
|
110
|
+
subdir = Path("facts")
|
|
111
|
+
filename = f"{safe_ts}__{doc_id}.md"
|
|
112
|
+
else:
|
|
113
|
+
subdir = Path()
|
|
114
|
+
filename = f"{doc_id}.md"
|
|
115
|
+
|
|
116
|
+
metadata = MemoryMetadata(
|
|
117
|
+
conversation_id=conversation_id,
|
|
118
|
+
role=role,
|
|
119
|
+
created_at=created_at,
|
|
120
|
+
summary_kind=summary_kind,
|
|
121
|
+
source_id=source_id,
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
front_matter = _render_front_matter(doc_id, metadata)
|
|
125
|
+
body = front_matter + "\n" + content.strip() + "\n"
|
|
126
|
+
|
|
127
|
+
file_path = entries_dir / safe_conversation / subdir / filename
|
|
128
|
+
file_path.parent.mkdir(parents=True, exist_ok=True)
|
|
129
|
+
|
|
130
|
+
atomic_write_text(file_path, body)
|
|
131
|
+
|
|
132
|
+
return MemoryFileRecord(id=doc_id, path=file_path, metadata=metadata, content=content)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def load_memory_files(root: Path) -> list[MemoryFileRecord]:
|
|
136
|
+
"""Load all memory files from disk."""
|
|
137
|
+
entries_dir, _ = ensure_store_dirs(root)
|
|
138
|
+
records: list[MemoryFileRecord] = []
|
|
139
|
+
for path in entries_dir.rglob("*.md"):
|
|
140
|
+
# Skip anything under a deleted tombstone folder.
|
|
141
|
+
if _DELETED_DIRNAME in path.parts:
|
|
142
|
+
continue
|
|
143
|
+
rec = read_memory_file(path)
|
|
144
|
+
if rec:
|
|
145
|
+
records.append(rec)
|
|
146
|
+
return records
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def read_memory_file(path: Path) -> MemoryFileRecord | None:
|
|
150
|
+
"""Parse a single memory file; return None if invalid."""
|
|
151
|
+
try:
|
|
152
|
+
text = path.read_text(encoding="utf-8")
|
|
153
|
+
except Exception:
|
|
154
|
+
LOGGER.warning("Failed to read memory file %s", path, exc_info=True)
|
|
155
|
+
return None
|
|
156
|
+
|
|
157
|
+
fm, body = _split_front_matter(text)
|
|
158
|
+
if fm is None:
|
|
159
|
+
LOGGER.warning("Memory file %s missing front matter; skipping", path)
|
|
160
|
+
return None
|
|
161
|
+
|
|
162
|
+
doc_id = fm.pop("id", None)
|
|
163
|
+
if not doc_id:
|
|
164
|
+
LOGGER.warning("Memory file %s missing id; skipping", path)
|
|
165
|
+
return None
|
|
166
|
+
|
|
167
|
+
try:
|
|
168
|
+
metadata = MemoryMetadata(**fm)
|
|
169
|
+
except ValidationError:
|
|
170
|
+
LOGGER.warning("Memory file %s has invalid metadata; skipping", path, exc_info=True)
|
|
171
|
+
return None
|
|
172
|
+
|
|
173
|
+
return MemoryFileRecord(id=str(doc_id), path=path, metadata=metadata, content=body.strip())
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def write_snapshot(snapshot_path: Path, records: Iterable[MemoryFileRecord]) -> None:
|
|
177
|
+
"""Write a JSON snapshot of current memories for easy inspection."""
|
|
178
|
+
payload = [
|
|
179
|
+
{
|
|
180
|
+
"id": rec.id,
|
|
181
|
+
"path": str(rec.path),
|
|
182
|
+
"metadata": rec.metadata.model_dump(exclude_none=True),
|
|
183
|
+
"content": rec.content,
|
|
184
|
+
}
|
|
185
|
+
for rec in records
|
|
186
|
+
]
|
|
187
|
+
|
|
188
|
+
atomic_write_text(snapshot_path, json.dumps(payload, ensure_ascii=False, indent=2))
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def load_snapshot(snapshot_path: Path) -> dict[str, MemoryFileRecord]:
|
|
192
|
+
"""Load snapshot into a mapping from id to record (if the snapshot exists)."""
|
|
193
|
+
if not snapshot_path.exists():
|
|
194
|
+
return {}
|
|
195
|
+
try:
|
|
196
|
+
data = json.loads(snapshot_path.read_text(encoding="utf-8"))
|
|
197
|
+
except Exception:
|
|
198
|
+
LOGGER.warning("Failed to read memory snapshot %s", snapshot_path, exc_info=True)
|
|
199
|
+
return {}
|
|
200
|
+
|
|
201
|
+
records: dict[str, MemoryFileRecord] = {}
|
|
202
|
+
for item in data:
|
|
203
|
+
try:
|
|
204
|
+
metadata = MemoryMetadata(**item["metadata"])
|
|
205
|
+
record = MemoryFileRecord(
|
|
206
|
+
id=str(item["id"]),
|
|
207
|
+
path=Path(item["path"]),
|
|
208
|
+
metadata=metadata,
|
|
209
|
+
content=str(item.get("content") or ""),
|
|
210
|
+
)
|
|
211
|
+
records[record.id] = record
|
|
212
|
+
except Exception:
|
|
213
|
+
LOGGER.warning("Invalid snapshot entry; skipping", exc_info=True)
|
|
214
|
+
continue
|
|
215
|
+
return records
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _render_front_matter(doc_id: str, metadata: MemoryMetadata) -> str:
|
|
219
|
+
"""Return YAML front matter string."""
|
|
220
|
+
import yaml # noqa: PLC0415
|
|
221
|
+
|
|
222
|
+
meta_dict = metadata.model_dump(exclude_none=True)
|
|
223
|
+
meta_dict = {"id": doc_id, **meta_dict}
|
|
224
|
+
yaml_block = yaml.safe_dump(meta_dict, sort_keys=False)
|
|
225
|
+
return f"---\n{yaml_block}---"
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def _split_front_matter(text: str) -> tuple[dict | None, str]:
|
|
229
|
+
"""Split YAML front matter from body."""
|
|
230
|
+
if not text.startswith("---"):
|
|
231
|
+
return None, text
|
|
232
|
+
end = text.find("\n---", 3)
|
|
233
|
+
if end == -1:
|
|
234
|
+
return None, text
|
|
235
|
+
yaml_part = text[3:end]
|
|
236
|
+
try:
|
|
237
|
+
import yaml # noqa: PLC0415
|
|
238
|
+
|
|
239
|
+
meta = yaml.safe_load(yaml_part) or {}
|
|
240
|
+
except Exception:
|
|
241
|
+
return None, text
|
|
242
|
+
body_start = end + len("\n---")
|
|
243
|
+
body = text[body_start:].lstrip("\n")
|
|
244
|
+
return meta, body
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def _slugify(value: str) -> str:
|
|
248
|
+
"""Filesystem-safe slug for folder names."""
|
|
249
|
+
safe = "".join(ch if ch.isalnum() or ch in "-._" else "_" for ch in value)
|
|
250
|
+
return safe or "default"
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""Filter conversion utilities for ChromaDB."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _convert_condition(key: str, value: Any) -> dict[str, Any] | None:
|
|
9
|
+
"""Convert a single filter condition to ChromaDB format."""
|
|
10
|
+
if isinstance(value, dict):
|
|
11
|
+
# Operator dict: {"gte": 10} → {"$gte": 10}
|
|
12
|
+
for op, val in value.items():
|
|
13
|
+
chroma_op = f"${op}" if not op.startswith("$") else op
|
|
14
|
+
return {key: {chroma_op: val}}
|
|
15
|
+
return None
|
|
16
|
+
# Simple equality
|
|
17
|
+
return {key: {"$eq": value}}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _process_or(conditions: list[dict[str, Any]]) -> dict[str, Any] | None:
|
|
21
|
+
"""Process $or conditions."""
|
|
22
|
+
or_conditions = []
|
|
23
|
+
for cond in conditions:
|
|
24
|
+
for sub_key, sub_val in cond.items():
|
|
25
|
+
converted = _convert_condition(sub_key, sub_val)
|
|
26
|
+
if converted:
|
|
27
|
+
or_conditions.append(converted)
|
|
28
|
+
if len(or_conditions) > 1:
|
|
29
|
+
return {"$or": or_conditions}
|
|
30
|
+
if or_conditions:
|
|
31
|
+
return or_conditions[0]
|
|
32
|
+
return None
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def to_chroma_where(filters: dict[str, Any] | None) -> dict[str, Any] | None:
|
|
36
|
+
"""Convert universal filter format to ChromaDB WHERE clause.
|
|
37
|
+
|
|
38
|
+
Supports:
|
|
39
|
+
- Simple equality: {"role": "user"} → {"role": {"$eq": "user"}}
|
|
40
|
+
- Operators: {"created_at": {"gte": "2024-01-01"}} → {"created_at": {"$gte": "2024-01-01"}}
|
|
41
|
+
- Logical OR: {"$or": [{"role": "user"}, {"role": "assistant"}]}
|
|
42
|
+
|
|
43
|
+
Operators: eq, ne, gt, gte, lt, lte, in, nin
|
|
44
|
+
"""
|
|
45
|
+
if not filters:
|
|
46
|
+
return None
|
|
47
|
+
|
|
48
|
+
processed: list[dict[str, Any]] = []
|
|
49
|
+
for key, value in filters.items():
|
|
50
|
+
if key == "$or":
|
|
51
|
+
or_result = _process_or(value)
|
|
52
|
+
if or_result:
|
|
53
|
+
processed.append(or_result)
|
|
54
|
+
elif not key.startswith("$"):
|
|
55
|
+
converted = _convert_condition(key, value)
|
|
56
|
+
if converted:
|
|
57
|
+
processed.append(converted)
|
|
58
|
+
|
|
59
|
+
if not processed:
|
|
60
|
+
return None
|
|
61
|
+
if len(processed) == 1:
|
|
62
|
+
return processed[0]
|
|
63
|
+
return {"$and": processed}
|