deja-cli 0.1.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 (34) hide show
  1. deja_cli-0.1.0/.gitignore +56 -0
  2. deja_cli-0.1.0/LICENSE +21 -0
  3. deja_cli-0.1.0/PKG-INFO +100 -0
  4. deja_cli-0.1.0/README.pypi.md +68 -0
  5. deja_cli-0.1.0/config/default.yaml +62 -0
  6. deja_cli-0.1.0/deja/__init__.py +0 -0
  7. deja_cli-0.1.0/deja/config.py +127 -0
  8. deja_cli-0.1.0/deja/core/__init__.py +0 -0
  9. deja_cli-0.1.0/deja/core/extractor.py +135 -0
  10. deja_cli-0.1.0/deja/core/reflection.py +364 -0
  11. deja_cli-0.1.0/deja/core/scheduler.py +65 -0
  12. deja_cli-0.1.0/deja/core/store.py +1413 -0
  13. deja_cli-0.1.0/deja/ingest/__init__.py +0 -0
  14. deja_cli-0.1.0/deja/ingest/watchers/__init__.py +0 -0
  15. deja_cli-0.1.0/deja/ingest/watchers/base.py +143 -0
  16. deja_cli-0.1.0/deja/ingest/watchers/claude_code.py +62 -0
  17. deja_cli-0.1.0/deja/ingest/watchers/codex_cli.py +95 -0
  18. deja_cli-0.1.0/deja/ingest/watchers/gemini_cli.py +96 -0
  19. deja_cli-0.1.0/deja/interfaces/__init__.py +0 -0
  20. deja_cli-0.1.0/deja/interfaces/cli.py +1967 -0
  21. deja_cli-0.1.0/deja/interfaces/mcp_server.py +96 -0
  22. deja_cli-0.1.0/deja/interfaces/web.py +104 -0
  23. deja_cli-0.1.0/deja/interfaces/web_ui/index.html +614 -0
  24. deja_cli-0.1.0/deja/llm/__init__.py +0 -0
  25. deja_cli-0.1.0/deja/llm/base.py +34 -0
  26. deja_cli-0.1.0/deja/llm/embedding.py +45 -0
  27. deja_cli-0.1.0/deja/llm/factory.py +90 -0
  28. deja_cli-0.1.0/deja/llm/providers/__init__.py +0 -0
  29. deja_cli-0.1.0/deja/llm/providers/anthropic.py +21 -0
  30. deja_cli-0.1.0/deja/llm/providers/ollama.py +30 -0
  31. deja_cli-0.1.0/deja/main.py +4 -0
  32. deja_cli-0.1.0/hooks/deja-post-fail.sh +88 -0
  33. deja_cli-0.1.0/hooks/deja-recall.sh +85 -0
  34. deja_cli-0.1.0/pyproject.toml +67 -0
