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.
Files changed (41) hide show
  1. oghma-0.3.0/PKG-INFO +26 -0
  2. oghma-0.3.0/README.md +89 -0
  3. oghma-0.3.0/pyproject.toml +62 -0
  4. oghma-0.3.0/setup.cfg +4 -0
  5. oghma-0.3.0/src/oghma/__init__.py +1 -0
  6. oghma-0.3.0/src/oghma/cli.py +342 -0
  7. oghma-0.3.0/src/oghma/config.py +262 -0
  8. oghma-0.3.0/src/oghma/daemon.py +198 -0
  9. oghma-0.3.0/src/oghma/embedder.py +107 -0
  10. oghma-0.3.0/src/oghma/exporter.py +177 -0
  11. oghma-0.3.0/src/oghma/extractor.py +180 -0
  12. oghma-0.3.0/src/oghma/mcp_server.py +112 -0
  13. oghma-0.3.0/src/oghma/migration.py +63 -0
  14. oghma-0.3.0/src/oghma/parsers/__init__.py +26 -0
  15. oghma-0.3.0/src/oghma/parsers/base.py +24 -0
  16. oghma-0.3.0/src/oghma/parsers/claude_code.py +62 -0
  17. oghma-0.3.0/src/oghma/parsers/codex.py +84 -0
  18. oghma-0.3.0/src/oghma/parsers/openclaw.py +64 -0
  19. oghma-0.3.0/src/oghma/parsers/opencode.py +90 -0
  20. oghma-0.3.0/src/oghma/storage.py +753 -0
  21. oghma-0.3.0/src/oghma/watcher.py +97 -0
  22. oghma-0.3.0/src/oghma.egg-info/PKG-INFO +26 -0
  23. oghma-0.3.0/src/oghma.egg-info/SOURCES.txt +35 -0
  24. oghma-0.3.0/src/oghma.egg-info/dependency_links.txt +1 -0
  25. oghma-0.3.0/src/oghma.egg-info/entry_points.txt +3 -0
  26. oghma-0.3.0/src/oghma.egg-info/requires.txt +14 -0
  27. oghma-0.3.0/src/oghma.egg-info/top_level.txt +1 -0
  28. oghma-0.3.0/tests/test_cli_commands.py +254 -0
  29. oghma-0.3.0/tests/test_config.py +128 -0
  30. oghma-0.3.0/tests/test_daemon.py +165 -0
  31. oghma-0.3.0/tests/test_embedder.py +78 -0
  32. oghma-0.3.0/tests/test_exporter.py +229 -0
  33. oghma-0.3.0/tests/test_extractor.py +324 -0
  34. oghma-0.3.0/tests/test_mcp_server.py +186 -0
  35. oghma-0.3.0/tests/test_migration.py +73 -0
  36. oghma-0.3.0/tests/test_storage.py +341 -0
  37. oghma-0.3.0/tests/test_watcher.py +185 -0
  38. oghma-0.0.1/PKG-INFO +0 -33
  39. oghma-0.0.1/README.md +0 -15
  40. oghma-0.0.1/pyproject.toml +0 -25
  41. 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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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()