cortex-claude 0.2.0__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 (85) hide show
  1. cortex_claude-0.3.0/.claude/hooks/post-tool-use.sh +117 -0
  2. {cortex_claude-0.2.0 → cortex_claude-0.3.0}/PKG-INFO +42 -11
  3. {cortex_claude-0.2.0 → cortex_claude-0.3.0}/README.md +41 -10
  4. {cortex_claude-0.2.0 → cortex_claude-0.3.0}/pyproject.toml +1 -1
  5. cortex_claude-0.3.0/src/cortex_claude/__init__.py +1 -0
  6. {cortex_claude-0.2.0 → cortex_claude-0.3.0}/src/cortex_claude/__main__.py +7 -3
  7. {cortex_claude-0.2.0 → cortex_claude-0.3.0}/src/cortex_claude/core/engine.py +6 -0
  8. cortex_claude-0.3.0/src/cortex_claude/core/privacy.py +21 -0
  9. cortex_claude-0.3.0/src/cortex_claude/setup.py +418 -0
  10. cortex_claude-0.3.0/tests/integration/test_privacy.py +48 -0
  11. cortex_claude-0.3.0/tests/unit/test_privacy.py +57 -0
  12. {cortex_claude-0.2.0 → cortex_claude-0.3.0}/uv.lock +1 -1
  13. cortex_claude-0.2.0/.claude/hooks/post-tool-use.sh +0 -61
  14. cortex_claude-0.2.0/src/cortex_claude/__init__.py +0 -1
  15. {cortex_claude-0.2.0 → cortex_claude-0.3.0}/.claude/hooks/session-start.sh +0 -0
  16. {cortex_claude-0.2.0 → cortex_claude-0.3.0}/.github/workflows/ci.yml +0 -0
  17. {cortex_claude-0.2.0 → cortex_claude-0.3.0}/.github/workflows/publish.yml +0 -0
  18. {cortex_claude-0.2.0 → cortex_claude-0.3.0}/.gitignore +0 -0
  19. {cortex_claude-0.2.0 → cortex_claude-0.3.0}/ARCHITECTURE.md +0 -0
  20. {cortex_claude-0.2.0 → cortex_claude-0.3.0}/CONTRIBUTING.md +0 -0
  21. {cortex_claude-0.2.0 → cortex_claude-0.3.0}/LICENSE +0 -0
  22. {cortex_claude-0.2.0 → cortex_claude-0.3.0}/docs/logo.png +0 -0
  23. {cortex_claude-0.2.0 → cortex_claude-0.3.0}/examples/config.json +0 -0
  24. {cortex_claude-0.2.0 → cortex_claude-0.3.0}/examples/mcp-global.json +0 -0
  25. {cortex_claude-0.2.0 → cortex_claude-0.3.0}/examples/mcp-project.json +0 -0
  26. {cortex_claude-0.2.0 → cortex_claude-0.3.0}/scripts/benchmark.py +0 -0
  27. {cortex_claude-0.2.0 → cortex_claude-0.3.0}/scripts/demo.py +0 -0
  28. {cortex_claude-0.2.0 → cortex_claude-0.3.0}/src/cortex_claude/core/__init__.py +0 -0
  29. {cortex_claude-0.2.0 → cortex_claude-0.3.0}/src/cortex_claude/core/decay.py +0 -0
  30. {cortex_claude-0.2.0 → cortex_claude-0.3.0}/src/cortex_claude/core/scope_manager.py +0 -0
  31. {cortex_claude-0.2.0 → cortex_claude-0.3.0}/src/cortex_claude/core/token_budget.py +0 -0
  32. {cortex_claude-0.2.0 → cortex_claude-0.3.0}/src/cortex_claude/daemon.py +0 -0
  33. {cortex_claude-0.2.0 → cortex_claude-0.3.0}/src/cortex_claude/embeddings/__init__.py +0 -0
  34. {cortex_claude-0.2.0 → cortex_claude-0.3.0}/src/cortex_claude/embeddings/engine.py +0 -0
  35. {cortex_claude-0.2.0 → cortex_claude-0.3.0}/src/cortex_claude/embeddings/tokenizer.py +0 -0
  36. {cortex_claude-0.2.0 → cortex_claude-0.3.0}/src/cortex_claude/facts/__init__.py +0 -0
  37. {cortex_claude-0.2.0 → cortex_claude-0.3.0}/src/cortex_claude/facts/claude_extract.py +0 -0
  38. {cortex_claude-0.2.0 → cortex_claude-0.3.0}/src/cortex_claude/facts/extractor.py +0 -0
  39. {cortex_claude-0.2.0 → cortex_claude-0.3.0}/src/cortex_claude/facts/normalizer.py +0 -0
  40. {cortex_claude-0.2.0 → cortex_claude-0.3.0}/src/cortex_claude/facts/patterns.py +0 -0
  41. {cortex_claude-0.2.0 → cortex_claude-0.3.0}/src/cortex_claude/facts/spacy_extract.py +0 -0
  42. {cortex_claude-0.2.0 → cortex_claude-0.3.0}/src/cortex_claude/models/__init__.py +0 -0
  43. {cortex_claude-0.2.0 → cortex_claude-0.3.0}/src/cortex_claude/models/fact.py +0 -0
  44. {cortex_claude-0.2.0 → cortex_claude-0.3.0}/src/cortex_claude/models/memory.py +0 -0
  45. {cortex_claude-0.2.0 → cortex_claude-0.3.0}/src/cortex_claude/models/scope.py +0 -0
  46. {cortex_claude-0.2.0 → cortex_claude-0.3.0}/src/cortex_claude/pipeline/__init__.py +0 -0
  47. {cortex_claude-0.2.0 → cortex_claude-0.3.0}/src/cortex_claude/server/__init__.py +0 -0
  48. {cortex_claude-0.2.0 → cortex_claude-0.3.0}/src/cortex_claude/server/app.py +0 -0
  49. {cortex_claude-0.2.0 → cortex_claude-0.3.0}/src/cortex_claude/server/config.py +0 -0
  50. {cortex_claude-0.2.0 → cortex_claude-0.3.0}/src/cortex_claude/server/tools/__init__.py +0 -0
  51. {cortex_claude-0.2.0 → cortex_claude-0.3.0}/src/cortex_claude/server/tools/facts.py +0 -0
  52. {cortex_claude-0.2.0 → cortex_claude-0.3.0}/src/cortex_claude/server/tools/forget.py +0 -0
  53. {cortex_claude-0.2.0 → cortex_claude-0.3.0}/src/cortex_claude/server/tools/recall.py +0 -0
  54. {cortex_claude-0.2.0 → cortex_claude-0.3.0}/src/cortex_claude/server/tools/save.py +0 -0
  55. {cortex_claude-0.2.0 → cortex_claude-0.3.0}/src/cortex_claude/server/tools/scopes.py +0 -0
  56. {cortex_claude-0.2.0 → cortex_claude-0.3.0}/src/cortex_claude/server/tools/status.py +0 -0
  57. {cortex_claude-0.2.0 → cortex_claude-0.3.0}/src/cortex_claude/server/tools/traverse.py +0 -0
  58. {cortex_claude-0.2.0 → cortex_claude-0.3.0}/src/cortex_claude/storage/__init__.py +0 -0
  59. {cortex_claude-0.2.0 → cortex_claude-0.3.0}/src/cortex_claude/storage/database.py +0 -0
  60. {cortex_claude-0.2.0 → cortex_claude-0.3.0}/src/cortex_claude/storage/fact_repo.py +0 -0
  61. {cortex_claude-0.2.0 → cortex_claude-0.3.0}/src/cortex_claude/storage/memory_repo.py +0 -0
  62. {cortex_claude-0.2.0 → cortex_claude-0.3.0}/src/cortex_claude/storage/migrations.py +0 -0
  63. {cortex_claude-0.2.0 → cortex_claude-0.3.0}/src/cortex_claude/summarizer/__init__.py +0 -0
  64. {cortex_claude-0.2.0 → cortex_claude-0.3.0}/src/cortex_claude/summarizer/extractive.py +0 -0
  65. {cortex_claude-0.2.0 → cortex_claude-0.3.0}/src/cortex_claude/summarizer/scoring.py +0 -0
  66. {cortex_claude-0.2.0 → cortex_claude-0.3.0}/tests/__init__.py +0 -0
  67. {cortex_claude-0.2.0 → cortex_claude-0.3.0}/tests/conftest.py +0 -0
  68. {cortex_claude-0.2.0 → cortex_claude-0.3.0}/tests/integration/__init__.py +0 -0
  69. {cortex_claude-0.2.0 → cortex_claude-0.3.0}/tests/integration/test_forget_scopes_status.py +0 -0
  70. {cortex_claude-0.2.0 → cortex_claude-0.3.0}/tests/integration/test_fts_and_decay.py +0 -0
  71. {cortex_claude-0.2.0 → cortex_claude-0.3.0}/tests/integration/test_graph_traversal.py +0 -0
  72. {cortex_claude-0.2.0 → cortex_claude-0.3.0}/tests/integration/test_progressive_recall.py +0 -0
  73. {cortex_claude-0.2.0 → cortex_claude-0.3.0}/tests/integration/test_save_recall.py +0 -0
  74. {cortex_claude-0.2.0 → cortex_claude-0.3.0}/tests/integration/test_storage.py +0 -0
  75. {cortex_claude-0.2.0 → cortex_claude-0.3.0}/tests/unit/__init__.py +0 -0
  76. {cortex_claude-0.2.0 → cortex_claude-0.3.0}/tests/unit/test_claude_extract.py +0 -0
  77. {cortex_claude-0.2.0 → cortex_claude-0.3.0}/tests/unit/test_config.py +0 -0
  78. {cortex_claude-0.2.0 → cortex_claude-0.3.0}/tests/unit/test_decay.py +0 -0
  79. {cortex_claude-0.2.0 → cortex_claude-0.3.0}/tests/unit/test_facts.py +0 -0
  80. {cortex_claude-0.2.0 → cortex_claude-0.3.0}/tests/unit/test_normalizer.py +0 -0
  81. {cortex_claude-0.2.0 → cortex_claude-0.3.0}/tests/unit/test_patterns.py +0 -0
  82. {cortex_claude-0.2.0 → cortex_claude-0.3.0}/tests/unit/test_scope_manager.py +0 -0
  83. {cortex_claude-0.2.0 → cortex_claude-0.3.0}/tests/unit/test_summarizer.py +0 -0
  84. {cortex_claude-0.2.0 → cortex_claude-0.3.0}/tests/unit/test_token_budget.py +0 -0
  85. {cortex_claude-0.2.0 → cortex_claude-0.3.0}/tests/unit/test_tokenizer.py +0 -0
