cli-mem 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,315 @@
1
+ Metadata-Version: 2.4
2
+ Name: cli-mem
3
+ Version: 0.1.0
4
+ Summary: Privacy-first CLI that turns shell history into searchable memory
5
+ Project-URL: GitHub, https://github.com/matinsaurralde/mem
6
+ Author: Matias Insaurralde
7
+ License-Expression: MIT
8
+ License-File: LICENSE
9
+ Classifier: Development Status :: 3 - Alpha
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.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Topic :: System :: Shells
18
+ Requires-Python: >=3.10
19
+ Requires-Dist: click
20
+ Requires-Dist: pydantic
21
+ Requires-Dist: rich
22
+ Provides-Extra: ai
23
+ Requires-Dist: apple-fm-sdk; extra == 'ai'
24
+ Provides-Extra: dev
25
+ Requires-Dist: pytest; extra == 'dev'
26
+ Requires-Dist: pytest-asyncio; extra == 'dev'
27
+ Requires-Dist: ruff; extra == 'dev'
28
+ Description-Content-Type: text/markdown
29
+
30
+ <p align="center">
31
+ <h1 align="center">mem</h1>
32
+ <p align="center">
33
+ <strong>Your shell history, understood. Not just searched.</strong>
34
+ </p>
35
+ <p align="center">
36
+ A privacy-first CLI that turns your terminal history into an intelligent,<br>
37
+ searchable memory system — powered by on-device AI, with zero cloud dependencies.
38
+ </p>
39
+ <p align="center">
40
+ <a href="#installation"><img alt="macOS 26+" src="https://img.shields.io/badge/macOS-26%2B-blue?logo=apple&logoColor=white"></a>
41
+ <a href="#installation"><img alt="Python 3.10+" src="https://img.shields.io/badge/python-3.10%2B-3776AB?logo=python&logoColor=white"></a>
42
+ <a href="LICENSE"><img alt="License: MIT" src="https://img.shields.io/badge/license-MIT-green"></a>
43
+ <a href="PHILOSOPHY.md"><img alt="Privacy: 100% on-device" src="https://img.shields.io/badge/privacy-100%25%20on--device-brightgreen"></a>
44
+ </p>
45
+ </p>
46
+
47
+ <p align="center">
48
+ <img src="assets/demo.gif" alt="mem demo" width="700">
49
+ </p>
50
+
51
+ <!-- TODO: Record a demo GIF showing: mem deploy → results from different repos -->
52
+ <!-- Use https://github.com/faressoft/terminalizer or asciinema + agg -->
53
+
54
+ ---
55
+
56
+ Unlike `Ctrl+R`, mem knows *where* you are. The same query returns different results depending on your current git repository — because `kubectl apply` means something different in your infra repo than in your backend repo.
57
+
58
+ Unlike cloud-based history tools, **nothing ever leaves your machine**. Every command, pattern, and session stays in plain text files you can `cat`, `grep`, and `tail`.
59
+
60
+ ## Features
61
+
62
+ - **Context-aware search** — results ranked by your current git repo, not just recency
63
+ - **AI pattern extraction** — learns that `kubectl get pods`, `kubectl get services`, `kubectl get deployments` are all `kubectl get <resource>`
64
+ - **100% on-device** — uses Apple Foundation Models locally. Zero network. Zero telemetry
65
+ - **Plain text storage** — everything in `~/.mem/` as JSONL files. Inspect with `cat`. Search with `grep`
66
+ - **Silent capture** — shell hook adds <5ms to prompt. You won't notice it
67
+ - **Session replay** — recall the exact sequence of commands from last Tuesday's debugging session
68
+
69
+ ## Quick start
70
+
71
+ ### 1. Install
72
+
73
+ ```bash
74
+ # Homebrew (recommended)
75
+ brew install matinsaurralde/tap/mem
76
+
77
+ # Quick install script
78
+ curl -fsSL https://raw.githubusercontent.com/matinsaurralde/mem/main/install.sh | bash
79
+
80
+ # pip / pipx
81
+ pipx install mem-cli
82
+ ```
83
+
84
+ ### 2. Activate
85
+
86
+ ```bash
87
+ echo 'eval "$(mem init zsh)"' >> ~/.zshrc
88
+ source ~/.zshrc
89
+ ```
90
+
91
+ ### 3. Use your terminal
92
+
93
+ Every command is silently captured with full context — directory, git repo, exit code, duration.
94
+
95
+ ### 4. Search
96
+
97
+ ```bash
98
+ mem deploy
99
+ ```
100
+
101
+ ```
102
+ 1 kubectl apply -f deployment.yaml infra 2h ago
103
+ 2 docker compose up -d backend 1d ago
104
+ 3 fly deploy api 3d ago
105
+ ```
106
+
107
+ That's it. mem gets smarter the more you use it.
108
+
109
+ ## Usage
110
+
111
+ ### Search history
112
+
113
+ ```bash
114
+ mem kubectl # search by keyword
115
+ mem "docker compose" # search by phrase
116
+ mem deploy -n 20 # show more results
117
+ mem deploy --json # machine-readable output
118
+ ```
119
+
120
+ ### See patterns
121
+
122
+ After running `mem sync`, mem uses on-device AI to extract structural patterns from your history:
123
+
124
+ ```bash
125
+ mem kubectl --pattern
126
+ ```
127
+
128
+ ```
129
+ Patterns for "kubectl":
130
+
131
+ kubectl get <resource>
132
+ kubectl describe <resource> <name>
133
+ kubectl logs <pod> [--tail=<n>]
134
+ kubectl apply -f <file>
135
+ ```
136
+
137
+ ### Recall sessions
138
+
139
+ ```bash
140
+ mem session "api outage"
141
+ ```
142
+
143
+ ```
144
+ +-----------------------------------------+
145
+ | Session: 2026-03-07 14:30 myapp |
146
+ |-----------------------------------------|
147
+ | 1 kubectl logs api-7f9b --tail=100 |
148
+ | 2 kubectl get pods -n production |
149
+ | 3 kubectl rollout restart deploy api |
150
+ | 4 curl -s localhost:8080/health |
151
+ +-----------------------------------------+
152
+
153
+ Replay a session? [number/n]: _
154
+ ```
155
+
156
+ ### More commands
157
+
158
+ ```bash
159
+ mem stats # top commands, repos, totals
160
+ mem sync # extract patterns + clean old data
161
+ mem forget "API_KEY=sk-..." # permanently delete sensitive commands
162
+ mem init zsh # print shell hook code
163
+ ```
164
+
165
+ ## How it works
166
+
167
+ ```
168
+ You type a command
169
+
170
+
171
+ Shell hook (preexec/precmd)
172
+
173
+
174
+ mem _capture ← runs in background, <5ms
175
+
176
+
177
+ Append one JSON line to ~/.mem/repos/<repo>.jsonl
178
+ ```
179
+
180
+ When you search, mem reads the JSONL file for your current repo and scores each command:
181
+
182
+ ```
183
+ score = (frequency × 0.4) + (recency × 0.4) + (context × 0.2)
184
+ ```
185
+
186
+ - **Frequency** — how often you've run this exact command
187
+ - **Recency** — exponential decay with a 7-day half-life
188
+ - **Context** — 1.0 if same repo, 0.5 if same directory prefix, 0.0 otherwise
189
+
190
+ Pattern extraction uses [Apple Foundation Models](https://developer.apple.com/machine-learning/api/) running entirely on-device. No API keys, no cloud calls, no data exfiltration — just your Mac's neural engine.
191
+
192
+ ## Storage
193
+
194
+ All data lives in `~/.mem/` as human-readable plain text:
195
+
196
+ ```
197
+ ~/.mem/
198
+ repos/
199
+ infra-k8s.jsonl # commands from this git repo
200
+ backend.jsonl
201
+ _global.jsonl # commands outside any repo
202
+ sessions/
203
+ 2026-03-05.jsonl # grouped work sessions
204
+ patterns/
205
+ kubectl.json # AI-extracted patterns
206
+ docker.json
207
+ ```
208
+
209
+ Every file is inspectable:
210
+
211
+ ```bash
212
+ cat ~/.mem/repos/myapp.jsonl
213
+ tail -f ~/.mem/repos/myapp.jsonl # watch commands arrive in real-time
214
+ grep "docker" ~/.mem/repos/*.jsonl # search across all repos with grep
215
+ ```
216
+
217
+ ## Privacy
218
+
219
+ mem is built on a simple promise: **your shell history never leaves your machine**.
220
+
221
+ - Zero network requests — not even update checks
222
+ - Zero telemetry — no usage tracking, no analytics, no crash reports
223
+ - Zero cloud dependencies — works fully offline, always
224
+ - On-device AI only — Apple Foundation Models run on your Mac's neural engine
225
+ - Plain text storage — no proprietary formats, no encrypted blobs. You own your data
226
+
227
+ Read more in [PHILOSOPHY.md](PHILOSOPHY.md).
228
+
229
+ ## Requirements
230
+
231
+ | Requirement | Version |
232
+ |-------------|---------|
233
+ | macOS | 26.0+ |
234
+ | Python | 3.10+ |
235
+ | Apple Intelligence | Enabled (for pattern extraction) |
236
+
237
+ > **Note:** mem works without Apple Intelligence — you just won't get AI-extracted patterns. Search, capture, and everything else works fine.
238
+
239
+ ## Installation
240
+
241
+ ### Homebrew
242
+
243
+ ```bash
244
+ brew tap matinsaurralde/tap
245
+ brew install mem
246
+ ```
247
+
248
+ ### Quick install
249
+
250
+ ```bash
251
+ curl -fsSL https://raw.githubusercontent.com/matinsaurralde/mem/main/install.sh | bash
252
+ ```
253
+
254
+ ### pipx (recommended for Python users)
255
+
256
+ ```bash
257
+ pipx install mem-cli
258
+ ```
259
+
260
+ ### From source
261
+
262
+ ```bash
263
+ git clone https://github.com/matinsaurralde/mem.git
264
+ cd mem
265
+ pip install -e ".[ai]" # with AI pattern extraction
266
+ pip install -e "." # without AI (search-only)
267
+ ```
268
+
269
+ ### Shell setup
270
+
271
+ After installation, add the hook to your shell:
272
+
273
+ ```bash
274
+ # zsh (v1)
275
+ echo 'eval "$(mem init zsh)"' >> ~/.zshrc
276
+ source ~/.zshrc
277
+ ```
278
+
279
+ Bash and fish support coming in v1.5.
280
+
281
+ ## Data retention
282
+
283
+ mem never grows unbounded. Running `mem sync` automatically cleans old data:
284
+
285
+ | Data | Retention | Rationale |
286
+ |------|-----------|-----------|
287
+ | Commands | 90 days | High-volume, older ones rarely recalled |
288
+ | Sessions | 30 days | Useful for recent postmortems |
289
+ | Patterns | Forever | Small files, accumulated learning |
290
+
291
+ Override defaults: `mem sync --keep-commands 180 --keep-sessions 60`
292
+
293
+ ## Uninstall
294
+
295
+ ```bash
296
+ brew uninstall mem # or: pipx uninstall mem-cli
297
+ rm -rf ~/.mem # remove all captured data
298
+ ```
299
+
300
+ Remove the `eval "$(mem init zsh)"` line from your `~/.zshrc`.
301
+
302
+ ## Contributing
303
+
304
+ Contributions are welcome. Please read the [PHILOSOPHY.md](PHILOSOPHY.md) first to understand the principles that guide this project.
305
+
306
+ ```bash
307
+ git clone https://github.com/matinsaurralde/mem.git
308
+ cd mem
309
+ pip install -e ".[dev]"
310
+ pytest
311
+ ```
312
+
313
+ ## License
314
+
315
+ [MIT](LICENSE)
@@ -0,0 +1,12 @@
1
+ mem/__init__.py,sha256=ewEZCQcr5gM10n0oX2bYh0wXGchwtdehJzqYSSBggmg,69
2
+ mem/capture.py,sha256=rvO-x0axVLrZ3uozeIcoWRbTbZNy674mUCjntCpdLlY,6652
3
+ mem/cli.py,sha256=Po_QiDm46yXd1ntXSlVPt9KV89HCgBWhCii5GiXNukI,10591
4
+ mem/models.py,sha256=AXLCjOR02-MW50uw1XLjlQJhM5JWjG8XWol8kRs6gf4,2206
5
+ mem/patterns.py,sha256=NeGQGQDZomt1m93__-6U74jLgx3aRt0sJeILqZ1oErE,8203
6
+ mem/search.py,sha256=AItSepaTFG63fXuVIdjGpt8f4r1yC6rF_WzXmETGKpk,6005
7
+ mem/storage.py,sha256=4eXFtpRnraliUVh8nRmeg1aFtBFP4qfS-ooCB4heHFg,10750
8
+ cli_mem-0.1.0.dist-info/METADATA,sha256=Won8Vj6QbgmjeINboWJuI3Zy7Um9qWBJz7gA6tojA0o,9018
9
+ cli_mem-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
10
+ cli_mem-0.1.0.dist-info/entry_points.txt,sha256=KiuCWp4iI84ovCretwybDasM3_8g9YdK-nypnmeOIWw,36
11
+ cli_mem-0.1.0.dist-info/licenses/LICENSE,sha256=SeV7jm8yfSsZQjMq2fYTOK9FZQqR6chFVGhqjrxl9P8,1084
12
+ cli_mem-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ mem = mem.cli:cli
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Matias Federico Insaurralde
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.
mem/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """mem — your shell history, understood."""
2
+
3
+ __version__ = "0.1.0"
mem/capture.py ADDED
@@ -0,0 +1,198 @@
1
+ """
2
+ Shell history capture module.
3
+
4
+ Handles command capture from shell hooks and session tracking.
5
+ The capture pipeline: shell hook -> mem _capture -> this module -> storage.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ import subprocess
12
+ import time
13
+ import uuid
14
+
15
+ from mem.models import CapturedCommand, SessionState, WorkSession
16
+ from mem import storage
17
+
18
+
19
+ def get_git_repo(directory: str) -> str | None:
20
+ """Detect the current git repository's root path.
21
+
22
+ Runs `git rev-parse --show-toplevel` to find the repo root
23
+ and returns the full absolute path (e.g., /Users/me/projects/myapp).
24
+
25
+ Uses the full path — not the basename — so repos with the same
26
+ folder name under different parents stay isolated (e.g.,
27
+ /work/client-a/api and /work/client-b/api are distinct).
28
+
29
+ Returns None if the directory is not inside a git repository.
30
+
31
+ Why subprocess over gitpython: zero dependencies. git is always
32
+ available on macOS, and we only need one command.
33
+ """
34
+ try:
35
+ result = subprocess.run(
36
+ ["git", "-C", directory, "rev-parse", "--show-toplevel"],
37
+ capture_output=True,
38
+ text=True,
39
+ timeout=5,
40
+ )
41
+ if result.returncode == 0:
42
+ return result.stdout.strip()
43
+ return None
44
+ except (subprocess.TimeoutExpired, FileNotFoundError):
45
+ return None
46
+
47
+
48
+ def capture_command(raw: str, directory: str, exit_code: int, duration_ms: int) -> None:
49
+ """Capture a shell command with full context and persist it.
50
+
51
+ Called by the shell hook after every command execution.
52
+ Builds a CapturedCommand with the current timestamp and git repo,
53
+ then appends it to the appropriate JSONL file.
54
+ """
55
+ repo = get_git_repo(directory)
56
+ cmd = CapturedCommand(
57
+ command=raw,
58
+ ts=int(time.time()),
59
+ dir=directory,
60
+ repo=repo,
61
+ exit_code=exit_code,
62
+ duration_ms=duration_ms,
63
+ )
64
+ storage.append_command(cmd)
65
+
66
+ # Update session tracking
67
+ try:
68
+ tracker = SessionTracker()
69
+ tracker.update(cmd)
70
+ except Exception:
71
+ pass # Session tracking failure should never block capture
72
+
73
+
74
+ class SessionTracker:
75
+ """Tracks work sessions across shell commands.
76
+
77
+ A session is a coherent sequence of commands grouped by time
78
+ proximity and repository context. Session boundaries are detected
79
+ when:
80
+
81
+ 1. More than 300 seconds (5 minutes) of idle time between commands
82
+ 2. The user switches to a different git repository
83
+
84
+ Why 300 seconds: Five minutes is long enough that brief interruptions
85
+ (reading docs, bathroom breaks) don't split a session, but short
86
+ enough that genuine context switches are detected. This threshold
87
+ was chosen by observing that most developers maintain focus on a
88
+ single task for at least 5 minutes, and breaks longer than that
89
+ typically indicate a task switch.
90
+
91
+ State is persisted in ~/.mem/.session_state.json so sessions
92
+ survive shell restarts.
93
+ """
94
+
95
+ def __init__(self) -> None:
96
+ self._state_path = storage.MEM_DIR / ".session_state.json"
97
+
98
+ def _load_state(self) -> SessionState | None:
99
+ """Load the current session state from disk."""
100
+ if not self._state_path.exists():
101
+ return None
102
+ try:
103
+ data = json.loads(self._state_path.read_text(encoding="utf-8"))
104
+ return SessionState(**data)
105
+ except Exception:
106
+ return None
107
+
108
+ def _save_state(self, state: SessionState) -> None:
109
+ """Persist session state to disk."""
110
+ storage.ensure_dirs()
111
+ self._state_path.write_text(
112
+ state.model_dump_json(), encoding="utf-8"
113
+ )
114
+
115
+ def _clear_state(self) -> None:
116
+ """Remove session state file."""
117
+ if self._state_path.exists():
118
+ self._state_path.unlink()
119
+
120
+ def update(self, cmd: CapturedCommand) -> None:
121
+ """Process a new command and update session state.
122
+
123
+ Detects session boundaries and closes sessions when:
124
+ - More than 300 seconds have passed since the last command
125
+ - The git repo has changed
126
+ """
127
+ state = self._load_state()
128
+
129
+ if state is None:
130
+ # Start a new session
131
+ new_state = SessionState(
132
+ session_id=uuid.uuid4().hex,
133
+ last_command_ts=cmd.ts,
134
+ last_repo=cmd.repo,
135
+ commands=[cmd.command],
136
+ )
137
+ self._save_state(new_state)
138
+ return
139
+
140
+ idle_time = cmd.ts - state.last_command_ts
141
+ repo_changed = cmd.repo != state.last_repo
142
+
143
+ # Session boundary: >300s idle OR repo change
144
+ if idle_time > 300 or repo_changed:
145
+ self._close_session(state)
146
+ # Start new session
147
+ new_state = SessionState(
148
+ session_id=uuid.uuid4().hex,
149
+ last_command_ts=cmd.ts,
150
+ last_repo=cmd.repo,
151
+ commands=[cmd.command],
152
+ )
153
+ self._save_state(new_state)
154
+ else:
155
+ # Continue current session
156
+ state.commands.append(cmd.command)
157
+ state.last_command_ts = cmd.ts
158
+ state.last_repo = cmd.repo
159
+ self._save_state(state)
160
+
161
+ def _close_session(self, state: SessionState) -> None:
162
+ """Close and persist a completed session."""
163
+ if not state.commands:
164
+ return
165
+
166
+ # Generate summary — use first command as fallback when AI unavailable
167
+ summary = self._generate_summary(state.commands, state.last_repo)
168
+
169
+ session = WorkSession(
170
+ id=state.session_id,
171
+ summary=summary,
172
+ started_at=state.last_command_ts - (len(state.commands) * 10), # approximate
173
+ ended_at=state.last_command_ts,
174
+ dir="", # not tracked in state for simplicity
175
+ repo=state.last_repo,
176
+ commands=state.commands,
177
+ )
178
+ storage.append_session(session)
179
+
180
+ def _generate_summary(self, commands: list[str], repo: str | None) -> str:
181
+ """Generate a session summary.
182
+
183
+ Uses Apple FM SDK if available, otherwise falls back to
184
+ using the first command as the summary.
185
+ """
186
+ try:
187
+ import asyncio
188
+ from mem.patterns import generate_session_summary
189
+ result = asyncio.run(generate_session_summary(commands))
190
+ if result:
191
+ return result
192
+ except Exception:
193
+ pass
194
+
195
+ # Fallback: first command + count
196
+ if len(commands) == 1:
197
+ return commands[0]
198
+ return f"{commands[0]} (+{len(commands) - 1} more commands)"