@@ -0,0 +1,56 @@
1
+ # Python bytecode
2
+ __pycache__/
3
+ *.pyc
4
+ *.pyo
5
+ *.pyd
6
+ *.pyc
7
+
8
+ # Distribution / packaging
9
+ dist/
10
+ build/
11
+ *.egg-info/
12
+ *.egg
13
+ MANIFEST
14
+
15
+ # Virtual environments
16
+ .venv/
17
+ venv/
18
+ env/
19
+ .env
20
+
21
+ # uv
22
+ .uv/
23
+
24
+ # Environment variables
25
+ .env
26
+ .env.*
27
+ !.env.example
28
+
29
+ # Testing
30
+ .pytest_cache/
31
+ .coverage
32
+ coverage.xml
33
+ htmlcov/
34
+ .tox/
35
+
36
+ # Type checking
37
+ .mypy_cache/
38
+ .ruff_cache/
39
+
40
+ # Editors
41
+ .vscode/
42
+ .idea/
43
+ *.swp
44
+ *.swo
45
+ *~
46
+
47
+ # macOS
48
+ .DS_Store
49
+
50
+ # Logs
51
+ *.log
52
+
53
+ # Local dev artifacts
54
+ *.db
55
+ *.db-shm
56
+ *.db-wal
deja_cli-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Mike
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,100 @@
1
+ Metadata-Version: 2.4
2
+ Name: deja-cli
3
+ Version: 0.1.0
4
+ Summary: Local-first persistent memory CLI for coding agents
5
+ Author-email: Mike <mike@bigtreeproduction.com>
6
+ License: MIT
7
+ License-File: LICENSE
8
+ Keywords: agent,claude,cli,developer-tools,llm,memory
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Environment :: Console
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
16
+ Classifier: Topic :: Utilities
17
+ Requires-Python: >=3.12
18
+ Requires-Dist: aiosqlite>=0.20
19
+ Requires-Dist: anthropic>=0.34
20
+ Requires-Dist: apscheduler<4.0,>=3.10
21
+ Requires-Dist: fastapi>=0.115
22
+ Requires-Dist: httpx>=0.27
23
+ Requires-Dist: mcp>=1.26.0
24
+ Requires-Dist: pydantic-settings>=2.4
25
+ Requires-Dist: pydantic>=2.0
26
+ Requires-Dist: python-ulid>=2.0
27
+ Requires-Dist: pyyaml>=6.0
28
+ Requires-Dist: typer>=0.12
29
+ Requires-Dist: uvicorn[standard]>=0.30
30
+ Requires-Dist: watchdog>=4.0
31
+ Description-Content-Type: text/markdown
32
+
33
+ # deja
34
+
35
+ Local-first persistent memory CLI for coding agents. Memories accumulate across sessions, deduplicate automatically, and stay on your machine.
36
+
37
+ ## Install
38
+
39
+ ```bash
40
+ uv tool install deja-cli
41
+ ```
42
+
43
+ ## Quick Start
44
+
45
+ ```bash
46
+ deja init # first-time setup
47
+ deja setup claude-code # inject memory instructions into Claude Code
48
+ ```
49
+
50
+ ## Core Commands
51
+
52
+ **Save a memory:**
53
+ ```bash
54
+ deja save "Always use explicit error handling over try/catch" --type preference
55
+ deja save "Auth tokens stored in localStorage, not cookies" --type decision --project myapp
56
+ deja save "Prisma migrations fail silently if DB_URL has wrong port" --type gotcha --project myapp
57
+ ```
58
+
59
+ **Load at session start:**
60
+ ```bash
61
+ deja load --project myapp --context "what you're working on"
62
+ ```
63
+
64
+ **Search mid-session:**
65
+ ```bash
66
+ deja search "authentication" --project myapp
67
+ ```
68
+
69
+ **Extract memories from a session:**
70
+ ```bash
71
+ deja save-session --project myapp
72
+ ```
73
+
74
+ ## Memory Types
75
+
76
+ | Type | When to use |
77
+ |---|---|
78
+ | `preference` | How you like to code — style, tools, habits |
79
+ | `pattern` | Reusable solution that applies across contexts |
80
+ | `decision` | Non-obvious architectural choice with reasoning |
81
+ | `gotcha` | Bug, trap, or non-obvious issue to avoid |
82
+ | `progress` | Current state of in-progress work |
83
+ | `procedure` | Ordered steps for a recurring task |
84
+
85
+ ## Setup for Coding Agents
86
+
87
+ ```bash
88
+ deja setup claude-code # Claude Code — global config + recall hooks
89
+ deja setup gemini-cli # Gemini CLI
90
+ deja setup codex # Codex CLI
91
+ deja setup cursor # Cursor
92
+ ```
93
+
94
+ ## MCP Server
95
+
96
+ ```bash
97
+ claude mcp add --scope user deja -- ~/.local/bin/deja-mcp
98
+ ```
99
+
100
+ Registers `memory_load`, `memory_save`, and `memory_search` as native tools in every Claude Code session.
@@ -0,0 +1,68 @@
1
+ # deja
2
+
3
+ Local-first persistent memory CLI for coding agents. Memories accumulate across sessions, deduplicate automatically, and stay on your machine.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ uv tool install deja-cli
9
+ ```
10
+
11
+ ## Quick Start
12
+
13
+ ```bash
14
+ deja init # first-time setup
15
+ deja setup claude-code # inject memory instructions into Claude Code
16
+ ```
17
+
18
+ ## Core Commands
19
+
20
+ **Save a memory:**
21
+ ```bash
22
+ deja save "Always use explicit error handling over try/catch" --type preference
23
+ deja save "Auth tokens stored in localStorage, not cookies" --type decision --project myapp
24
+ deja save "Prisma migrations fail silently if DB_URL has wrong port" --type gotcha --project myapp
25
+ ```
26
+
27
+ **Load at session start:**
28
+ ```bash
29
+ deja load --project myapp --context "what you're working on"
30
+ ```
31
+
32
+ **Search mid-session:**
33
+ ```bash
34
+ deja search "authentication" --project myapp
35
+ ```
36
+
37
+ **Extract memories from a session:**
38
+ ```bash
39
+ deja save-session --project myapp
40
+ ```
41
+
42
+ ## Memory Types
43
+
44
+ | Type | When to use |
45
+ |---|---|
46
+ | `preference` | How you like to code — style, tools, habits |
47
+ | `pattern` | Reusable solution that applies across contexts |
48
+ | `decision` | Non-obvious architectural choice with reasoning |
49
+ | `gotcha` | Bug, trap, or non-obvious issue to avoid |
50
+ | `progress` | Current state of in-progress work |
51
+ | `procedure` | Ordered steps for a recurring task |
52
+
53
+ ## Setup for Coding Agents
54
+
55
+ ```bash
56
+ deja setup claude-code # Claude Code — global config + recall hooks
57
+ deja setup gemini-cli # Gemini CLI
58
+ deja setup codex # Codex CLI
59
+ deja setup cursor # Cursor
60
+ ```
61
+
62
+ ## MCP Server
63
+
64
+ ```bash
65
+ claude mcp add --scope user deja -- ~/.local/bin/deja-mcp
66
+ ```
67
+
68
+ Registers `memory_load`, `memory_save`, and `memory_search` as native tools in every Claude Code session.
@@ -0,0 +1,62 @@
1
+ llm:
2
+ # provider: none means the agent does extraction itself via deja save.
3
+ # No API key or local model required for the active (CLAUDE.md) workflow.
4
+ # Only change this if you want the passive watcher to auto-extract memories
5
+ # from session summaries without agent involvement.
6
+ extraction:
7
+ provider: none
8
+
9
+ reflection:
10
+ provider: none
11
+
12
+ # To enable LLM-powered extraction (passive watcher + deja save-session --transcript):
13
+ #
14
+ # Option A — Anthropic API (best quality, ~$0.50/day heavy use):
15
+ # extraction:
16
+ # provider: anthropic
17
+ # model: claude-haiku-4-5-20251001
18
+ # api_key: "${ANTHROPIC_API_KEY}"
19
+ # reflection:
20
+ # provider: anthropic
21
+ # model: claude-haiku-4-5-20251001
22
+ # api_key: "${ANTHROPIC_API_KEY}"
23
+ #
24
+ # Option B — Local model via Ollama (no API cost, needs Ollama running):
25
+ # extraction:
26
+ # provider: ollama
27
+ # model: qwen2.5:7b # best local quality for structured JSON
28
+ # base_url: http://localhost:11434
29
+ # reflection:
30
+ # provider: ollama
31
+ # model: llama3.1:8b-instruct-q4_K_M
32
+ # base_url: http://localhost:11434
33
+
34
+ embedding:
35
+ # Semantic (natural language) search. Set provider to 'ollama' and pull the model:
36
+ # ollama pull nomic-embed-text
37
+ # Then run 'deja embed' to backfill embeddings for existing memories.
38
+ provider: ollama # none | ollama
39
+ model: nomic-embed-text
40
+ base_url: http://localhost:11434
41
+
42
+ store:
43
+ path: ~/.deja/store/memories.db
44
+ vault_path: ~/.deja/store/vault/
45
+
46
+ reflection:
47
+ observer_trigger_tokens: 30000
48
+ reflector_trigger_tokens: 40000
49
+ kg_merge_schedule: "0 2 * * *"
50
+ confidence_archive_threshold: 0.3
51
+ # agent memories (gotcha, decision, progress, pattern) — operational knowledge goes stale
52
+ confidence_decay_per_week: 0.05
53
+ # user memories (preference) — personal habits are stable, decay ~5x slower
54
+ user_confidence_decay_per_week: 0.01
55
+ min_project_pattern_count: 2
56
+
57
+ watchers:
58
+ claude_code: true
59
+ gemini_cli: false
60
+ codex_cli: false
61
+ aider: false
62
+ debounce_seconds: 30
File without changes
@@ -0,0 +1,127 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import re
5
+ from pathlib import Path
6
+ from typing import Optional
7
+
8
+ import yaml
9
+ from pydantic import BaseModel, field_validator
10
+
11
+
12
+ def _substitute_env(value: str) -> str:
13
+ """Substitute ${ENV_VAR} patterns with environment variable values."""
14
+ def replacer(match: re.Match) -> str:
15
+ var_name = match.group(1)
16
+ return os.environ.get(var_name, match.group(0))
17
+
18
+ return re.sub(r"\$\{([^}]+)\}", replacer, value)
19
+
20
+
21
+ def _expand_paths(data: dict) -> dict:
22
+ """Recursively expand ~ in string values."""
23
+ result = {}
24
+ for key, value in data.items():
25
+ if isinstance(value, dict):
26
+ result[key] = _expand_paths(value)
27
+ elif isinstance(value, str):
28
+ result[key] = _substitute_env(value)
29
+ else:
30
+ result[key] = value
31
+ return result
32
+
33
+
34
+ class LLMProviderConfig(BaseModel):
35
+ provider: str = "ollama"
36
+ model: str = "qwen2.5:3b"
37
+ base_url: str = "http://localhost:11434"
38
+ api_key: Optional[str] = None
39
+
40
+ @field_validator("api_key", mode="before")
41
+ @classmethod
42
+ def expand_env_vars(cls, v: Optional[str]) -> Optional[str]:
43
+ if v is None:
44
+ return v
45
+ return _substitute_env(v)
46
+
47
+
48
+ class LLMConfig(BaseModel):
49
+ extraction: LLMProviderConfig = LLMProviderConfig(provider="none")
50
+ reflection: LLMProviderConfig = LLMProviderConfig(provider="none")
51
+ fallback: LLMProviderConfig = LLMProviderConfig(
52
+ provider="anthropic",
53
+ model="claude-haiku-4-5-20251001",
54
+ api_key="${ANTHROPIC_API_KEY}",
55
+ )
56
+
57
+
58
+ class StoreConfig(BaseModel):
59
+ path: str = "~/.deja/store/memories.db"
60
+ vault_path: str = "~/.deja/store/vault/"
61
+
62
+ @property
63
+ def db_path(self) -> Path:
64
+ return Path(self.path).expanduser()
65
+
66
+ @property
67
+ def vault_dir(self) -> Path:
68
+ return Path(self.vault_path).expanduser()
69
+
70
+
71
+ class ReflectionConfig(BaseModel):
72
+ observer_trigger_tokens: int = 30000
73
+ reflector_trigger_tokens: int = 40000
74
+ kg_merge_schedule: str = "0 2 * * *"
75
+ confidence_archive_threshold: float = 0.3
76
+ # agent memories (gotcha, decision, progress, pattern) decay at this rate.
77
+ # Operational knowledge goes stale; 0.05/week means a memory hits the 0.3
78
+ # archive threshold in ~14 weeks without re-confirmation.
79
+ confidence_decay_per_week: float = 0.05
80
+ # user memories (preferences, habits) decay ~5x slower. Personal style
81
+ # preferences don't go stale the way project-specific gotchas do.
82
+ user_confidence_decay_per_week: float = 0.01
83
+ min_project_pattern_count: int = 2
84
+
85
+
86
+ class WatchersConfig(BaseModel):
87
+ claude_code: bool = True
88
+ gemini_cli: bool = False
89
+ codex_cli: bool = False
90
+ aider: bool = False
91
+ debounce_seconds: int = 30
92
+
93
+
94
+ class EmbeddingConfig(BaseModel):
95
+ provider: str = "none" # none | ollama
96
+ model: str = "nomic-embed-text"
97
+ base_url: str = "http://localhost:11434"
98
+
99
+
100
+ class Config(BaseModel):
101
+ llm: LLMConfig = LLMConfig()
102
+ store: StoreConfig = StoreConfig()
103
+ reflection: ReflectionConfig = ReflectionConfig()
104
+ watchers: WatchersConfig = WatchersConfig()
105
+ embedding: EmbeddingConfig = EmbeddingConfig()
106
+
107
+
108
+ def load_config(path: Optional[Path] = None) -> Config:
109
+ """Load config from path, falling back to ~/.deja/config.yaml,
110
+ then to the bundled default."""
111
+ candidates = []
112
+ if path:
113
+ candidates.append(Path(path).expanduser())
114
+ candidates.append(Path("~/.deja/config.yaml").expanduser())
115
+
116
+ # Bundled default
117
+ default_path = Path(__file__).parent.parent / "config" / "default.yaml"
118
+ candidates.append(default_path)
119
+
120
+ for candidate in candidates:
121
+ if candidate.exists():
122
+ with open(candidate) as f:
123
+ raw = yaml.safe_load(f) or {}
124
+ raw = _expand_paths(raw)
125
+ return Config.model_validate(raw)
126
+
127
+ return Config()
File without changes
@@ -0,0 +1,135 @@
1
+ from __future__ import annotations
2
+
3
+ import sys
4
+ from typing import Optional
5
+
6
+ from deja.llm.base import LLMAdapter
7
+
8
+ EXTRACTION_SYSTEM = """You are a memory extraction system for a software engineer.
9
+ Given a coding session transcript or summary, extract ONLY memories
10
+ that would be genuinely useful in a future session.
11
+
12
+ Be ruthless — most session content is NOT worth remembering.
13
+ Only extract things that are:
14
+ - Non-obvious (not derivable from reading the codebase)
15
+ - Reusable (would apply in future sessions)
16
+ - Important (would cause problems if forgotten)
17
+
18
+ Memory types:
19
+ - preference: how the user likes to code (style, tools, patterns)
20
+ - pattern: reusable solution / architectural approach that applies across contexts ("knowing what works")
21
+ - decision: non-obvious architectural choice with reasoning
22
+ - gotcha: bug, trap, or non-obvious issue to avoid
23
+ - progress: current state of in-progress work
24
+ - procedure: reusable ordered steps for a recurring class of work ("knowing how to execute"). Keep thin: numbered steps + tool hints + exit criteria only. Do NOT inline gotchas or decisions — save those as separate memory types.
25
+
26
+ Category:
27
+ - user: personal preferences and habits (applies across all projects)
28
+ - agent: operational knowledge discovered while doing work
29
+
30
+ Domain (optional, coarse routing tag — use for procedure type mainly):
31
+ - debug | build | test | deploy | research
32
+
33
+ Output ONLY valid JSON:
34
+ {
35
+ "memories": [
36
+ {
37
+ "type": "preference|pattern|decision|gotcha|progress|procedure",
38
+ "category": "user|agent",
39
+ "content": "concise, self-contained fact. 1-2 sentences max for most types. For procedure: one-line description followed by numbered steps, tool hints, and exit criteria.",
40
+ "scope": "global|project",
41
+ "project": "project_name or null if global",
42
+ "confidence": 0.0-1.0,
43
+ "domain": "debug|build|test|deploy|research|null"
44
+ }
45
+ ]
46
+ }
47
+
48
+ If nothing is worth remembering, return: {"memories": []}"""
49
+
50
+ EXTRACTION_SCHEMA = {
51
+ "type": "object",
52
+ "properties": {
53
+ "memories": {
54
+ "type": "array",
55
+ "items": {
56
+ "type": "object",
57
+ "properties": {
58
+ "type": {
59
+ "type": "string",
60
+ "enum": ["preference", "pattern", "decision", "gotcha", "progress", "procedure"],
61
+ },
62
+ "category": {"type": "string", "enum": ["user", "agent"]},
63
+ "content": {"type": "string"},
64
+ "scope": {"type": "string", "enum": ["global", "project"]},
65
+ "project": {"type": ["string", "null"]},
66
+ "confidence": {"type": "number"},
67
+ "domain": {"type": ["string", "null"]},
68
+ },
69
+ "required": ["type", "category", "content", "scope", "confidence"],
70
+ },
71
+ }
72
+ },
73
+ "required": ["memories"],
74
+ }
75
+
76
+
77
+ async def extract_memories(
78
+ transcript: str,
79
+ project: str,
80
+ source: str,
81
+ adapter: LLMAdapter,
82
+ ) -> list[dict]:
83
+ """Extract memories from a session transcript or summary.
84
+
85
+ Returns list of memory dicts ready to pass to store.save().
86
+ """
87
+ if not transcript.strip():
88
+ return []
89
+
90
+ user_prompt = f"Session transcript/summary to extract memories from:\n\n{transcript}"
91
+
92
+ try:
93
+ result = await adapter.complete_structured(
94
+ system=EXTRACTION_SYSTEM,
95
+ user=user_prompt,
96
+ schema=EXTRACTION_SCHEMA,
97
+ )
98
+ except Exception as e:
99
+ print(f"[deja] Extraction LLM error: {e}", file=sys.stderr)
100
+ return []
101
+
102
+ memories = result.get("memories", [])
103
+ if not isinstance(memories, list):
104
+ return []
105
+
106
+ output = []
107
+ for mem in memories:
108
+ if not isinstance(mem, dict):
109
+ continue
110
+ if not mem.get("content") or not mem.get("type"):
111
+ continue
112
+
113
+ # Normalize scope: if scope is "project" but no project given, use the provided project
114
+ scope = mem.get("scope", "global")
115
+ mem_project = mem.get("project") or (project if scope == "project" else None)
116
+ if scope == "project" and mem_project:
117
+ scope_value = f"project:{mem_project}"
118
+ else:
119
+ scope_value = "global"
120
+ mem_project = None
121
+
122
+ output.append(
123
+ {
124
+ "type": mem["type"],
125
+ "category": mem.get("category", "agent"),
126
+ "content": mem["content"],
127
+ "scope": scope_value,
128
+ "project": mem_project,
129
+ "source": source,
130
+ "confidence": float(mem.get("confidence", 0.8)),
131
+ "domain": mem.get("domain"),
132
+ }
133
+ )
134
+
135
+ return output