@@ -0,0 +1,117 @@
1
+ #!/bin/bash
2
+ # Cortex Claude — PostToolUse hook
3
+ # Auto-captures all tool results to Cortex memory via daemon.
4
+
5
+ CORTEX_HOME="${CORTEX_HOME:-$HOME/.cortex-claude}"
6
+ SOCKET="$CORTEX_HOME/cortex.sock"
7
+
8
+ [ ! -S "$SOCKET" ] && exit 0
9
+
10
+ # Read stdin BEFORE backgrounding
11
+ INPUT=$(cat)
12
+
13
+ # Background the processing
14
+ (echo "$INPUT" | python3 -c "
15
+ import json, socket, sys, os
16
+
17
+ data = json.load(sys.stdin)
18
+ tool = data.get('tool_name', '')
19
+
20
+ if 'cortex' in tool.lower():
21
+ sys.exit(0)
22
+
23
+ ti = data.get('tool_input', {})
24
+ tr = str(data.get('tool_response', ''))[:1000]
25
+ cwd = data.get('cwd', '.')
26
+
27
+ import re
28
+ def strip_private(t):
29
+ t = re.sub(r'<private>.*?</private>', '', t, flags=re.DOTALL|re.IGNORECASE)
30
+ return re.sub(r'<private\s*/?>', '', t, flags=re.IGNORECASE).strip()
31
+
32
+ tr = strip_private(tr)
33
+ for k, v in list(ti.items()):
34
+ if isinstance(v, str):
35
+ ti[k] = strip_private(v)
36
+
37
+ if len(tr) < 20:
38
+ sys.exit(0)
39
+
40
+ content = None
41
+ tags = ['auto-capture']
42
+
43
+ if tool == 'Bash':
44
+ cmd = str(ti.get('command', ''))[:150]
45
+ noise = ('ls', 'cd ', 'pwd', 'echo ', 'which ', 'cat ', 'head ', 'tail ', 'mkdir', 'touch ', 'chmod', 'true', 'false', 'test ')
46
+ if any(cmd.startswith(n) for n in noise) or not cmd:
47
+ sys.exit(0)
48
+ content = f'Command: {cmd}\nResult: {tr[:500]}'
49
+ tags.append('bash')
50
+
51
+ elif tool == 'Read':
52
+ path = ti.get('file_path', '')
53
+ content = f'Read file: {path}\nPreview: {tr[:300]}'
54
+ tags.append('file-read')
55
+
56
+ elif tool in ('Edit', 'Write'):
57
+ path = ti.get('file_path', '')
58
+ old = str(ti.get('old_string', ''))[:100]
59
+ new = str(ti.get('new_string', ''))[:100]
60
+ if old and new:
61
+ content = f'Edited {path}: replaced \"{old}\" with \"{new}\"'
62
+ elif tool == 'Write':
63
+ content = f'Created file: {path}'
64
+ else:
65
+ content = f'Modified file: {path}'
66
+ tags.append('file-change')
67
+
68
+ elif tool == 'Grep':
69
+ pattern = ti.get('pattern', '')
70
+ path = ti.get('path', '')
71
+ content = f'Search \"{pattern}\" in {path}\nResults: {tr[:500]}'
72
+ tags.append('search')
73
+
74
+ elif tool == 'Glob':
75
+ pattern = ti.get('pattern', '')
76
+ content = f'File search: {pattern}\nFound: {tr[:500]}'
77
+ tags.append('search')
78
+
79
+ elif tool == 'Agent':
80
+ prompt = str(ti.get('prompt', ''))[:200]
81
+ content = f'Agent task: {prompt}\nResult: {tr[:500]}'
82
+ tags.append('agent')
83
+
84
+ elif tool.startswith('mcp__'):
85
+ parts = tool.split('__')
86
+ server = parts[1] if len(parts) > 1 else 'unknown'
87
+ method = parts[2] if len(parts) > 2 else 'unknown'
88
+ content = f'MCP {server}.{method}\nInput: {json.dumps(ti)[:200]}\nResult: {tr[:400]}'
89
+ tags.append('mcp')
90
+ tags.append(server)
91
+
92
+ elif tool in ('WebSearch', 'WebFetch'):
93
+ query = ti.get('query', ti.get('url', ''))
94
+ content = f'{tool}: {query}\nResult: {tr[:500]}'
95
+ tags.append('web')
96
+
97
+ else:
98
+ content = f'{tool}: {json.dumps(ti)[:150]}\nResult: {tr[:400]}'
99
+ tags.append(tool.lower())
100
+
101
+ if not content:
102
+ sys.exit(0)
103
+
104
+ sock_path = os.path.expanduser('$SOCKET')
105
+ try:
106
+ s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
107
+ s.settimeout(3)
108
+ s.connect(sock_path)
109
+ s.sendall(json.dumps({'action':'save','content':content,'tags':tags,'cwd':cwd}).encode())
110
+ s.shutdown(socket.SHUT_WR)
111
+ s.recv(512)
112
+ s.close()
113
+ except Exception:
114
+ pass
115
+ ") &
116
+
117
+ exit 0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cortex-claude
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Summary: Local-first, token-efficient memory system for Claude Code via MCP
5
5
  Project-URL: Homepage, https://github.com/rafaelaugusto/cortex-claude
6
6
  Project-URL: Repository, https://github.com/rafaelaugusto/cortex-claude
@@ -51,7 +51,7 @@ Description-Content-Type: text/markdown
51
51
  <img src="https://img.shields.io/badge/License-MIT-blue.svg" alt="License">
52
52
  </a>
53
53
  <a href="pyproject.toml">
54
- <img src="https://img.shields.io/badge/version-0.1.0-green.svg" alt="Version">
54
+ <img src="https://img.shields.io/badge/version-0.3.0-green.svg" alt="Version">
55
55
  </a>
56
56
  <a href="pyproject.toml">
57
57
  <img src="https://img.shields.io/badge/python-%3E%3D3.11-brightgreen.svg" alt="Python">
@@ -113,39 +113,50 @@ The system stops at the cheapest layer that answers the question. **66% fewer to
113
113
  - **Background daemon** &mdash; pre-loaded model for instant saves via Unix socket (~0.3s vs ~5s cold start)
114
114
  - **Multi-language** &mdash; EN, PT (auto-detected). ES, DE, FR with additional spaCy models
115
115
  - **Local-first** &mdash; SQLite + local embeddings + local NLP. Zero API calls, zero network, zero cost
116
+ - **Privacy tags** &mdash; wrap sensitive content in `<private>...</private>` to exclude it from memory
116
117
  - **Fully configurable** &mdash; all thresholds, ratios, and behaviors via config.json
117
118
 
118
119
  ---
119
120
 
120
121
  ## Quick Start
121
122
 
122
- ### Install
123
+ ### One-Command Install
123
124
 
124
125
  ```bash
125
- pip install cortex-claude
126
+ pip install cortex-claude && cortex-claude setup
127
+ ```
128
+
129
+ That's it. The setup command:
130
+ - Configures the MCP server globally for Claude Code
131
+ - Installs auto-capture hooks (SessionStart + PostToolUse)
132
+ - Creates `~/.claude/CLAUDE.md` with instructions for Claude to use Cortex automatically
133
+ - Downloads the embedding model (~80MB) and spaCy model (~12MB)
134
+ - Starts the background daemon for instant saves
135
+
136
+ Restart Claude Code and it works in **every project**, no per-project config needed. Claude will automatically consult Cortex before saying "I don't know" and save important context to memory.
137
+
138
+ ### With Claude-assisted extraction (optional)
126
139
 
127
- # With Claude-assisted extraction (optional)
140
+ ```bash
128
141
  pip install cortex-claude[claude]
129
142
  ```
130
143
 
131
- ### Configure Claude Code
144
+ ### Manual Setup (alternative)
132
145
 
133
- Add a `.mcp.json` to your project root (or `~/.claude.json` for global):
146
+ If you prefer manual configuration, add a `.mcp.json` to your project root:
134
147
 
135
148
  ```json
136
149
  {
137
150
  "mcpServers": {
138
151
  "cortex": {
139
152
  "type": "stdio",
140
- "command": "python",
153
+ "command": "python3",
141
154
  "args": ["-m", "cortex_claude"]
142
155
  }
143
156
  }
144
157
  }
145
158
  ```
146
159
 
147
- First run downloads the embedding model (~80MB) and spaCy model (~12MB) automatically.
148
-
149
160
  ### Use
150
161
 
151
162
  Just talk to Claude naturally. Cortex works in the background:
@@ -229,6 +240,26 @@ score = e^(-lambda * days) * (1 + log(access_count))
229
240
 
230
241
  Recalculated on server startup. Frequently accessed memories get boosted. Stale ones fade.
231
242
 
243
+ ### Auto-Capture &amp; Hooks
244
+
245
+ Cortex uses Claude Code hooks to work automatically:
246
+
247
+ - **SessionStart** &mdash; injects memory stats and known facts when you open a session. Claude knows it has memory and consults it before saying "I don't know".
248
+ - **PostToolUse** &mdash; captures results from **all tools** in background: Bash, Read, Edit, Write, Grep, Glob, Agent, WebSearch, WebFetch, and all third-party MCP tools. No manual save needed.
249
+ - **Background Daemon** &mdash; keeps the embedding model pre-loaded via Unix socket. First save after boot: ~5s (model load). Subsequent saves: **~0.3s**.
250
+
251
+ All hooks are installed globally by `cortex-claude setup`. No per-project config needed.
252
+
253
+ ### Privacy
254
+
255
+ Wrap sensitive content in `<private>` tags to exclude it from memory:
256
+
257
+ ```
258
+ The API uses JWT for auth. <private>API_KEY=sk-abc123secret</private> Tokens expire in 24h.
259
+ ```
260
+
261
+ Cortex strips everything between `<private>...</private>` before saving. Works in both manual saves and auto-capture. If the entire content is private, nothing is saved.
262
+
232
263
  ### Hybrid Search
233
264
 
234
265
  Combines **vector similarity** (semantic meaning) with **FTS5** (exact keyword match) for best recall. FTS5 synced automatically via SQLite triggers.
@@ -250,7 +281,7 @@ Most memory solutions for AI assistants follow the same pattern: capture observa
250
281
  | **Graph navigation** | None | Multi-hop traversal (A &rarr; B &rarr; C) |
251
282
  | **Staleness** | No decay | Unused memories lose relevance over time |
252
283
  | **Duplicates** | Can accumulate | Auto-merged (cosine similarity > 0.92) |
253
- | **Auto-capture** | All tool calls via hooks | Bash, Read, Grep, Edit, Write via hooks + background daemon |
284
+ | **Auto-capture** | All tool calls via hooks | All tools (Bash, Read, Edit, Write, Grep, Glob, Agent, MCP, Web) via hooks + daemon |
254
285
  | **Session injection** | Full context dump on start | Lightweight facts injection via direct SQLite (<1s) |
255
286
  | **Dependencies** | Node.js, Bun, Chroma vector DB | Python + SQLite only (zero external services) |
256
287
  | **Web UI** | Viewer on localhost:37777 | CLI only (planned) |
@@ -13,7 +13,7 @@
13
13
  <img src="https://img.shields.io/badge/License-MIT-blue.svg" alt="License">
14
14
  </a>
15
15
  <a href="pyproject.toml">
16
- <img src="https://img.shields.io/badge/version-0.1.0-green.svg" alt="Version">
16
+ <img src="https://img.shields.io/badge/version-0.3.0-green.svg" alt="Version">
17
17
  </a>
18
18
  <a href="pyproject.toml">
19
19
  <img src="https://img.shields.io/badge/python-%3E%3D3.11-brightgreen.svg" alt="Python">
@@ -75,39 +75,50 @@ The system stops at the cheapest layer that answers the question. **66% fewer to
75
75
  - **Background daemon** &mdash; pre-loaded model for instant saves via Unix socket (~0.3s vs ~5s cold start)
76
76
  - **Multi-language** &mdash; EN, PT (auto-detected). ES, DE, FR with additional spaCy models
77
77
  - **Local-first** &mdash; SQLite + local embeddings + local NLP. Zero API calls, zero network, zero cost
78
+ - **Privacy tags** &mdash; wrap sensitive content in `<private>...</private>` to exclude it from memory
78
79
  - **Fully configurable** &mdash; all thresholds, ratios, and behaviors via config.json
79
80
 
80
81
  ---
81
82
 
82
83
  ## Quick Start
83
84
 
84
- ### Install
85
+ ### One-Command Install
85
86
 
86
87
  ```bash
87
- pip install cortex-claude
88
+ pip install cortex-claude && cortex-claude setup
89
+ ```
90
+
91
+ That's it. The setup command:
92
+ - Configures the MCP server globally for Claude Code
93
+ - Installs auto-capture hooks (SessionStart + PostToolUse)
94
+ - Creates `~/.claude/CLAUDE.md` with instructions for Claude to use Cortex automatically
95
+ - Downloads the embedding model (~80MB) and spaCy model (~12MB)
96
+ - Starts the background daemon for instant saves
97
+
98
+ Restart Claude Code and it works in **every project**, no per-project config needed. Claude will automatically consult Cortex before saying "I don't know" and save important context to memory.
99
+
100
+ ### With Claude-assisted extraction (optional)
88
101
 
89
- # With Claude-assisted extraction (optional)
102
+ ```bash
90
103
  pip install cortex-claude[claude]
91
104
  ```
92
105
 
93
- ### Configure Claude Code
106
+ ### Manual Setup (alternative)
94
107
 
95
- Add a `.mcp.json` to your project root (or `~/.claude.json` for global):
108
+ If you prefer manual configuration, add a `.mcp.json` to your project root:
96
109
 
97
110
  ```json
98
111
  {
99
112
  "mcpServers": {
100
113
  "cortex": {
101
114
  "type": "stdio",
102
- "command": "python",
115
+ "command": "python3",
103
116
  "args": ["-m", "cortex_claude"]
104
117
  }
105
118
  }
106
119
  }
107
120
  ```
108
121
 
109
- First run downloads the embedding model (~80MB) and spaCy model (~12MB) automatically.
110
-
111
122
  ### Use
112
123
 
113
124
  Just talk to Claude naturally. Cortex works in the background:
@@ -191,6 +202,26 @@ score = e^(-lambda * days) * (1 + log(access_count))
191
202
 
192
203
  Recalculated on server startup. Frequently accessed memories get boosted. Stale ones fade.
193
204
 
205
+ ### Auto-Capture &amp; Hooks
206
+
207
+ Cortex uses Claude Code hooks to work automatically:
208
+
209
+ - **SessionStart** &mdash; injects memory stats and known facts when you open a session. Claude knows it has memory and consults it before saying "I don't know".
210
+ - **PostToolUse** &mdash; captures results from **all tools** in background: Bash, Read, Edit, Write, Grep, Glob, Agent, WebSearch, WebFetch, and all third-party MCP tools. No manual save needed.
211
+ - **Background Daemon** &mdash; keeps the embedding model pre-loaded via Unix socket. First save after boot: ~5s (model load). Subsequent saves: **~0.3s**.
212
+
213
+ All hooks are installed globally by `cortex-claude setup`. No per-project config needed.
214
+
215
+ ### Privacy
216
+
217
+ Wrap sensitive content in `<private>` tags to exclude it from memory:
218
+
219
+ ```
220
+ The API uses JWT for auth. <private>API_KEY=sk-abc123secret</private> Tokens expire in 24h.
221
+ ```
222
+
223
+ Cortex strips everything between `<private>...</private>` before saving. Works in both manual saves and auto-capture. If the entire content is private, nothing is saved.
224
+
194
225
  ### Hybrid Search
195
226
 
196
227
  Combines **vector similarity** (semantic meaning) with **FTS5** (exact keyword match) for best recall. FTS5 synced automatically via SQLite triggers.
@@ -212,7 +243,7 @@ Most memory solutions for AI assistants follow the same pattern: capture observa
212
243
  | **Graph navigation** | None | Multi-hop traversal (A &rarr; B &rarr; C) |
213
244
  | **Staleness** | No decay | Unused memories lose relevance over time |
214
245
  | **Duplicates** | Can accumulate | Auto-merged (cosine similarity > 0.92) |
215
- | **Auto-capture** | All tool calls via hooks | Bash, Read, Grep, Edit, Write via hooks + background daemon |
246
+ | **Auto-capture** | All tool calls via hooks | All tools (Bash, Read, Edit, Write, Grep, Glob, Agent, MCP, Web) via hooks + daemon |
216
247
  | **Session injection** | Full context dump on start | Lightweight facts injection via direct SQLite (<1s) |
217
248
  | **Dependencies** | Node.js, Bun, Chroma vector DB | Python + SQLite only (zero external services) |
218
249
  | **Web UI** | Viewer on localhost:37777 | CLI only (planned) |
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "cortex-claude"
3
- version = "0.2.0"
3
+ version = "0.3.0"
4
4
  description = "Local-first, token-efficient memory system for Claude Code via MCP"
5
5
  readme = "README.md"
6
6
  license = { text = "MIT" }
@@ -0,0 +1 @@
1
+ __version__ = "0.3.0"
@@ -1,12 +1,16 @@
1
1
  import sys
2
2
 
3
- from cortex_claude.server.app import mcp, _init_engine
4
-
5
3
 
6
4
  def main() -> None:
7
- if len(sys.argv) > 1 and sys.argv[1] == "daemon":
5
+ cmd = sys.argv[1] if len(sys.argv) > 1 else ""
6
+
7
+ if cmd == "setup":
8
+ from cortex_claude.setup import run_setup
9
+ run_setup()
10
+ elif cmd == "daemon":
8
11
  _run_daemon()
9
12
  else:
13
+ from cortex_claude.server.app import mcp, _init_engine
10
14
  _init_engine()
11
15
  mcp.run(transport="stdio")
12
16
 
@@ -6,6 +6,7 @@ from pathlib import Path
6
6
  from typing import Literal
7
7
 
8
8
  from cortex_claude.core.decay import recalculate_decay_scores
9
+ from cortex_claude.core.privacy import is_fully_private, strip_private
9
10
  from cortex_claude.core.scope_manager import ScopeManager
10
11
  from cortex_claude.core.token_budget import TokenBudget
11
12
  from cortex_claude.embeddings import EmbeddingEngine, count_tokens
@@ -58,6 +59,11 @@ class CortexEngine:
58
59
  scope: str | None = None,
59
60
  cwd: str = ".",
60
61
  ) -> SaveResult:
