memgit 0.9.1__tar.gz → 0.11.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.
- {memgit-0.9.1 → memgit-0.11.0}/PKG-INFO +92 -2
- {memgit-0.9.1 → memgit-0.11.0}/README.md +90 -0
- {memgit-0.9.1 → memgit-0.11.0}/memgit/__init__.py +1 -1
- {memgit-0.9.1 → memgit-0.11.0}/memgit/cli.py +234 -1
- {memgit-0.9.1 → memgit-0.11.0}/memgit/cloud/commands.py +42 -3
- memgit-0.11.0/memgit/env.py +88 -0
- memgit-0.11.0/memgit/license.py +270 -0
- {memgit-0.9.1 → memgit-0.11.0}/memgit/mcp_server.py +113 -3
- {memgit-0.9.1 → memgit-0.11.0}/memgit/repo.py +250 -17
- memgit-0.11.0/memgit/savings.py +288 -0
- {memgit-0.9.1 → memgit-0.11.0}/memgit/store.py +50 -0
- {memgit-0.9.1 → memgit-0.11.0}/memgit.egg-info/PKG-INFO +92 -2
- {memgit-0.9.1 → memgit-0.11.0}/memgit.egg-info/SOURCES.txt +13 -1
- {memgit-0.9.1 → memgit-0.11.0}/memgit.egg-info/requires.txt +1 -1
- {memgit-0.9.1 → memgit-0.11.0}/pyproject.toml +23 -2
- memgit-0.11.0/tests/test_cloud_guard.py +66 -0
- memgit-0.11.0/tests/test_dependency_floor.py +78 -0
- memgit-0.11.0/tests/test_env_inventory.py +86 -0
- memgit-0.11.0/tests/test_git_args.py +71 -0
- memgit-0.11.0/tests/test_license.py +208 -0
- memgit-0.11.0/tests/test_lock_exclusion.py +87 -0
- memgit-0.11.0/tests/test_savings.py +149 -0
- memgit-0.11.0/tests/test_server_lifecycle.py +113 -0
- {memgit-0.9.1 → memgit-0.11.0}/tests/test_store_repo.py +184 -0
- memgit-0.11.0/tests/test_version_stamps.py +83 -0
- {memgit-0.9.1 → memgit-0.11.0}/LICENSE +0 -0
- {memgit-0.9.1 → memgit-0.11.0}/memgit/backup.py +0 -0
- {memgit-0.9.1 → memgit-0.11.0}/memgit/cloud/__init__.py +0 -0
- {memgit-0.9.1 → memgit-0.11.0}/memgit/cloud/client.py +0 -0
- {memgit-0.9.1 → memgit-0.11.0}/memgit/cloud/crypto.py +0 -0
- {memgit-0.9.1 → memgit-0.11.0}/memgit/cloud/state.py +0 -0
- {memgit-0.9.1 → memgit-0.11.0}/memgit/cloud/sync.py +0 -0
- {memgit-0.9.1 → memgit-0.11.0}/memgit/delivery.py +0 -0
- {memgit-0.9.1 → memgit-0.11.0}/memgit/evaluate.py +0 -0
- {memgit-0.9.1 → memgit-0.11.0}/memgit/gitdigest.py +0 -0
- {memgit-0.9.1 → memgit-0.11.0}/memgit/graph.py +0 -0
- {memgit-0.9.1 → memgit-0.11.0}/memgit/hooks.py +0 -0
- {memgit-0.9.1 → memgit-0.11.0}/memgit/http_server.py +0 -0
- {memgit-0.9.1 → memgit-0.11.0}/memgit/importer.py +0 -0
- {memgit-0.9.1 → memgit-0.11.0}/memgit/links.py +0 -0
- {memgit-0.9.1 → memgit-0.11.0}/memgit/metrics.py +0 -0
- {memgit-0.9.1 → memgit-0.11.0}/memgit/models.py +0 -0
- {memgit-0.9.1 → memgit-0.11.0}/memgit/project.py +0 -0
- {memgit-0.9.1 → memgit-0.11.0}/memgit/sanitize.py +0 -0
- {memgit-0.9.1 → memgit-0.11.0}/memgit/scorer.py +0 -0
- {memgit-0.9.1 → memgit-0.11.0}/memgit/tokens.py +0 -0
- {memgit-0.9.1 → memgit-0.11.0}/memgit/toon.py +0 -0
- {memgit-0.9.1 → memgit-0.11.0}/memgit/usage.py +0 -0
- {memgit-0.9.1 → memgit-0.11.0}/memgit.egg-info/dependency_links.txt +0 -0
- {memgit-0.9.1 → memgit-0.11.0}/memgit.egg-info/entry_points.txt +0 -0
- {memgit-0.9.1 → memgit-0.11.0}/memgit.egg-info/top_level.txt +0 -0
- {memgit-0.9.1 → memgit-0.11.0}/setup.cfg +0 -0
- {memgit-0.9.1 → memgit-0.11.0}/tests/test_accrue.py +0 -0
- {memgit-0.9.1 → memgit-0.11.0}/tests/test_advanced.py +0 -0
- {memgit-0.9.1 → memgit-0.11.0}/tests/test_aliases.py +0 -0
- {memgit-0.9.1 → memgit-0.11.0}/tests/test_backup.py +0 -0
- {memgit-0.9.1 → memgit-0.11.0}/tests/test_core.py +0 -0
- {memgit-0.9.1 → memgit-0.11.0}/tests/test_delivery.py +0 -0
- {memgit-0.9.1 → memgit-0.11.0}/tests/test_setup.py +0 -0
- {memgit-0.9.1 → memgit-0.11.0}/tests/test_toon.py +0 -0
- {memgit-0.9.1 → memgit-0.11.0}/tests/test_v020.py +0 -0
- {memgit-0.9.1 → memgit-0.11.0}/tests/test_v030.py +0 -0
- {memgit-0.9.1 → memgit-0.11.0}/tests/test_v040.py +0 -0
- {memgit-0.9.1 → memgit-0.11.0}/tests/test_v060.py +0 -0
- {memgit-0.9.1 → memgit-0.11.0}/tests/test_v070.py +0 -0
- {memgit-0.9.1 → memgit-0.11.0}/tests/test_v080.py +0 -0
- {memgit-0.9.1 → memgit-0.11.0}/tests/test_v081.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: memgit
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.11.0
|
|
4
4
|
Summary: Git for AI memory — version-controlled context persistence across Claude, GPT, Gemini, Cursor, Windsurf, and more
|
|
5
5
|
License: MIT
|
|
6
6
|
Project-URL: Homepage, https://memgit.dev
|
|
@@ -21,7 +21,7 @@ Description-Content-Type: text/markdown
|
|
|
21
21
|
License-File: LICENSE
|
|
22
22
|
Requires-Dist: click>=8.0
|
|
23
23
|
Requires-Dist: rich>=13.0
|
|
24
|
-
Requires-Dist: mcp
|
|
24
|
+
Requires-Dist: mcp<2,>=1.28.1
|
|
25
25
|
Provides-Extra: dev
|
|
26
26
|
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
27
27
|
Requires-Dist: pytest-anyio>=0.0.0; extra == "dev"
|
|
@@ -298,6 +298,33 @@ Since 0.8.0 it also **starts itself**: a project's first guide is created automa
|
|
|
298
298
|
|
|
299
299
|
---
|
|
300
300
|
|
|
301
|
+
## memgit Pro — one memory across every machine
|
|
302
|
+
|
|
303
|
+
The local engine, the store, the MCP server and every command below are MIT and never
|
|
304
|
+
gated. **Pro** is the hosted layer for people who work on more than one machine or with a
|
|
305
|
+
team: end-to-end-encrypted sync where the server stores ciphertext only, unlocked by one
|
|
306
|
+
licence key.
|
|
307
|
+
|
|
308
|
+
```bash
|
|
309
|
+
memgit pro activate <key> # validate the key with Polar and store it (mode 0600)
|
|
310
|
+
memgit pro status # Free / Pro, last verification, expiry
|
|
311
|
+
memgit pro deactivate # remove the key from this machine
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
- Buy a key at [memgit.dev/#pricing](https://memgit.dev/#pricing) — $12/month or $99/year,
|
|
315
|
+
billed by Polar (merchant of record, taxes handled). The key arrives by email and in your
|
|
316
|
+
Polar purchases page.
|
|
317
|
+
- The only bytes that leave your machine are the key and memgit's public organisation id,
|
|
318
|
+
sent to Polar's validation endpoint. **No memory content is ever sent.**
|
|
319
|
+
- Headless hosts: set `MEMGIT_LICENSE_KEY=<key>` in the MCP server's environment; it is
|
|
320
|
+
validated with the same cache and never written to disk.
|
|
321
|
+
- Fail-open: a key that verified in the last 14 days keeps working offline; a rejected key
|
|
322
|
+
drops to Free with a clear message. A lapsed plan never locks your data — reads, pulls and
|
|
323
|
+
exports keep working.
|
|
324
|
+
- If this store is logged in to memgit cloud, `pro activate` also upgrades that account.
|
|
325
|
+
|
|
326
|
+
---
|
|
327
|
+
|
|
301
328
|
## Backups that actually happen
|
|
302
329
|
|
|
303
330
|
memgit's premise is that the AI is the operator — but backup used to require a human to remember `memgit git init --remote <url>` and keep pushing. On this project's own store that meant 1,734 memories on one disk with no copy anywhere, five weeks in. A maintenance task that needs a human command is a task that will not happen.
|
|
@@ -414,6 +441,10 @@ memgit setup gemini-cli
|
|
|
414
441
|
memgit setup hooks # Claude Code hooks: resume at start, per-prompt recall,
|
|
415
442
|
# capture guard + auto-sync at stop (--no-recall / --no-guard)
|
|
416
443
|
|
|
444
|
+
# Pro (licence key)
|
|
445
|
+
memgit pro activate <key> # Polar-issued key; unlocks hosted E2E sync
|
|
446
|
+
memgit pro status / deactivate
|
|
447
|
+
|
|
417
448
|
# Server
|
|
418
449
|
memgit serve # MCP stdio (Claude Code, Cursor, Windsurf, Cline)
|
|
419
450
|
memgit serve --http # HTTP REST (ChatGPT Custom Actions, Gemini)
|
|
@@ -444,6 +475,65 @@ memgit thread list / switch / create
|
|
|
444
475
|
|
|
445
476
|
---
|
|
446
477
|
|
|
478
|
+
## What memgit reads from your environment
|
|
479
|
+
|
|
480
|
+
memgit runs inside your agent, so the honest answer to "what does this process
|
|
481
|
+
read from my environment?" should be checkable rather than a promise. The table
|
|
482
|
+
below is generated from [`memgit/env.py`](memgit/env.py), and
|
|
483
|
+
`tests/test_env_inventory.py` walks the package on every test run and fails if
|
|
484
|
+
the code reads a name this list does not carry — or carries a name nothing
|
|
485
|
+
reads. The list cannot drift away from the code.
|
|
486
|
+
|
|
487
|
+
memgit reads these and nothing else. It writes none of them. None of them turn
|
|
488
|
+
network access on: the cloud endpoints are overrides for a sync you have already
|
|
489
|
+
opted into with `memgit cloud login`, and with no licence and no cloud login,
|
|
490
|
+
memgit talks to nothing.
|
|
491
|
+
|
|
492
|
+
| | Variable | What memgit does with it |
|
|
493
|
+
|---|---|---|
|
|
494
|
+
| Store and state | `MEMGIT_STORE` | Absolute path to the memory store. When set it is the ONLY candidate — an explicit store never silently falls back to another one. |
|
|
495
|
+
| | `MEMGIT_HOME` | Base directory for memgit's per-person state, currently the licence file (default ~/.memgit, mode 0600). |
|
|
496
|
+
| | `MEMGIT_PROJECT` | Forces the project label instead of detecting it from the working directory. |
|
|
497
|
+
| Attribution | `MEMGIT_AUTHOR` | Explicit author stamped on checkpoints, for multi-agent jobs where several writers share one machine account. Wins over MEMGIT_CLIENT. |
|
|
498
|
+
| | `MEMGIT_CLIENT` | The host that launched this process (claude-code, cursor, ...), stamped into checkpoints so a memory says where it came from. |
|
|
499
|
+
| | `USER` | Fallback author name when neither MEMGIT_AUTHOR nor MEMGIT_CLIENT is set. |
|
|
500
|
+
| | `USERNAME` | Windows fallback for USER. |
|
|
501
|
+
| Licensing (memgit Pro) | `MEMGIT_LICENSE_KEY` | memgit Pro licence key. Never printed in full — `memgit pro status` shows the last four characters. |
|
|
502
|
+
| | `MEMGIT_POLAR_API` | Overrides the licence-validation API base URL (testing). |
|
|
503
|
+
| | `MEMGIT_POLAR_ORG_ID` | Overrides the Polar organisation the licence is checked against. |
|
|
504
|
+
| Cloud sync | `MEMGIT_CLOUD_API` | Overrides the memgit cloud API base URL. |
|
|
505
|
+
| | `MEMGIT_CLOUD_APP` | Overrides the memgit cloud web app base URL used in printed links. |
|
|
506
|
+
| | `MEMGIT_CLOUD_NO_CACHE` | Set to 1/true/yes to keep decrypted keys out of credentials.json; the passphrase is then re-prompted per command. |
|
|
507
|
+
| Tuning | `MEMGIT_LOCK_TIMEOUT` | Seconds a writer waits for the store lock before giving up (default 10). |
|
|
508
|
+
| | `MEMGIT_IDLE_EVICT_SECONDS` | Idle seconds before the MCP server drops its in-memory caches while staying connected (default 900). |
|
|
509
|
+
| | `MEMGIT_HOUSEKEEPING_INTERVAL` | Seconds between the MCP server's housekeeping passes. |
|
|
510
|
+
| Set by other software | `CLAUDE_PROJECT_DIR` | Set by Claude Code for hook processes; used as one input to project detection when the working directory is not the project root. |
|
|
511
|
+
| | `PYTEST_CURRENT_TEST` | Set by pytest. Its presence suppresses automatic backups, so a test run never writes into a real backup location. |
|
|
512
|
+
|
|
513
|
+
### How the project label is decided
|
|
514
|
+
|
|
515
|
+
Memories are project-scoped, so a wrong label files a memory where you will not
|
|
516
|
+
find it again. The label comes from the first of these that yields one, and
|
|
517
|
+
there is exactly one detection path shared by the MCP server, the CLI and the
|
|
518
|
+
hooks — so a label derived at save time and one derived at recall time cannot
|
|
519
|
+
disagree:
|
|
520
|
+
|
|
521
|
+
1. `MEMGIT_PROJECT` — a forced label, taken verbatim
|
|
522
|
+
2. the `cwd` a host reports in its hook payload (the real workspace, even when
|
|
523
|
+
the process working directory is elsewhere)
|
|
524
|
+
3. `CLAUDE_PROJECT_DIR`
|
|
525
|
+
4. the process working directory
|
|
526
|
+
|
|
527
|
+
When none of them yields a label — running from `$HOME`, for instance — a write
|
|
528
|
+
is **quarantined** under `_unknown` rather than filed globally, and
|
|
529
|
+
`memgit doctor --relabel` moves it once you say where it belongs. Folding a
|
|
530
|
+
free-text label onto an existing project (`log-report` → `Downloads-log-report`)
|
|
531
|
+
happens only when the input is a trailing segment of exactly one known label and
|
|
532
|
+
that label already holds memories; anything ambiguous is left alone, and every
|
|
533
|
+
fold is reported rather than done silently.
|
|
534
|
+
|
|
535
|
+
---
|
|
536
|
+
|
|
447
537
|
## TOON format — compact, readable, diffable
|
|
448
538
|
|
|
449
539
|
Standard markdown memory file:
|
|
@@ -264,6 +264,33 @@ Since 0.8.0 it also **starts itself**: a project's first guide is created automa
|
|
|
264
264
|
|
|
265
265
|
---
|
|
266
266
|
|
|
267
|
+
## memgit Pro — one memory across every machine
|
|
268
|
+
|
|
269
|
+
The local engine, the store, the MCP server and every command below are MIT and never
|
|
270
|
+
gated. **Pro** is the hosted layer for people who work on more than one machine or with a
|
|
271
|
+
team: end-to-end-encrypted sync where the server stores ciphertext only, unlocked by one
|
|
272
|
+
licence key.
|
|
273
|
+
|
|
274
|
+
```bash
|
|
275
|
+
memgit pro activate <key> # validate the key with Polar and store it (mode 0600)
|
|
276
|
+
memgit pro status # Free / Pro, last verification, expiry
|
|
277
|
+
memgit pro deactivate # remove the key from this machine
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
- Buy a key at [memgit.dev/#pricing](https://memgit.dev/#pricing) — $12/month or $99/year,
|
|
281
|
+
billed by Polar (merchant of record, taxes handled). The key arrives by email and in your
|
|
282
|
+
Polar purchases page.
|
|
283
|
+
- The only bytes that leave your machine are the key and memgit's public organisation id,
|
|
284
|
+
sent to Polar's validation endpoint. **No memory content is ever sent.**
|
|
285
|
+
- Headless hosts: set `MEMGIT_LICENSE_KEY=<key>` in the MCP server's environment; it is
|
|
286
|
+
validated with the same cache and never written to disk.
|
|
287
|
+
- Fail-open: a key that verified in the last 14 days keeps working offline; a rejected key
|
|
288
|
+
drops to Free with a clear message. A lapsed plan never locks your data — reads, pulls and
|
|
289
|
+
exports keep working.
|
|
290
|
+
- If this store is logged in to memgit cloud, `pro activate` also upgrades that account.
|
|
291
|
+
|
|
292
|
+
---
|
|
293
|
+
|
|
267
294
|
## Backups that actually happen
|
|
268
295
|
|
|
269
296
|
memgit's premise is that the AI is the operator — but backup used to require a human to remember `memgit git init --remote <url>` and keep pushing. On this project's own store that meant 1,734 memories on one disk with no copy anywhere, five weeks in. A maintenance task that needs a human command is a task that will not happen.
|
|
@@ -380,6 +407,10 @@ memgit setup gemini-cli
|
|
|
380
407
|
memgit setup hooks # Claude Code hooks: resume at start, per-prompt recall,
|
|
381
408
|
# capture guard + auto-sync at stop (--no-recall / --no-guard)
|
|
382
409
|
|
|
410
|
+
# Pro (licence key)
|
|
411
|
+
memgit pro activate <key> # Polar-issued key; unlocks hosted E2E sync
|
|
412
|
+
memgit pro status / deactivate
|
|
413
|
+
|
|
383
414
|
# Server
|
|
384
415
|
memgit serve # MCP stdio (Claude Code, Cursor, Windsurf, Cline)
|
|
385
416
|
memgit serve --http # HTTP REST (ChatGPT Custom Actions, Gemini)
|
|
@@ -410,6 +441,65 @@ memgit thread list / switch / create
|
|
|
410
441
|
|
|
411
442
|
---
|
|
412
443
|
|
|
444
|
+
## What memgit reads from your environment
|
|
445
|
+
|
|
446
|
+
memgit runs inside your agent, so the honest answer to "what does this process
|
|
447
|
+
read from my environment?" should be checkable rather than a promise. The table
|
|
448
|
+
below is generated from [`memgit/env.py`](memgit/env.py), and
|
|
449
|
+
`tests/test_env_inventory.py` walks the package on every test run and fails if
|
|
450
|
+
the code reads a name this list does not carry — or carries a name nothing
|
|
451
|
+
reads. The list cannot drift away from the code.
|
|
452
|
+
|
|
453
|
+
memgit reads these and nothing else. It writes none of them. None of them turn
|
|
454
|
+
network access on: the cloud endpoints are overrides for a sync you have already
|
|
455
|
+
opted into with `memgit cloud login`, and with no licence and no cloud login,
|
|
456
|
+
memgit talks to nothing.
|
|
457
|
+
|
|
458
|
+
| | Variable | What memgit does with it |
|
|
459
|
+
|---|---|---|
|
|
460
|
+
| Store and state | `MEMGIT_STORE` | Absolute path to the memory store. When set it is the ONLY candidate — an explicit store never silently falls back to another one. |
|
|
461
|
+
| | `MEMGIT_HOME` | Base directory for memgit's per-person state, currently the licence file (default ~/.memgit, mode 0600). |
|
|
462
|
+
| | `MEMGIT_PROJECT` | Forces the project label instead of detecting it from the working directory. |
|
|
463
|
+
| Attribution | `MEMGIT_AUTHOR` | Explicit author stamped on checkpoints, for multi-agent jobs where several writers share one machine account. Wins over MEMGIT_CLIENT. |
|
|
464
|
+
| | `MEMGIT_CLIENT` | The host that launched this process (claude-code, cursor, ...), stamped into checkpoints so a memory says where it came from. |
|
|
465
|
+
| | `USER` | Fallback author name when neither MEMGIT_AUTHOR nor MEMGIT_CLIENT is set. |
|
|
466
|
+
| | `USERNAME` | Windows fallback for USER. |
|
|
467
|
+
| Licensing (memgit Pro) | `MEMGIT_LICENSE_KEY` | memgit Pro licence key. Never printed in full — `memgit pro status` shows the last four characters. |
|
|
468
|
+
| | `MEMGIT_POLAR_API` | Overrides the licence-validation API base URL (testing). |
|
|
469
|
+
| | `MEMGIT_POLAR_ORG_ID` | Overrides the Polar organisation the licence is checked against. |
|
|
470
|
+
| Cloud sync | `MEMGIT_CLOUD_API` | Overrides the memgit cloud API base URL. |
|
|
471
|
+
| | `MEMGIT_CLOUD_APP` | Overrides the memgit cloud web app base URL used in printed links. |
|
|
472
|
+
| | `MEMGIT_CLOUD_NO_CACHE` | Set to 1/true/yes to keep decrypted keys out of credentials.json; the passphrase is then re-prompted per command. |
|
|
473
|
+
| Tuning | `MEMGIT_LOCK_TIMEOUT` | Seconds a writer waits for the store lock before giving up (default 10). |
|
|
474
|
+
| | `MEMGIT_IDLE_EVICT_SECONDS` | Idle seconds before the MCP server drops its in-memory caches while staying connected (default 900). |
|
|
475
|
+
| | `MEMGIT_HOUSEKEEPING_INTERVAL` | Seconds between the MCP server's housekeeping passes. |
|
|
476
|
+
| Set by other software | `CLAUDE_PROJECT_DIR` | Set by Claude Code for hook processes; used as one input to project detection when the working directory is not the project root. |
|
|
477
|
+
| | `PYTEST_CURRENT_TEST` | Set by pytest. Its presence suppresses automatic backups, so a test run never writes into a real backup location. |
|
|
478
|
+
|
|
479
|
+
### How the project label is decided
|
|
480
|
+
|
|
481
|
+
Memories are project-scoped, so a wrong label files a memory where you will not
|
|
482
|
+
find it again. The label comes from the first of these that yields one, and
|
|
483
|
+
there is exactly one detection path shared by the MCP server, the CLI and the
|
|
484
|
+
hooks — so a label derived at save time and one derived at recall time cannot
|
|
485
|
+
disagree:
|
|
486
|
+
|
|
487
|
+
1. `MEMGIT_PROJECT` — a forced label, taken verbatim
|
|
488
|
+
2. the `cwd` a host reports in its hook payload (the real workspace, even when
|
|
489
|
+
the process working directory is elsewhere)
|
|
490
|
+
3. `CLAUDE_PROJECT_DIR`
|
|
491
|
+
4. the process working directory
|
|
492
|
+
|
|
493
|
+
When none of them yields a label — running from `$HOME`, for instance — a write
|
|
494
|
+
is **quarantined** under `_unknown` rather than filed globally, and
|
|
495
|
+
`memgit doctor --relabel` moves it once you say where it belongs. Folding a
|
|
496
|
+
free-text label onto an existing project (`log-report` → `Downloads-log-report`)
|
|
497
|
+
happens only when the input is a trailing segment of exactly one known label and
|
|
498
|
+
that label already holds memories; anything ambiguous is left alone, and every
|
|
499
|
+
fold is reported rather than done silently.
|
|
500
|
+
|
|
501
|
+
---
|
|
502
|
+
|
|
413
503
|
## TOON format — compact, readable, diffable
|
|
414
504
|
|
|
415
505
|
Standard markdown memory file:
|
|
@@ -118,11 +118,18 @@ def init(directory):
|
|
|
118
118
|
|
|
119
119
|
# Step-by-step flow: find existing memories automatically and offer the
|
|
120
120
|
# import, instead of making the user discover the right path themselves.
|
|
121
|
+
# Narrow on purpose: an unreadable ~/.claude tree (OSError) or a memory file
|
|
122
|
+
# that will not parse (ValueError, which UnicodeDecodeError subclasses) is a
|
|
123
|
+
# reason to skip the offer, not to abort `init`. Anything else is a bug in
|
|
124
|
+
# the importer and must reach the user rather than being swallowed here.
|
|
121
125
|
try:
|
|
122
126
|
from .importer import from_claude_code
|
|
123
127
|
found = from_claude_code()
|
|
124
|
-
except
|
|
128
|
+
except (OSError, ValueError) as exc:
|
|
125
129
|
found = []
|
|
130
|
+
console.print(f'[dim]Could not scan ~/.claude/projects for existing '
|
|
131
|
+
f'memories ({exc.__class__.__name__}: {exc}) — skipping '
|
|
132
|
+
f'the import offer.[/dim]')
|
|
126
133
|
if found:
|
|
127
134
|
projects = {m.project for m in found if m.project}
|
|
128
135
|
console.print(f'\nFound [bold]{len(found)}[/bold] existing Claude Code memories '
|
|
@@ -1978,6 +1985,105 @@ def metrics(fmt_json, reset):
|
|
|
1978
1985
|
console.print()
|
|
1979
1986
|
|
|
1980
1987
|
|
|
1988
|
+
@cli.command()
|
|
1989
|
+
@click.option('--json', 'fmt_json', is_flag=True, help='Machine-readable output')
|
|
1990
|
+
@click.option('--match', default=None, type=float,
|
|
1991
|
+
help='Share of a memory\'s terms a file must carry to count as '
|
|
1992
|
+
'containing the fact (default 0.5)')
|
|
1993
|
+
@click.option('--reads', default=None, type=int,
|
|
1994
|
+
help='How many files the counterfactual reader opens (default 3)')
|
|
1995
|
+
@click.option('--all-projects', is_flag=True,
|
|
1996
|
+
help='Every project, not just the one you are standing in')
|
|
1997
|
+
@click.option('--root', type=click.Path(exists=True, file_okay=False),
|
|
1998
|
+
help='Directory the counterfactual reader would search (default: cwd)')
|
|
1999
|
+
def savings(fmt_json, match, reads, all_projects, root):
|
|
2000
|
+
"""What memory cost, against what finding the same fact would have cost.
|
|
2001
|
+
|
|
2002
|
+
The counterfactual is stated, not implied: without memgit an agent needing
|
|
2003
|
+
a fact would grep this project and read the best-matching files. That path
|
|
2004
|
+
is mechanical, so it can be measured. Memories no file contains are counted
|
|
2005
|
+
separately and NEVER priced — without memgit those facts are not found at
|
|
2006
|
+
any number of tokens, and turning that into a savings figure would be the
|
|
2007
|
+
fabrication `memgit metrics` was right to refuse.
|
|
2008
|
+
|
|
2009
|
+
The estimate is conservative on purpose: it counts the files only, not the
|
|
2010
|
+
grep output, not the turns spent choosing what to open, and not the reads
|
|
2011
|
+
that miss.
|
|
2012
|
+
"""
|
|
2013
|
+
from . import savings as sv
|
|
2014
|
+
from .project import detect_project
|
|
2015
|
+
repo = _require_repo()
|
|
2016
|
+
search_root = Path(root) if root else Path.cwd()
|
|
2017
|
+
label = None if all_projects else detect_project(search_root)
|
|
2018
|
+
r = sv.measure(repo, search_root, project=label,
|
|
2019
|
+
match=match if match is not None else sv.DEFAULT_MATCH,
|
|
2020
|
+
reads=reads if reads is not None else sv.DEFAULT_READS)
|
|
2021
|
+
|
|
2022
|
+
if fmt_json:
|
|
2023
|
+
import json as _j
|
|
2024
|
+
print(_j.dumps(r, indent=2))
|
|
2025
|
+
return
|
|
2026
|
+
|
|
2027
|
+
from rich.rule import Rule
|
|
2028
|
+
from rich import box
|
|
2029
|
+
|
|
2030
|
+
console.print()
|
|
2031
|
+
console.print(Rule('[bold cyan]memgit savings[/bold cyan] '
|
|
2032
|
+
'[dim]measured against a stated counterfactual[/dim]'))
|
|
2033
|
+
console.print()
|
|
2034
|
+
console.print(f'[dim]Project:[/dim] {r["project"] or "all"} '
|
|
2035
|
+
f'[dim]Files a reader could open:[/dim] {r["files_indexed"]:,} '
|
|
2036
|
+
f'[dim]Reads assumed:[/dim] {r["parameters"]["reads"]} '
|
|
2037
|
+
f'[dim]Match:[/dim] {r["parameters"]["match"]:.0%}')
|
|
2038
|
+
console.print()
|
|
2039
|
+
|
|
2040
|
+
if r['memories_considered'] == 0:
|
|
2041
|
+
console.print('[dim]No recalled memories for this project yet — nothing '
|
|
2042
|
+
'to compare. The usage ledger fills as memories get '
|
|
2043
|
+
'surfaced.[/dim]')
|
|
2044
|
+
console.print()
|
|
2045
|
+
return
|
|
2046
|
+
|
|
2047
|
+
t = Table(box=box.SIMPLE, header_style='bold')
|
|
2048
|
+
t.add_column('', min_width=34)
|
|
2049
|
+
t.add_column('Tokens', justify='right')
|
|
2050
|
+
t.add_row('With memgit (what was injected)', f'{r["memgit_tokens"]:,}')
|
|
2051
|
+
t.add_row('Without memgit (files that would be read)',
|
|
2052
|
+
f'{r["without_memgit_tokens"]:,}')
|
|
2053
|
+
t.add_row('[bold]Saved[/bold]', f'[bold]{r["saved_tokens"]:,}[/bold]')
|
|
2054
|
+
console.print(t)
|
|
2055
|
+
if r['ratio']:
|
|
2056
|
+
console.print(f' Reading costs [bold]{r["ratio"]:.0f}x[/bold] what recall '
|
|
2057
|
+
f'costs, over {r["recalls_counted"]:,} recorded recalls of '
|
|
2058
|
+
f'{r["recoverable_from_files"]:,} memories.')
|
|
2059
|
+
console.print()
|
|
2060
|
+
|
|
2061
|
+
if r['unrecoverable_from_files']:
|
|
2062
|
+
console.print(
|
|
2063
|
+
f'[bold yellow]{r["unrecoverable_from_files"]:,} of '
|
|
2064
|
+
f'{r["memories_considered"]:,} recalled memories are in NO file in '
|
|
2065
|
+
f'this project.[/bold yellow]')
|
|
2066
|
+
console.print(' [dim]Not priced, deliberately. Without memgit these are '
|
|
2067
|
+
'not found more expensively, they are not found. That is '
|
|
2068
|
+
'the claim worth making, and it is a count, not a token '
|
|
2069
|
+
'figure.[/dim]')
|
|
2070
|
+
console.print()
|
|
2071
|
+
|
|
2072
|
+
if r['top_savers']:
|
|
2073
|
+
t2 = Table(box=box.SIMPLE, header_style='bold',
|
|
2074
|
+
title='Where the saving comes from', title_justify='left')
|
|
2075
|
+
t2.add_column('Memory', min_width=30)
|
|
2076
|
+
t2.add_column('Recalls', justify='right')
|
|
2077
|
+
t2.add_column('Recall tok', justify='right')
|
|
2078
|
+
t2.add_column('Read tok', justify='right')
|
|
2079
|
+
for row in r['top_savers'][:8]:
|
|
2080
|
+
t2.add_row(row['slug'][:48], f'{row["hits"]:,}',
|
|
2081
|
+
f'{row["memgit_tokens_per_recall"]:,}',
|
|
2082
|
+
f'{row["without_tokens_per_recall"]:,}')
|
|
2083
|
+
console.print(t2)
|
|
2084
|
+
console.print()
|
|
2085
|
+
|
|
2086
|
+
|
|
1981
2087
|
@cli.group(invoke_without_command=True)
|
|
1982
2088
|
@click.pass_context
|
|
1983
2089
|
def backup(ctx):
|
|
@@ -2587,6 +2693,13 @@ def git_init(remote):
|
|
|
2587
2693
|
|
|
2588
2694
|
Then teammates run `memgit git pull` to get your memories.
|
|
2589
2695
|
"""
|
|
2696
|
+
if remote:
|
|
2697
|
+
from .repo import validate_git_remote
|
|
2698
|
+
bad = validate_git_remote(remote)
|
|
2699
|
+
if bad:
|
|
2700
|
+
err.print(f'[red]{bad}[/red]')
|
|
2701
|
+
raise SystemExit(1)
|
|
2702
|
+
|
|
2590
2703
|
repo = _require_repo()
|
|
2591
2704
|
ok = repo.git_init()
|
|
2592
2705
|
if not ok:
|
|
@@ -3354,6 +3467,126 @@ if __name__ == '__main__':
|
|
|
3354
3467
|
cli()
|
|
3355
3468
|
|
|
3356
3469
|
|
|
3470
|
+
# ── pro (licence-key entitlement via Polar; no extra, no telemetry) ──────────
|
|
3471
|
+
|
|
3472
|
+
@cli.group(invoke_without_command=True)
|
|
3473
|
+
@click.pass_context
|
|
3474
|
+
def pro(ctx):
|
|
3475
|
+
"""memgit Pro — activate, inspect or remove a licence key.
|
|
3476
|
+
|
|
3477
|
+
Pro unlocks the hosted sync layer (memgit cloud) and future paid features.
|
|
3478
|
+
The free CLI, store and MCP server are MIT and never gated. The only bytes
|
|
3479
|
+
that leave this machine are the key and memgit's public organisation id,
|
|
3480
|
+
sent to Polar's validation endpoint; no memory content is ever sent.
|
|
3481
|
+
"""
|
|
3482
|
+
if ctx.invoked_subcommand is None:
|
|
3483
|
+
ctx.invoke(pro_status)
|
|
3484
|
+
|
|
3485
|
+
|
|
3486
|
+
@pro.command('activate')
|
|
3487
|
+
@click.argument('key')
|
|
3488
|
+
@click.option('--no-cloud', is_flag=True, help='Do not also attach the key to a logged-in memgit cloud account')
|
|
3489
|
+
def pro_activate(key, no_cloud):
|
|
3490
|
+
"""Store KEY and validate it with Polar. Buy one at memgit.dev/#pricing."""
|
|
3491
|
+
from . import license as lic_mod
|
|
3492
|
+
from datetime import datetime, timezone
|
|
3493
|
+
key = key.strip()
|
|
3494
|
+
if len(key) < 8:
|
|
3495
|
+
console.print('[red]✗[/red] that does not look like a licence key')
|
|
3496
|
+
sys.exit(2)
|
|
3497
|
+
lic = lic_mod.License(key=key)
|
|
3498
|
+
now = datetime.now(timezone.utc)
|
|
3499
|
+
lic = lic_mod.refresh(lic, now=now, force=True)
|
|
3500
|
+
if lic.status == 'granted':
|
|
3501
|
+
lic_mod.write(lic)
|
|
3502
|
+
console.print(f'[green]✓[/green] memgit Pro activated for key {lic.masked}'
|
|
3503
|
+
+ (f' ({lic.customer_email})' if lic.customer_email else ''))
|
|
3504
|
+
if lic.expires_at:
|
|
3505
|
+
console.print(f' [dim]expires {lic.expires_at}[/dim]')
|
|
3506
|
+
elif lic.status in ('revoked', 'disabled', 'invalid'):
|
|
3507
|
+
console.print(f'[red]✗[/red] key {lic.masked} was rejected: {lic.error or lic.status}')
|
|
3508
|
+
console.print(' [dim]Keys are issued by Polar on purchase and shown in your purchases page.[/dim]')
|
|
3509
|
+
sys.exit(1)
|
|
3510
|
+
else: # unknown — offline or provider down: keep the key, verify on next run
|
|
3511
|
+
lic.status = 'none'
|
|
3512
|
+
lic_mod.write(lic)
|
|
3513
|
+
console.print(f'[yellow]![/yellow] could not reach the licence service ({lic.error}); '
|
|
3514
|
+
f'key {lic.masked} saved and will be verified on the next run')
|
|
3515
|
+
if not no_cloud and lic.status == 'granted':
|
|
3516
|
+
_pro_attach_cloud(key)
|
|
3517
|
+
|
|
3518
|
+
|
|
3519
|
+
def _pro_attach_cloud(key: str) -> None:
|
|
3520
|
+
"""Best-effort: if this store is logged in to memgit cloud, tell the API about
|
|
3521
|
+
the key so the hosted side unlocks too. Silent when not logged in."""
|
|
3522
|
+
try:
|
|
3523
|
+
from .cloud.state import CloudState
|
|
3524
|
+
from .repo import Repository
|
|
3525
|
+
repo = Repository.find()
|
|
3526
|
+
if repo is None:
|
|
3527
|
+
return
|
|
3528
|
+
cstate = CloudState(repo.path)
|
|
3529
|
+
creds = cstate.creds
|
|
3530
|
+
if not creds.get('access'):
|
|
3531
|
+
return
|
|
3532
|
+
from .cloud.client import ApiClient, DEFAULT_API_URL
|
|
3533
|
+
import os as _os
|
|
3534
|
+
api_url = _os.environ.get('MEMGIT_CLOUD_API') or creds.get('api_url') or DEFAULT_API_URL
|
|
3535
|
+
api = ApiClient(api_url, cstate)
|
|
3536
|
+
api._check(api._request('POST', '/v1/billing/polar/activate', json={'license_key': key}))
|
|
3537
|
+
console.print('[green]✓[/green] memgit cloud account upgraded to Pro')
|
|
3538
|
+
except Exception as e: # never let the hosted side break local activation
|
|
3539
|
+
console.print(f'[dim]cloud: not attached ({str(e)[:80]}) — run `memgit cloud login` then re-activate[/dim]')
|
|
3540
|
+
|
|
3541
|
+
|
|
3542
|
+
@pro.command('status')
|
|
3543
|
+
@click.option('--json', 'fmt_json', is_flag=True, help='JSON output')
|
|
3544
|
+
@click.option('--offline', is_flag=True, help='Answer from the cache only; never touch the network')
|
|
3545
|
+
def pro_status(fmt_json, offline):
|
|
3546
|
+
"""Show whether this machine is entitled to memgit Pro."""
|
|
3547
|
+
from . import license as lic_mod
|
|
3548
|
+
from datetime import datetime, timezone
|
|
3549
|
+
now = datetime.now(timezone.utc)
|
|
3550
|
+
lic = lic_mod.read()
|
|
3551
|
+
if lic.key and not offline and lic.needs_recheck(now):
|
|
3552
|
+
lic = lic_mod.refresh(lic, now=now)
|
|
3553
|
+
if lic.source == 'file':
|
|
3554
|
+
try:
|
|
3555
|
+
lic_mod.write(lic)
|
|
3556
|
+
except OSError:
|
|
3557
|
+
pass
|
|
3558
|
+
ent = lic.is_entitled(now)
|
|
3559
|
+
if fmt_json:
|
|
3560
|
+
import json as _j
|
|
3561
|
+
print(_j.dumps({'entitled': ent, 'key': lic.masked, 'status': lic.status,
|
|
3562
|
+
'validated_at': lic.validated_at, 'expires_at': lic.expires_at,
|
|
3563
|
+
'source': lic.source, 'error': lic.error}))
|
|
3564
|
+
return
|
|
3565
|
+
if not lic.key:
|
|
3566
|
+
console.print('[bold]memgit Free[/bold] — no licence key. '
|
|
3567
|
+
f'Pro (hosted E2E sync across machines and teams): {lic_mod.PRO_URL}')
|
|
3568
|
+
return
|
|
3569
|
+
tier = '[green]memgit Pro[/green]' if ent else '[yellow]memgit Free (key not entitled)[/yellow]'
|
|
3570
|
+
console.print(f'{tier} key {lic.masked} status {lic.status}'
|
|
3571
|
+
f'{" via $MEMGIT_LICENSE_KEY" if lic.source != "file" else ""}')
|
|
3572
|
+
if lic.validated_at:
|
|
3573
|
+
console.print(f' [dim]last verified {lic.validated_at}[/dim]')
|
|
3574
|
+
if lic.expires_at:
|
|
3575
|
+
console.print(f' [dim]expires {lic.expires_at}[/dim]')
|
|
3576
|
+
if lic.error:
|
|
3577
|
+
console.print(f' [yellow]{lic.error}[/yellow]')
|
|
3578
|
+
|
|
3579
|
+
|
|
3580
|
+
@pro.command('deactivate')
|
|
3581
|
+
def pro_deactivate():
|
|
3582
|
+
"""Remove the stored licence key from this machine."""
|
|
3583
|
+
from . import license as lic_mod
|
|
3584
|
+
if lic_mod.clear():
|
|
3585
|
+
console.print('[green]✓[/green] licence key removed; memgit is back on the Free tier')
|
|
3586
|
+
else:
|
|
3587
|
+
console.print('[dim]no licence key was stored[/dim]')
|
|
3588
|
+
|
|
3589
|
+
|
|
3357
3590
|
# ── cloud (E2E-encrypted team sync — optional extra) ──────────────────────────
|
|
3358
3591
|
|
|
3359
3592
|
try:
|
|
@@ -136,7 +136,12 @@ def cloud():
|
|
|
136
136
|
@cloud.command()
|
|
137
137
|
@click.option('--email', prompt=True)
|
|
138
138
|
def signup(email):
|
|
139
|
-
"""Create a memgit cloud account (
|
|
139
|
+
"""Create a memgit cloud account (free trial; the server sets the length).
|
|
140
|
+
|
|
141
|
+
The trial length is stamped by the server at signup and printed below, so this
|
|
142
|
+
help text does not hardcode it. It said "30-day" while the deployed server granted
|
|
143
|
+
14, which is a promise broken on day 15 for every reader of --help.
|
|
144
|
+
"""
|
|
140
145
|
_require_extra()
|
|
141
146
|
from . import crypto
|
|
142
147
|
from nacl import utils
|
|
@@ -345,13 +350,41 @@ def _report(res, direction):
|
|
|
345
350
|
console.print(f'[green]{icons.get(res.action, "·")}[/green] {direction}: {res.action}{detail}{head}')
|
|
346
351
|
|
|
347
352
|
|
|
353
|
+
def _guard_uncommitted(repo, thread: str, allow: bool) -> None:
|
|
354
|
+
"""Refuse to transmit while staged memories are not in a checkpoint.
|
|
355
|
+
|
|
356
|
+
Sync ships CHECKPOINTS. `memgit add` stages a memory but does not create
|
|
357
|
+
one, so pushing straight after `add` uploads nothing of the user's — and
|
|
358
|
+
said "created (2 objects up)" while doing it, with the receiving machine
|
|
359
|
+
reporting a clean, empty, fsck-OK store (measured 2026-09-06). Nothing in
|
|
360
|
+
that sequence errored, which is why it reads as "sync is broken" rather
|
|
361
|
+
than "you have not committed". An error naming the fix is the whole point;
|
|
362
|
+
`--allow-uncommitted` is there for anyone who means it.
|
|
363
|
+
"""
|
|
364
|
+
if allow:
|
|
365
|
+
return
|
|
366
|
+
pending = repo.uncommitted_slugs(thread)
|
|
367
|
+
if not pending:
|
|
368
|
+
return
|
|
369
|
+
shown = ', '.join(pending[:5]) + (f' and {len(pending) - 5} more' if len(pending) > 5 else '')
|
|
370
|
+
console.print(
|
|
371
|
+
f'[red]✗[/red] {len(pending)} memor{"y is" if len(pending) == 1 else "ies are"} '
|
|
372
|
+
f'not committed, and sync only ships committed memory: [bold]{shown}[/bold]')
|
|
373
|
+
console.print(' [dim]commit them first: memgit commit -m "what changed"[/dim]')
|
|
374
|
+
console.print(' [dim]or push the committed state anyway: --allow-uncommitted[/dim]')
|
|
375
|
+
sys.exit(1)
|
|
376
|
+
|
|
377
|
+
|
|
348
378
|
@cloud.command()
|
|
349
379
|
@click.option('--thread', default=None, help='thread to push (default: current)')
|
|
350
|
-
|
|
380
|
+
@click.option('--allow-uncommitted', is_flag=True,
|
|
381
|
+
help='Push committed state even though staged memories would not be sent')
|
|
382
|
+
def push(thread, allow_uncommitted):
|
|
351
383
|
"""Encrypt and upload local checkpoints, then advance the remote ref."""
|
|
352
384
|
repo, cstate, api = _ctx()
|
|
353
385
|
engine = _linked(repo, cstate, api)
|
|
354
386
|
th = thread or repo.current_thread()
|
|
387
|
+
_guard_uncommitted(repo, th, allow_uncommitted)
|
|
355
388
|
res = _run(lambda: engine.push(th), 'push')
|
|
356
389
|
_report(res, f'push {th}')
|
|
357
390
|
|
|
@@ -369,11 +402,17 @@ def pull(thread):
|
|
|
369
402
|
|
|
370
403
|
@cloud.command('sync')
|
|
371
404
|
@click.option('--thread', default=None, help='thread to sync (default: current)')
|
|
372
|
-
|
|
405
|
+
@click.option('--allow-uncommitted', is_flag=True,
|
|
406
|
+
help='Sync committed state even though staged memories would not be sent')
|
|
407
|
+
def cloud_sync(thread, allow_uncommitted):
|
|
373
408
|
"""Pull then push — the everyday command."""
|
|
374
409
|
repo, cstate, api = _ctx()
|
|
375
410
|
engine = _linked(repo, cstate, api)
|
|
376
411
|
th = thread or repo.current_thread()
|
|
412
|
+
# Guarded before the pull as well as the push: sync is the command users
|
|
413
|
+
# reach for, and finding out after a merge that half of it was a no-op is
|
|
414
|
+
# worse than being told up front which memories are not committed yet.
|
|
415
|
+
_guard_uncommitted(repo, th, allow_uncommitted)
|
|
377
416
|
pulled, pushed = _run(lambda: engine.sync(th), 'sync')
|
|
378
417
|
_report(pulled, f'pull {th}')
|
|
379
418
|
_report(pushed, f'push {th}')
|