oghma 0.0.1__tar.gz → 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.
- oghma-0.3.0/PKG-INFO +26 -0
- oghma-0.3.0/README.md +89 -0
- oghma-0.3.0/pyproject.toml +62 -0
- oghma-0.3.0/setup.cfg +4 -0
- oghma-0.3.0/src/oghma/__init__.py +1 -0
- oghma-0.3.0/src/oghma/cli.py +342 -0
- oghma-0.3.0/src/oghma/config.py +262 -0
- oghma-0.3.0/src/oghma/daemon.py +198 -0
- oghma-0.3.0/src/oghma/embedder.py +107 -0
- oghma-0.3.0/src/oghma/exporter.py +177 -0
- oghma-0.3.0/src/oghma/extractor.py +180 -0
- oghma-0.3.0/src/oghma/mcp_server.py +112 -0
- oghma-0.3.0/src/oghma/migration.py +63 -0
- oghma-0.3.0/src/oghma/parsers/__init__.py +26 -0
- oghma-0.3.0/src/oghma/parsers/base.py +24 -0
- oghma-0.3.0/src/oghma/parsers/claude_code.py +62 -0
- oghma-0.3.0/src/oghma/parsers/codex.py +84 -0
- oghma-0.3.0/src/oghma/parsers/openclaw.py +64 -0
- oghma-0.3.0/src/oghma/parsers/opencode.py +90 -0
- oghma-0.3.0/src/oghma/storage.py +753 -0
- oghma-0.3.0/src/oghma/watcher.py +97 -0
- oghma-0.3.0/src/oghma.egg-info/PKG-INFO +26 -0
- oghma-0.3.0/src/oghma.egg-info/SOURCES.txt +35 -0
- oghma-0.3.0/src/oghma.egg-info/dependency_links.txt +1 -0
- oghma-0.3.0/src/oghma.egg-info/entry_points.txt +3 -0
- oghma-0.3.0/src/oghma.egg-info/requires.txt +14 -0
- oghma-0.3.0/src/oghma.egg-info/top_level.txt +1 -0
- oghma-0.3.0/tests/test_cli_commands.py +254 -0
- oghma-0.3.0/tests/test_config.py +128 -0
- oghma-0.3.0/tests/test_daemon.py +165 -0
- oghma-0.3.0/tests/test_embedder.py +78 -0
- oghma-0.3.0/tests/test_exporter.py +229 -0
- oghma-0.3.0/tests/test_extractor.py +324 -0
- oghma-0.3.0/tests/test_mcp_server.py +186 -0
- oghma-0.3.0/tests/test_migration.py +73 -0
- oghma-0.3.0/tests/test_storage.py +341 -0
- oghma-0.3.0/tests/test_watcher.py +185 -0
- oghma-0.0.1/PKG-INFO +0 -33
- oghma-0.0.1/README.md +0 -15
- oghma-0.0.1/pyproject.toml +0 -25
- oghma-0.0.1/src/oghma/__init__.py +0 -3
oghma-0.3.0/PKG-INFO
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: oghma
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Unified AI memory layer for coding assistants
|
|
5
|
+
Author: Oghma Contributors
|
|
6
|
+
License: MIT
|
|
7
|
+
Classifier: Development Status :: 3 - Alpha
|
|
8
|
+
Classifier: Intended Audience :: Developers
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Requires-Python: >=3.10
|
|
15
|
+
Requires-Dist: click>=8.0
|
|
16
|
+
Requires-Dist: pyyaml>=6.0
|
|
17
|
+
Requires-Dist: openai>=1.0
|
|
18
|
+
Requires-Dist: rich>=13.0
|
|
19
|
+
Requires-Dist: mcp[cli]>=1.0
|
|
20
|
+
Requires-Dist: sqlite-vec>=0.1.0
|
|
21
|
+
Provides-Extra: local
|
|
22
|
+
Requires-Dist: sentence-transformers>=2.0; extra == "local"
|
|
23
|
+
Provides-Extra: dev
|
|
24
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
25
|
+
Requires-Dist: pytest-cov>=4.0; extra == "dev"
|
|
26
|
+
Requires-Dist: ruff>=0.1; extra == "dev"
|
oghma-0.3.0/README.md
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# Oghma
|
|
2
|
+
|
|
3
|
+
Unified AI memory layer. Watches transcripts from Claude Code, Codex, OpenClaw, and OpenCode. Extracts memories via LLM. Search with FTS5.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install oghma
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
oghma init
|
|
15
|
+
oghma start
|
|
16
|
+
oghma search "python typing"
|
|
17
|
+
oghma export -o ./memories
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Configuration
|
|
21
|
+
|
|
22
|
+
Config at ~/.oghma/config.yaml. Key settings:
|
|
23
|
+
- tools: Enable/disable tool watching
|
|
24
|
+
- daemon.poll_interval: How often to check for changes (default 300s)
|
|
25
|
+
- extraction.model: LLM for memory extraction (default gpt-4o-mini)
|
|
26
|
+
|
|
27
|
+
### Model Selection
|
|
28
|
+
|
|
29
|
+
Oghma supports both OpenAI and OpenRouter models:
|
|
30
|
+
|
|
31
|
+
| Model | Provider | Quality | Cost | Notes |
|
|
32
|
+
|-------|----------|---------|------|-------|
|
|
33
|
+
| gpt-4o-mini | OpenAI | Good | ~$0.30/M | Default, factual |
|
|
34
|
+
| google/gemini-3-flash-preview | OpenRouter | Excellent | ~$1.50/M | Best quality/cost |
|
|
35
|
+
| google/gemini-2.0-flash-001 | OpenRouter | Good | ~$0.25/M | Budget option |
|
|
36
|
+
| deepseek/deepseek-chat-v3-0324 | OpenRouter | Good | ~$0.14/M | Cheapest |
|
|
37
|
+
|
|
38
|
+
To use OpenRouter models, set in config.yaml:
|
|
39
|
+
```yaml
|
|
40
|
+
extraction:
|
|
41
|
+
model: google/gemini-3-flash-preview
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Or via environment: `OGHMA_EXTRACTION_MODEL=google/gemini-3-flash-preview`
|
|
45
|
+
|
|
46
|
+
## Commands
|
|
47
|
+
|
|
48
|
+
| Command | Description |
|
|
49
|
+
|---------|-------------|
|
|
50
|
+
| oghma init | Create default config |
|
|
51
|
+
| oghma status | Show daemon and database status |
|
|
52
|
+
| oghma start | Start background daemon |
|
|
53
|
+
| oghma stop | Stop daemon |
|
|
54
|
+
| oghma search "query" | Search memories |
|
|
55
|
+
| oghma export | Export memories to files |
|
|
56
|
+
|
|
57
|
+
## MCP Server
|
|
58
|
+
|
|
59
|
+
Native Claude Code integration via MCP. Add to `~/.claude.json`:
|
|
60
|
+
|
|
61
|
+
```json
|
|
62
|
+
{
|
|
63
|
+
"mcpServers": {
|
|
64
|
+
"oghma": {
|
|
65
|
+
"command": "uvx",
|
|
66
|
+
"args": ["--from", "oghma", "oghma", "mcp-server"]
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Available tools:
|
|
73
|
+
- `oghma_search`: Search memories by keyword (supports category/source_tool filters)
|
|
74
|
+
- `ogma_get`: Get a memory by ID
|
|
75
|
+
- `oghma_stats`: Get memory database statistics
|
|
76
|
+
- `oghma_categories`: List categories with memory counts
|
|
77
|
+
|
|
78
|
+
## Environment Variables
|
|
79
|
+
|
|
80
|
+
- OGHMA_DB_PATH: Override database path
|
|
81
|
+
- OGHMA_POLL_INTERVAL: Override poll interval
|
|
82
|
+
- OGHMA_LOG_LEVEL: Set log level (DEBUG/INFO/WARNING/ERROR)
|
|
83
|
+
- OGHMA_EXTRACTION_MODEL: Override extraction model
|
|
84
|
+
- OPENAI_API_KEY: Required for OpenAI models (gpt-4o-mini)
|
|
85
|
+
- OPENROUTER_API_KEY: Required for OpenRouter models (Gemini, DeepSeek, etc.)
|
|
86
|
+
|
|
87
|
+
## License
|
|
88
|
+
|
|
89
|
+
MIT
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "oghma"
|
|
7
|
+
version = "0.3.0"
|
|
8
|
+
description = "Unified AI memory layer for coding assistants"
|
|
9
|
+
license = {text = "MIT"}
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
authors = [
|
|
12
|
+
{name = "Oghma Contributors"},
|
|
13
|
+
]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 3 - Alpha",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Programming Language :: Python :: 3.10",
|
|
20
|
+
"Programming Language :: Python :: 3.11",
|
|
21
|
+
"Programming Language :: Python :: 3.12",
|
|
22
|
+
]
|
|
23
|
+
dependencies = [
|
|
24
|
+
"click>=8.0",
|
|
25
|
+
"pyyaml>=6.0",
|
|
26
|
+
"openai>=1.0",
|
|
27
|
+
"rich>=13.0",
|
|
28
|
+
"mcp[cli]>=1.0",
|
|
29
|
+
"sqlite-vec>=0.1.0",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
[project.optional-dependencies]
|
|
33
|
+
local = [
|
|
34
|
+
"sentence-transformers>=2.0",
|
|
35
|
+
]
|
|
36
|
+
dev = [
|
|
37
|
+
"pytest>=8.0",
|
|
38
|
+
"pytest-cov>=4.0",
|
|
39
|
+
"ruff>=0.1",
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
[project.scripts]
|
|
43
|
+
oghma = "oghma.cli:main"
|
|
44
|
+
oghma-mcp = "oghma.mcp_server:main"
|
|
45
|
+
|
|
46
|
+
[tool.ruff]
|
|
47
|
+
target-version = "py310"
|
|
48
|
+
line-length = 100
|
|
49
|
+
|
|
50
|
+
[tool.ruff.lint]
|
|
51
|
+
select = ["E", "F", "I", "N", "W", "UP", "B", "C4"]
|
|
52
|
+
ignore = []
|
|
53
|
+
|
|
54
|
+
[tool.ruff.format]
|
|
55
|
+
quote-style = "double"
|
|
56
|
+
indent-style = "space"
|
|
57
|
+
|
|
58
|
+
[tool.pytest.ini_options]
|
|
59
|
+
testpaths = ["tests"]
|
|
60
|
+
python_files = ["test_*.py"]
|
|
61
|
+
python_classes = ["Test*"]
|
|
62
|
+
python_functions = ["test_*"]
|
oghma-0.3.0/setup.cfg
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.3.0"
|
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import signal
|
|
3
|
+
import time
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import click
|
|
7
|
+
from rich.console import Console
|
|
8
|
+
from rich.table import Table
|
|
9
|
+
|
|
10
|
+
from oghma import __version__
|
|
11
|
+
from oghma.config import (
|
|
12
|
+
create_default_config,
|
|
13
|
+
get_config_path,
|
|
14
|
+
load_config,
|
|
15
|
+
validate_config,
|
|
16
|
+
)
|
|
17
|
+
from oghma.daemon import Daemon, get_daemon_pid
|
|
18
|
+
from oghma.embedder import EmbedConfig, create_embedder
|
|
19
|
+
from oghma.exporter import Exporter, ExportOptions
|
|
20
|
+
from oghma.migration import EmbeddingMigration
|
|
21
|
+
from oghma.storage import Storage
|
|
22
|
+
|
|
23
|
+
console = Console()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@click.group()
|
|
27
|
+
@click.version_option(version=__version__, prog_name="oghma")
|
|
28
|
+
def cli() -> None:
|
|
29
|
+
pass
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@cli.command()
|
|
33
|
+
def init() -> None:
|
|
34
|
+
config_path = get_config_path()
|
|
35
|
+
|
|
36
|
+
if config_path.exists():
|
|
37
|
+
console.print(f"[yellow]Config already exists at {config_path}[/yellow]")
|
|
38
|
+
if not click.confirm("Overwrite existing config?"):
|
|
39
|
+
console.print("[green]Init cancelled[/green]")
|
|
40
|
+
return
|
|
41
|
+
|
|
42
|
+
console.print("[blue]Creating Oghma configuration...[/blue]")
|
|
43
|
+
config = create_default_config()
|
|
44
|
+
console.print(f"[green]Config created at {config_path}[/green]")
|
|
45
|
+
console.print(f"[cyan]Database path: {config['storage']['db_path']}[/cyan]")
|
|
46
|
+
console.print("\n[yellow]Run 'oghma status' to verify setup[/yellow]")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@cli.command()
|
|
50
|
+
def status() -> None:
|
|
51
|
+
try:
|
|
52
|
+
config_path = get_config_path()
|
|
53
|
+
config = load_config()
|
|
54
|
+
db_path = config["storage"]["db_path"]
|
|
55
|
+
pid_file = config["daemon"]["pid_file"]
|
|
56
|
+
|
|
57
|
+
table = Table(title="Oghma Status", show_header=True, header_style="bold magenta")
|
|
58
|
+
table.add_column("Property", style="cyan")
|
|
59
|
+
table.add_column("Value", style="green")
|
|
60
|
+
|
|
61
|
+
table.add_row("Config Path", str(config_path))
|
|
62
|
+
|
|
63
|
+
pid = get_daemon_pid(pid_file)
|
|
64
|
+
if pid:
|
|
65
|
+
table.add_row("Daemon Status", f"[green]Running (PID: {pid})[/green]")
|
|
66
|
+
else:
|
|
67
|
+
table.add_row("Daemon Status", "[red]Stopped[/red]")
|
|
68
|
+
|
|
69
|
+
table.add_row("Database Path", db_path)
|
|
70
|
+
|
|
71
|
+
if Path(db_path).exists():
|
|
72
|
+
storage = Storage(db_path, config)
|
|
73
|
+
memory_count = storage.get_memory_count()
|
|
74
|
+
table.add_row("Memory Count", str(memory_count))
|
|
75
|
+
|
|
76
|
+
logs = storage.get_recent_extraction_logs(limit=1)
|
|
77
|
+
if logs:
|
|
78
|
+
last_extraction = logs[0]["created_at"]
|
|
79
|
+
table.add_row("Last Extraction", last_extraction)
|
|
80
|
+
else:
|
|
81
|
+
table.add_row("Last Extraction", "Never")
|
|
82
|
+
|
|
83
|
+
table.add_row("Database Status", "[green]Exists[/green]")
|
|
84
|
+
|
|
85
|
+
from oghma.watcher import Watcher
|
|
86
|
+
|
|
87
|
+
watcher = Watcher(config, storage)
|
|
88
|
+
watched_files = watcher.discover_files()
|
|
89
|
+
table.add_row("Watched Files", str(len(watched_files)))
|
|
90
|
+
else:
|
|
91
|
+
table.add_row("Memory Count", "0")
|
|
92
|
+
table.add_row("Last Extraction", "Never")
|
|
93
|
+
table.add_row("Database Status", "[yellow]Not created yet[/yellow]")
|
|
94
|
+
table.add_row("Watched Files", "0")
|
|
95
|
+
|
|
96
|
+
console.print(table)
|
|
97
|
+
|
|
98
|
+
errors = validate_config(config)
|
|
99
|
+
if errors:
|
|
100
|
+
console.print("\n[red]Configuration errors:[/red]")
|
|
101
|
+
for error in errors:
|
|
102
|
+
console.print(f" [red]- {error}[/red]")
|
|
103
|
+
|
|
104
|
+
except FileNotFoundError:
|
|
105
|
+
console.print("[red]Config not found. Run 'oghma init' first.[/red]")
|
|
106
|
+
except Exception as e:
|
|
107
|
+
console.print(f"[red]Error: {e}[/red]")
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@cli.command()
|
|
111
|
+
@click.option("--foreground", "-f", is_flag=True, help="Run in foreground (don't daemonize)")
|
|
112
|
+
def start(foreground: bool) -> None:
|
|
113
|
+
try:
|
|
114
|
+
config = load_config()
|
|
115
|
+
pid_file = config["daemon"]["pid_file"]
|
|
116
|
+
|
|
117
|
+
pid = get_daemon_pid(pid_file)
|
|
118
|
+
if pid:
|
|
119
|
+
console.print(f"[red]Daemon already running (PID: {pid})[/red]")
|
|
120
|
+
console.print("Use 'oghma stop' to stop it first.")
|
|
121
|
+
raise SystemExit(1)
|
|
122
|
+
|
|
123
|
+
console.print("[blue]Starting Oghma daemon...[/blue]")
|
|
124
|
+
|
|
125
|
+
if not foreground:
|
|
126
|
+
try:
|
|
127
|
+
pid = os.fork()
|
|
128
|
+
if pid > 0:
|
|
129
|
+
console.print(f"[green]Daemon started in background (PID: {pid})[/green]")
|
|
130
|
+
return
|
|
131
|
+
except OSError as e:
|
|
132
|
+
console.print(f"[yellow]Fork failed: {e}. Running in foreground.[/yellow]")
|
|
133
|
+
|
|
134
|
+
daemon = Daemon(config)
|
|
135
|
+
daemon.start()
|
|
136
|
+
|
|
137
|
+
except FileNotFoundError:
|
|
138
|
+
console.print("[red]Config not found. Run 'oghma init' first.[/red]")
|
|
139
|
+
raise SystemExit(1) from None
|
|
140
|
+
except Exception as e:
|
|
141
|
+
console.print(f"[red]Error starting daemon: {e}[/red]")
|
|
142
|
+
raise SystemExit(1) from None
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
@cli.command()
|
|
146
|
+
def stop() -> None:
|
|
147
|
+
try:
|
|
148
|
+
config = load_config()
|
|
149
|
+
pid_file = config["daemon"]["pid_file"]
|
|
150
|
+
|
|
151
|
+
pid = get_daemon_pid(pid_file)
|
|
152
|
+
if not pid:
|
|
153
|
+
console.print("[yellow]Daemon is not running[/yellow]")
|
|
154
|
+
return
|
|
155
|
+
|
|
156
|
+
console.print(f"[blue]Stopping daemon (PID: {pid})...[/blue]")
|
|
157
|
+
|
|
158
|
+
try:
|
|
159
|
+
os.kill(pid, signal.SIGTERM)
|
|
160
|
+
except ProcessLookupError:
|
|
161
|
+
console.print("[yellow]Daemon process not found. Cleaning up PID file.[/yellow]")
|
|
162
|
+
Path(pid_file).unlink(missing_ok=True)
|
|
163
|
+
return
|
|
164
|
+
|
|
165
|
+
for _ in range(10):
|
|
166
|
+
time.sleep(0.5)
|
|
167
|
+
if not get_daemon_pid(pid_file):
|
|
168
|
+
console.print("[green]Daemon stopped successfully[/green]")
|
|
169
|
+
return
|
|
170
|
+
|
|
171
|
+
console.print("[yellow]Daemon did not stop gracefully. Sending SIGKILL...[/yellow]")
|
|
172
|
+
try:
|
|
173
|
+
os.kill(pid, signal.SIGKILL)
|
|
174
|
+
except ProcessLookupError:
|
|
175
|
+
pass
|
|
176
|
+
|
|
177
|
+
Path(pid_file).unlink(missing_ok=True)
|
|
178
|
+
console.print("[green]Daemon force stopped[/green]")
|
|
179
|
+
|
|
180
|
+
except FileNotFoundError:
|
|
181
|
+
console.print("[red]Config not found. Run 'oghma init' first.[/red]")
|
|
182
|
+
raise SystemExit(1) from None
|
|
183
|
+
except Exception as e:
|
|
184
|
+
console.print(f"[red]Error stopping daemon: {e}[/red]")
|
|
185
|
+
raise SystemExit(1) from None
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
@cli.command()
|
|
189
|
+
@click.argument("query")
|
|
190
|
+
@click.option("--limit", "-n", default=10, help="Max results")
|
|
191
|
+
@click.option("--category", "-c", help="Filter by category")
|
|
192
|
+
@click.option(
|
|
193
|
+
"--mode",
|
|
194
|
+
type=click.Choice(["keyword", "vector", "hybrid"]),
|
|
195
|
+
default="keyword",
|
|
196
|
+
show_default=True,
|
|
197
|
+
help="Search strategy",
|
|
198
|
+
)
|
|
199
|
+
def search(query: str, limit: int, category: str | None, mode: str) -> None:
|
|
200
|
+
try:
|
|
201
|
+
config = load_config()
|
|
202
|
+
storage = Storage(config=config)
|
|
203
|
+
query_embedding: list[float] | None = None
|
|
204
|
+
|
|
205
|
+
if mode in {"vector", "hybrid"}:
|
|
206
|
+
embed_config = config.get("embedding", {})
|
|
207
|
+
embedder = create_embedder(EmbedConfig.from_dict(embed_config))
|
|
208
|
+
query_embedding = embedder.embed(query)
|
|
209
|
+
|
|
210
|
+
results = storage.search_memories_hybrid(
|
|
211
|
+
query=query,
|
|
212
|
+
query_embedding=query_embedding,
|
|
213
|
+
limit=limit,
|
|
214
|
+
category=category,
|
|
215
|
+
search_mode=mode,
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
if not results:
|
|
219
|
+
console.print(f"[yellow]No memories found matching: {query}[/yellow]")
|
|
220
|
+
return
|
|
221
|
+
|
|
222
|
+
console.print(f"[cyan]Found {len(results)} memories matching: {query}[/cyan]\n")
|
|
223
|
+
|
|
224
|
+
for idx, memory in enumerate(results, 1):
|
|
225
|
+
table = Table(show_header=False, box=None, padding=(0, 0))
|
|
226
|
+
table.add_column("", style="cyan")
|
|
227
|
+
table.add_column("")
|
|
228
|
+
|
|
229
|
+
table.add_row(f"[bold]#{idx}[/bold]", f"[dim]{memory['created_at']}[/dim]")
|
|
230
|
+
table.add_row("Category", f"[green]{memory['category']}[/green]")
|
|
231
|
+
table.add_row("Source", f"{memory['source_tool']} ({Path(memory['source_file']).name})")
|
|
232
|
+
table.add_row("Confidence", f"{memory['confidence']:.0%}")
|
|
233
|
+
table.add_row("Content", memory["content"])
|
|
234
|
+
|
|
235
|
+
console.print(table)
|
|
236
|
+
console.print()
|
|
237
|
+
|
|
238
|
+
except FileNotFoundError:
|
|
239
|
+
console.print("[red]Config not found. Run 'oghma init' first.[/red]")
|
|
240
|
+
raise SystemExit(1) from None
|
|
241
|
+
except Exception as e:
|
|
242
|
+
console.print(f"[red]Error searching memories: {e}[/red]")
|
|
243
|
+
raise SystemExit(1) from None
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
@cli.command("migrate-embeddings")
|
|
247
|
+
@click.option("--batch-size", default=100, show_default=True, help="Batch size")
|
|
248
|
+
@click.option("--dry-run", is_flag=True, help="Preview migration without writing embeddings")
|
|
249
|
+
def migrate_embeddings(batch_size: int, dry_run: bool) -> None:
|
|
250
|
+
try:
|
|
251
|
+
config = load_config()
|
|
252
|
+
storage = Storage(config=config)
|
|
253
|
+
|
|
254
|
+
done_before, total = storage.get_embedding_progress()
|
|
255
|
+
console.print(
|
|
256
|
+
f"[blue]Embedding progress before migration:[/blue] {done_before}/{total} memories"
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
if done_before == total and total > 0:
|
|
260
|
+
console.print("[green]All active memories already have embeddings.[/green]")
|
|
261
|
+
return
|
|
262
|
+
|
|
263
|
+
embed_config = config.get("embedding", {})
|
|
264
|
+
embedder = create_embedder(EmbedConfig.from_dict(embed_config, batch_size=batch_size))
|
|
265
|
+
|
|
266
|
+
migration = EmbeddingMigration(
|
|
267
|
+
storage=storage,
|
|
268
|
+
embedder=embedder,
|
|
269
|
+
batch_size=batch_size,
|
|
270
|
+
)
|
|
271
|
+
result = migration.run(dry_run=dry_run)
|
|
272
|
+
|
|
273
|
+
done_after, total_after = storage.get_embedding_progress()
|
|
274
|
+
if dry_run:
|
|
275
|
+
console.print(
|
|
276
|
+
f"[yellow]Dry run complete.[/yellow] "
|
|
277
|
+
f"Would process {result.processed} memories."
|
|
278
|
+
)
|
|
279
|
+
return
|
|
280
|
+
|
|
281
|
+
console.print(
|
|
282
|
+
"[green]Migration complete.[/green] "
|
|
283
|
+
f"Processed={result.processed}, migrated={result.migrated}, "
|
|
284
|
+
f"failed={result.failed}, skipped={result.skipped}"
|
|
285
|
+
)
|
|
286
|
+
console.print(
|
|
287
|
+
f"[cyan]Embedding progress after migration:[/cyan] {done_after}/{total_after} memories"
|
|
288
|
+
)
|
|
289
|
+
except FileNotFoundError:
|
|
290
|
+
console.print("[red]Config not found. Run 'oghma init' first.[/red]")
|
|
291
|
+
raise SystemExit(1) from None
|
|
292
|
+
except Exception as e:
|
|
293
|
+
console.print(f"[red]Error migrating embeddings: {e}[/red]")
|
|
294
|
+
raise SystemExit(1) from None
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
@cli.command()
|
|
298
|
+
@click.option("--output", "-o", type=click.Path(), help="Output directory")
|
|
299
|
+
@click.option("--format", "-f", type=click.Choice(["markdown", "json"]), default="markdown")
|
|
300
|
+
@click.option(
|
|
301
|
+
"--group-by", "-g", type=click.Choice(["category", "date", "source"]), default="category"
|
|
302
|
+
)
|
|
303
|
+
@click.option("--category", "-c", help="Export only this category")
|
|
304
|
+
def export(output: str | None, format: str, group_by: str, category: str | None) -> None:
|
|
305
|
+
"""Export memories to files."""
|
|
306
|
+
try:
|
|
307
|
+
config = load_config()
|
|
308
|
+
storage = Storage(config=config)
|
|
309
|
+
|
|
310
|
+
output_dir = Path(output or config["export"]["output_dir"])
|
|
311
|
+
|
|
312
|
+
options = ExportOptions(output_dir=output_dir, format=format, group_by=group_by)
|
|
313
|
+
exporter = Exporter(storage, options)
|
|
314
|
+
|
|
315
|
+
if category:
|
|
316
|
+
console.print(f"[blue]Exporting memories for category: {category}[/blue]")
|
|
317
|
+
file_path = exporter.export_category(category)
|
|
318
|
+
console.print(f"[green]Exported to: {file_path}[/green]")
|
|
319
|
+
else:
|
|
320
|
+
console.print(f"[blue]Exporting memories (grouped by {group_by})...[/blue]")
|
|
321
|
+
files = exporter.export()
|
|
322
|
+
|
|
323
|
+
if not files:
|
|
324
|
+
console.print("[yellow]No memories found to export[/yellow]")
|
|
325
|
+
return
|
|
326
|
+
|
|
327
|
+
for file_path in files:
|
|
328
|
+
console.print(f"[green]Exported to: {file_path}[/green]")
|
|
329
|
+
|
|
330
|
+
except ValueError as e:
|
|
331
|
+
console.print(f"[red]Error: {e}[/red]")
|
|
332
|
+
raise SystemExit(1) from None
|
|
333
|
+
except FileNotFoundError:
|
|
334
|
+
console.print("[red]Config not found. Run 'oghma init' first.[/red]")
|
|
335
|
+
raise SystemExit(1) from None
|
|
336
|
+
except Exception as e:
|
|
337
|
+
console.print(f"[red]Error exporting memories: {e}[/red]")
|
|
338
|
+
raise SystemExit(1) from None
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def main() -> None:
|
|
342
|
+
cli()
|