62
+ if is_fully_private(content):
63
+ return SaveResult(memory_id="", scope="", tokens_stored=0)
64
+
65
+ content = strip_private(content)
66
+
61
67
  write_scope = scope or self._scope_manager.get_write_scope(cwd)
62
68
  conn = self._storage.get_database(write_scope)
63
69
 
@@ -0,0 +1,21 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+
5
+ PRIVATE_PATTERN = re.compile(r"<private>.*?</private>", re.DOTALL | re.IGNORECASE)
6
+ PRIVATE_BLOCK_PATTERN = re.compile(r"<private\s*/?>", re.IGNORECASE)
7
+
8
+
9
+ def strip_private(text: str) -> str:
10
+ text = PRIVATE_PATTERN.sub("", text)
11
+ text = PRIVATE_BLOCK_PATTERN.sub("", text)
12
+ return text.strip()
13
+
14
+
15
+ def contains_private(text: str) -> bool:
16
+ return bool(PRIVATE_PATTERN.search(text) or PRIVATE_BLOCK_PATTERN.search(text))
17
+
18
+
19
+ def is_fully_private(text: str) -> bool:
20
+ stripped = strip_private(text)
21
+ return len(stripped) == 0
@@ -0,0 +1,418 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import os
5
+ import shutil
6
+ import stat
7
+ import subprocess
8
+ import sys
9
+ from pathlib import Path
10
+
11
+ CORTEX_HOME = Path(os.environ.get("CORTEX_HOME", str(Path.home() / ".cortex-claude")))
12
+ CLAUDE_CONFIG = Path.home() / ".claude.json"
13
+ CLAUDE_SETTINGS = Path.home() / ".claude" / "settings.json"
14
+ CLAUDE_MD = Path.home() / ".claude" / "CLAUDE.md"
15
+ HOOKS_DIR = CORTEX_HOME / "hooks"
16
+
17
+ CLAUDE_MD_CONTENT = """# Cortex Memory
18
+
19
+ You have access to **Cortex**, a persistent memory system with a knowledge graph. Use it.
20
+
21
+ ## Rules
22
+
23
+ 1. **Before saying "I don't know"** — always check Cortex first with `cortex_recall` or `cortex_facts`.
24
+ 2. **Save important context** — when you learn something significant about the project, user preferences, architecture decisions, or bugs, save it with `cortex_save`.
25
+ 3. **Use facts for quick lookups** — `cortex_facts` returns structured triplets at minimal token cost. Use it for "what does X use?" type questions.
26
+ 4. **Use traverse for exploration** — `cortex_traverse` follows entity connections. Use it to discover related information you didn't know to ask about.
27
+ 5. **Respect privacy** — never save content wrapped in `<private>...</private>` tags. Cortex strips these automatically, but be mindful of sensitive data.
28
+
29
+ ## Available Tools
30
+
31
+ | Tool | When to use |
32
+ |------|------------|
33
+ | `cortex_save` | Save information worth remembering across sessions |
34
+ | `cortex_recall` | Search memories by meaning (semantic + keyword) |
35
+ | `cortex_facts` | Quick structured lookup from knowledge graph |
36
+ | `cortex_traverse` | Explore connections: start from entity, follow links |
37
+ | `cortex_forget` | Delete outdated or wrong memories (dry-run first) |
38
+ | `cortex_scopes` | Manage memory scopes (list, create, link) |
39
+ | `cortex_status` | Check memory stats (count, size, scopes) |
40
+
41
+ ## What to Save
42
+
43
+ - Architecture decisions and their rationale
44
+ - User preferences and conventions
45
+ - Bug context and root causes
46
+ - Project-specific terminology
47
+ - Environment and deployment details
48
+ - API contracts and integrations
49
+
50
+ ## What NOT to Save
51
+
52
+ - Code that's already in files (it's in the repo)
53
+ - Temporary debugging output
54
+ - Content marked with `<private>` tags
55
+ - Information that changes every commit (use git for that)
56
+ """
57
+
58
+ SESSION_START_HOOK = '''#!/bin/bash
59
+ CORTEX_HOME="${CORTEX_HOME:-$HOME/.cortex-claude}"
60
+ GLOBAL_DB="$CORTEX_HOME/global.db"
61
+ SOCKET="$CORTEX_HOME/cortex.sock"
62
+
63
+ if [ ! -S "$SOCKET" ]; then
64
+ python3 -c "
65
+ import sys
66
+ sys.path.insert(0, '.')
67
+ try:
68
+ from cortex_claude.daemon import ensure_running
69
+ ensure_running()
70
+ except Exception:
71
+ pass
72
+ " 2>/dev/null &
73
+ fi
74
+
75
+ [ ! -f "$GLOBAL_DB" ] && exit 0
76
+
77
+ CONTEXT=$(python3 -c "
78
+ import sqlite3, os, glob
79
+
80
+ cortex_home = os.path.expanduser('${CORTEX_HOME:-~/.cortex-claude}')
81
+ dbs = []
82
+
83
+ global_db = os.path.join(cortex_home, 'global.db')
84
+ if os.path.exists(global_db):
85
+ dbs.append(('global', global_db))
86
+
87
+ scopes_dir = os.path.join(cortex_home, 'scopes')
88
+ if os.path.isdir(scopes_dir):
89
+ for f in glob.glob(os.path.join(scopes_dir, '*.db')):
90
+ name = os.path.basename(f).replace('.db','').replace('__',':')
91
+ dbs.append((name, f))
92
+
93
+ if not dbs:
94
+ exit(0)
95
+
96
+ total_memories = 0
97
+ total_facts = 0
98
+ recent_facts = []
99
+
100
+ for scope_name, db_path in dbs:
101
+ try:
102
+ conn = sqlite3.connect(db_path)
103
+ conn.row_factory = sqlite3.Row
104
+ row = conn.execute('SELECT COUNT(*) as c FROM memories').fetchone()
105
+ total_memories += row[0]
106
+ row = conn.execute('SELECT COUNT(*) as c FROM facts').fetchone()
107
+ total_facts += row[0]
108
+ rows = conn.execute(
109
+ 'SELECT subject, relation, object FROM facts ORDER BY confidence DESC, created_at DESC LIMIT 15'
110
+ ).fetchall()
111
+ for r in rows:
112
+ recent_facts.append(f'{r[0]} -> {r[1]} -> {r[2]}')
113
+ conn.close()
114
+ except Exception:
115
+ pass
116
+
117
+ if total_memories == 0:
118
+ exit(0)
119
+
120
+ lines = [
121
+ 'You have access to Cortex persistent memory (cortex_save, cortex_recall, cortex_facts, cortex_traverse, cortex_forget, cortex_scopes, cortex_status).',
122
+ 'IMPORTANT: Always use cortex_recall or cortex_facts BEFORE saying you do not know something.',
123
+ f'Memory: {total_memories} memories, {total_facts} facts, {len(dbs)} scope(s).',
124
+ ]
125
+
126
+ if recent_facts:
127
+ lines.append('')
128
+ lines.append('Known facts:')
129
+ for f in recent_facts[:10]:
130
+ lines.append(f' {f}')
131
+
132
+ print(chr(10).join(lines))
133
+ " 2>/dev/null)
134
+
135
+ if [ -n "$CONTEXT" ]; then
136
+ jq -n --arg ctx "$CONTEXT" '{
137
+ hookSpecificOutput: {
138
+ hookEventName: "SessionStart",
139
+ additionalContext: $ctx
140
+ }
141
+ }'
142
+ fi
143
+
144
+ exit 0
145
+ '''
146
+
147
+ POST_TOOL_USE_HOOK = '''#!/bin/bash
148
+ CORTEX_HOME="${CORTEX_HOME:-$HOME/.cortex-claude}"
149
+ SOCKET="$CORTEX_HOME/cortex.sock"
150
+
151
+ [ ! -S "$SOCKET" ] && exit 0
152
+
153
+ INPUT=$(cat)
154
+
155
+ (echo "$INPUT" | python3 -c "
156
+ import json, socket, sys, os
157
+
158
+ data = json.load(sys.stdin)
159
+ tool = data.get('tool_name', '')
160
+
161
+ if 'cortex' in tool.lower():
162
+ sys.exit(0)
163
+
164
+ ti = data.get('tool_input', {})
165
+ tr = str(data.get('tool_response', ''))[:1000]
166
+ cwd = data.get('cwd', '.')
167
+
168
+ import re
169
+ def strip_private(t):
170
+ t = re.sub(r'<private>.*?</private>', '', t, flags=re.DOTALL|re.IGNORECASE)
171
+ return re.sub(r'<private\\s*/?>', '', t, flags=re.IGNORECASE).strip()
172
+
173
+ tr = strip_private(tr)
174
+ for k, v in list(ti.items()):
175
+ if isinstance(v, str):
176
+ ti[k] = strip_private(v)
177
+
178
+ if len(tr) < 20:
179
+ sys.exit(0)
180
+
181
+ content = None
182
+ tags = ['auto-capture']
183
+
184
+ if tool == 'Bash':
185
+ cmd = str(ti.get('command', ''))[:150]
186
+ noise = ('ls', 'cd ', 'pwd', 'echo ', 'which ', 'cat ', 'head ', 'tail ', 'mkdir', 'touch ', 'chmod', 'true', 'false', 'test ')
187
+ if any(cmd.startswith(n) for n in noise) or not cmd:
188
+ sys.exit(0)
189
+ content = f'Command: {cmd}' + chr(10) + f'Result: {tr[:500]}'
190
+ tags.append('bash')
191
+ elif tool == 'Read':
192
+ path = ti.get('file_path', '')
193
+ content = f'Read file: {path}' + chr(10) + f'Preview: {tr[:300]}'
194
+ tags.append('file-read')
195
+ elif tool in ('Edit', 'Write'):
196
+ path = ti.get('file_path', '')
197
+ old = str(ti.get('old_string', ''))[:100]
198
+ new = str(ti.get('new_string', ''))[:100]
199
+ if old and new:
200
+ content = f'Edited {path}: replaced \\\"{old}\\\" with \\\"{new}\\\"'
201
+ elif tool == 'Write':
202
+ content = f'Created file: {path}'
203
+ else:
204
+ content = f'Modified file: {path}'
205
+ tags.append('file-change')
206
+ elif tool == 'Grep':
207
+ pattern = ti.get('pattern', '')
208
+ path = ti.get('path', '')
209
+ content = f'Search \\\"{pattern}\\\" in {path}' + chr(10) + f'Results: {tr[:500]}'
210
+ tags.append('search')
211
+ elif tool == 'Glob':
212
+ pattern = ti.get('pattern', '')
213
+ content = f'File search: {pattern}' + chr(10) + f'Found: {tr[:500]}'
214
+ tags.append('search')
215
+ elif tool == 'Agent':
216
+ prompt = str(ti.get('prompt', ''))[:200]
217
+ content = f'Agent task: {prompt}' + chr(10) + f'Result: {tr[:500]}'
218
+ tags.append('agent')
219
+ elif tool.startswith('mcp__'):
220
+ parts = tool.split('__')
221
+ server = parts[1] if len(parts) > 1 else 'unknown'
222
+ method = parts[2] if len(parts) > 2 else 'unknown'
223
+ content = f'MCP {server}.{method}' + chr(10) + f'Input: {json.dumps(ti)[:200]}' + chr(10) + f'Result: {tr[:400]}'
224
+ tags.append('mcp')
225
+ tags.append(server)
226
+ elif tool in ('WebSearch', 'WebFetch'):
227
+ query = ti.get('query', ti.get('url', ''))
228
+ content = f'{tool}: {query}' + chr(10) + f'Result: {tr[:500]}'
229
+ tags.append('web')
230
+ else:
231
+ content = f'{tool}: {json.dumps(ti)[:150]}' + chr(10) + f'Result: {tr[:400]}'
232
+ tags.append(tool.lower())
233
+
234
+ if not content:
235
+ sys.exit(0)
236
+
237
+ sock_path = os.path.expanduser('$SOCKET')
238
+ try:
239
+ s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
240
+ s.settimeout(3)
241
+ s.connect(sock_path)
242
+ s.sendall(json.dumps({'action':'save','content':content,'tags':tags,'cwd':cwd}).encode())
243
+ s.shutdown(socket.SHUT_WR)
244
+ s.recv(512)
245
+ s.close()
246
+ except Exception:
247
+ pass
248
+ ") &
249
+
250
+ exit 0
251
+ '''
252
+
253
+
254
+ def _print(msg: str) -> None:
255
+ print(f" {msg}")
256
+
257
+
258
+ def _ok(msg: str) -> None:
259
+ print(f" [ok] {msg}")
260
+
261
+
262
+ def _skip(msg: str) -> None:
263
+ print(f" [skip] {msg}")
264
+
265
+
266
+ def run_setup() -> None:
267
+ print()
268
+ print(" Cortex Claude Setup")
269
+ print(" ====================")
270
+ print()
271
+
272
+ # 1. Create data directory
273
+ CORTEX_HOME.mkdir(parents=True, exist_ok=True)
274
+ (CORTEX_HOME / "scopes").mkdir(exist_ok=True)
275
+ _ok(f"Data directory: {CORTEX_HOME}")
276
+
277
+ # 2. Install hooks
278
+ HOOKS_DIR.mkdir(parents=True, exist_ok=True)
279
+
280
+ session_start = HOOKS_DIR / "session-start.sh"
281
+ session_start.write_text(SESSION_START_HOOK)
282
+ session_start.chmod(session_start.stat().st_mode | stat.S_IEXEC)
283
+
284
+ post_tool_use = HOOKS_DIR / "post-tool-use.sh"
285
+ post_tool_use.write_text(POST_TOOL_USE_HOOK)
286
+ post_tool_use.chmod(post_tool_use.stat().st_mode | stat.S_IEXEC)
287
+
288
+ _ok(f"Hooks installed: {HOOKS_DIR}")
289
+
290
+ # 3. Configure Claude Code MCP (global)
291
+ mcp_config = {}
292
+ if CLAUDE_CONFIG.exists():
293
+ try:
294
+ with open(CLAUDE_CONFIG) as f:
295
+ mcp_config = json.load(f)
296
+ except json.JSONDecodeError:
297
+ mcp_config = {}
298
+
299
+ mcp_config.setdefault("mcpServers", {})["cortex"] = {
300
+ "type": "stdio",
301
+ "command": "python3",
302
+ "args": ["-m", "cortex_claude"],
303
+ }
304
+
305
+ with open(CLAUDE_CONFIG, "w") as f:
306
+ json.dump(mcp_config, f, indent=2)
307
+
308
+ _ok(f"MCP server configured: {CLAUDE_CONFIG}")
309
+
310
+ # 4. Configure hooks in Claude settings (global)
311
+ CLAUDE_SETTINGS.parent.mkdir(parents=True, exist_ok=True)
312
+
313
+ settings = {}
314
+ if CLAUDE_SETTINGS.exists():
315
+ try:
316
+ with open(CLAUDE_SETTINGS) as f:
317
+ settings = json.load(f)
318
+ except json.JSONDecodeError:
319
+ settings = {}
320
+
321
+ hooks = settings.setdefault("hooks", {})
322
+
323
+ hooks["SessionStart"] = [
324
+ {
325
+ "matcher": "",
326
+ "hooks": [
327
+ {
328
+ "type": "command",
329
+ "command": str(session_start),
330
+ "timeout": 10,
331
+ }
332
+ ],
333
+ }
334
+ ]
335
+
336
+ hooks["PostToolUse"] = [
337
+ {
338
+ "matcher": "",
339
+ "hooks": [
340
+ {
341
+ "type": "command",
342
+ "command": str(post_tool_use),
343
+ "async": True,
344
+ "timeout": 30,
345
+ }
346
+ ],
347
+ }
348
+ ]
349
+
350
+ with open(CLAUDE_SETTINGS, "w") as f:
351
+ json.dump(settings, f, indent=2)
352
+
353
+ _ok(f"Hooks configured: {CLAUDE_SETTINGS}")
354
+
355
+ # 5. Create/update CLAUDE.md
356
+ CLAUDE_MD.parent.mkdir(parents=True, exist_ok=True)
357
+
358
+ cortex_marker = "# Cortex Memory"
359
+ if CLAUDE_MD.exists():
360
+ existing = CLAUDE_MD.read_text()
361
+ if cortex_marker in existing:
362
+ _skip(f"CLAUDE.md already has Cortex instructions")
363
+ else:
364
+ with open(CLAUDE_MD, "a") as f:
365
+ f.write("\n\n" + CLAUDE_MD_CONTENT)
366
+ _ok(f"Cortex instructions appended to {CLAUDE_MD}")
367
+ else:
368
+ CLAUDE_MD.write_text(CLAUDE_MD_CONTENT)
369
+ _ok(f"CLAUDE.md created: {CLAUDE_MD}")
370
+
371
+ # 7. Download spaCy model
372
+ _print("Downloading spaCy model (en_core_web_sm)...")
373
+ try:
374
+ import spacy
375
+ try:
376
+ spacy.load("en_core_web_sm")
377
+ _skip("spaCy model already installed")
378
+ except OSError:
379
+ subprocess.run(
380
+ [sys.executable, "-m", "spacy", "download", "en_core_web_sm"],
381
+ capture_output=True,
382
+ )
383
+ _ok("spaCy model downloaded")
384
+ except ImportError:
385
+ _print("spaCy not installed, skipping model download")
386
+
387
+ # 8. Pre-warm embedding model
388
+ _print("Downloading embedding model (all-MiniLM-L6-v2)...")
389
+ try:
390
+ from sentence_transformers import SentenceTransformer
391
+ SentenceTransformer("all-MiniLM-L6-v2")
392
+ _ok("Embedding model ready")
393
+ except Exception as e:
394
+ _print(f"Could not pre-load embedding model: {e}")
395
+
396
+ # 9. Start daemon
397
+ _print("Starting Cortex daemon...")
398
+ try:
399
+ from cortex_claude.daemon import ensure_running, is_running
400
+ ensure_running()
401
+ if is_running():
402
+ _ok("Daemon running")
403
+ else:
404
+ _print("Daemon may take a few seconds to start")
405
+ except Exception as e:
406
+ _print(f"Could not start daemon: {e}")
407
+
408
+ # Done
409
+ print()
410
+ print(" Setup complete!")
411
+ print()
412
+ print(" Cortex is now configured globally for Claude Code.")
413
+ print(" Restart Claude Code to activate.")
414
+ print()
415
+ print(" Usage:")
416
+ print(" Just talk to Claude — memories are captured automatically.")
417
+ print(" Use 'cortex_recall', 'cortex_facts', 'cortex_traverse' to query.")
418
+ print()
@@ -0,0 +1,48 @@
1
+ from __future__ import annotations
2
+
3
+ import pytest
4
+
5
+ from cortex_claude.core.engine import CortexEngine
6
+
7
+
8
+ @pytest.mark.asyncio
9
+ async def test_save_strips_private_content(engine: CortexEngine):
10
+ await engine.save(
11
+ content="The API uses JWT. <private>API_KEY=sk-abc123</private> Auth works well."
12
+ )
13
+
14
+ result = await engine.recall(query="API JWT auth", depth="full", max_tokens=500)
15
+ for m in result.memories:
16
+ assert "sk-abc123" not in m.content
17
+ assert "API_KEY" not in m.content
18
+
19
+
20
+ @pytest.mark.asyncio
21
+ async def test_save_fully_private_is_skipped(engine: CortexEngine):
22
+ result = await engine.save(content="<private>Everything is secret</private>")
23
+ assert result.memory_id == ""
24
+ assert result.tokens_stored == 0
25
+
26
+
27
+ @pytest.mark.asyncio
28
+ async def test_save_preserves_public_content(engine: CortexEngine):
29
+ await engine.save(
30
+ content="Database is PostgreSQL. <private>password=hunter2</private> Uses port 5432."
31
+ )
32
+
33
+ result = await engine.recall(query="database", depth="full", max_tokens=500)
34
+ assert len(result.memories) > 0
35
+ has_postgres = any("PostgreSQL" in m.content for m in result.memories)
36
+ assert has_postgres
37
+
38
+
39
+ @pytest.mark.asyncio
40
+ async def test_facts_dont_contain_private(engine: CortexEngine):
41
+ await engine.save(
42
+ content="The service uses Redis. <private>REDIS_URL=redis://secret:6379</private>"
43
+ )
44
+
45
+ facts = await engine.get_facts(topic="redis")
46
+ for f in facts:
47
+ assert "secret" not in f.subject
48
+ assert "secret" not in f.object
@@ -0,0 +1,57 @@
1
+ from cortex_claude.core.privacy import contains_private, is_fully_private, strip_private
2
+
3
+
4
+ def test_strip_private_inline():
5
+ text = "The API key is <private>sk-abc123xyz</private> and it works."
6
+ result = strip_private(text)
7
+ assert "sk-abc123xyz" not in result
8
+ assert "API key" in result
9
+
10
+
11
+ def test_strip_private_multiline():
12
+ text = "Public info.\n<private>\nSECRET_KEY=abc\nDB_PASSWORD=xyz\n</private>\nMore public."
13
+ result = strip_private(text)
14
+ assert "SECRET_KEY" not in result
15
+ assert "DB_PASSWORD" not in result
16
+ assert "Public info" in result
17
+ assert "More public" in result
18
+
19
+
20
+ def test_strip_private_multiple():
21
+ text = "A <private>secret1</private> B <private>secret2</private> C"
22
+ result = strip_private(text)
23
+ assert "secret1" not in result
24
+ assert "secret2" not in result
25
+ assert "A" in result
26
+ assert "B" in result
27
+ assert "C" in result
28
+
29
+
30
+ def test_strip_private_self_closing():
31
+ text = "Before <private/> After"
32
+ result = strip_private(text)
33
+ assert "Before" in result
34
+ assert "After" in result
35
+
36
+
37
+ def test_strip_private_case_insensitive():
38
+ text = "Data <PRIVATE>hidden</PRIVATE> visible"
39
+ result = strip_private(text)
40
+ assert "hidden" not in result
41
+ assert "visible" in result
42
+
43
+
44
+ def test_contains_private():
45
+ assert contains_private("Has <private>secret</private> here") is True
46
+ assert contains_private("No secrets here") is False
47
+
48
+
49
+ def test_is_fully_private():
50
+ assert is_fully_private("<private>all secret</private>") is True
51
+ assert is_fully_private("public <private>secret</private>") is False
52
+ assert is_fully_private("no tags at all") is False
53
+
54
+
55
+ def test_strip_private_no_tags():
56
+ text = "Normal text without any tags"
57
+ assert strip_private(text) == text
@@ -317,7 +317,7 @@ wheels = [
317
317
 
318
318
  [[package]]
319
319
  name = "cortex-claude"
320
- version = "0.1.0"
320
+ version = "0.2.0"
321
321
  source = { editable = "." }
322
322
  dependencies = [
323
323
  { name = "mcp" },
@@ -1,61 +0,0 @@
1
- #!/bin/bash
2
- # Cortex Claude — PostToolUse hook
3
- # Sends tool results to daemon via Unix socket.
4
- # Uses only stdlib Python — no cortex_claude imports, instant startup.
5
-
6
- CORTEX_HOME="${CORTEX_HOME:-$HOME/.cortex-claude}"
7
- SOCKET="$CORTEX_HOME/cortex.sock"
8
-
9
- # No daemon = no save
10
- [ ! -S "$SOCKET" ] && exit 0
11
-
12
- # Pipe stdin to a lightweight Python script in background
13
- python3 -c "
14
- import json, socket, sys, os
15
-
16
- data = json.load(sys.stdin)
17
- tool = data.get('tool_name', '')
18
-
19
- if tool not in ('Bash', 'Read', 'Grep', 'Edit', 'Write'):
20
- sys.exit(0)
21
- if tool.startswith('mcp__cortex') or tool.startswith('cortex_'):
22
- sys.exit(0)
23
-
24
- ti = data.get('tool_input', {})
25
- tr = str(data.get('tool_response', ''))[:800]
26
- cwd = data.get('cwd', '.')
27
-
28
- if len(tr) < 30:
29
- sys.exit(0)
30
-
31
- if tool == 'Bash':
32
- cmd = str(ti.get('command', ''))[:100]
33
- if cmd.split()[0] in ('ls','cd','pwd','echo','which','cat','head','tail') if cmd else True:
34
- sys.exit(0)
35
- content = 'Command: ' + cmd + '\nResult: ' + tr[:400]
36
- tags = ['auto-capture', 'bash']
37
- elif tool == 'Read':
38
- content = 'Read file: ' + ti.get('file_path', '')
39
- tags = ['auto-capture', 'file-read']
40
- elif tool in ('Edit', 'Write'):
41
- content = 'Modified file: ' + ti.get('file_path', '')
42
- tags = ['auto-capture', 'file-change']
43
- elif tool == 'Grep':
44
- content = 'Search: ' + ti.get('pattern', '') + '\nResults: ' + tr[:400]
45
- tags = ['auto-capture', 'search']
46
- else:
47
- sys.exit(0)
48
-
49
- try:
50
- s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
51
- s.settimeout(3)
52
- s.connect(os.path.expanduser('$SOCKET'))
53
- s.sendall(json.dumps({'action':'save','content':content,'tags':tags,'cwd':cwd}).encode())
54
- s.shutdown(socket.SHUT_WR)
55
- s.recv(512)
56
- s.close()
57
- except Exception:
58
- pass
59
- " &
60
-
61
- exit 0
@@ -1 +0,0 @@
1
- __version__ = "0.2.0"
File without changes
File without changes