cli-mem 0.4.0__tar.gz → 0.5.1__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 (105) hide show
  1. cli_mem-0.5.1/.github/workflows/ci.yml +86 -0
  2. cli_mem-0.5.1/.github/workflows/release.yml +151 -0
  3. {cli_mem-0.4.0 → cli_mem-0.5.1}/ARCHITECTURE.md +106 -16
  4. cli_mem-0.5.1/CHANGELOG.md +278 -0
  5. {cli_mem-0.4.0 → cli_mem-0.5.1}/PHILOSOPHY.md +55 -6
  6. cli_mem-0.5.1/PKG-INFO +781 -0
  7. cli_mem-0.5.1/README.md +751 -0
  8. cli_mem-0.5.1/command-groups/security/recon.json +23 -0
  9. cli_mem-0.5.1/docs/AUDIT-2026-09.md +849 -0
  10. cli_mem-0.5.1/docs/BACKLOG.md +17 -0
  11. cli_mem-0.5.1/docs/DEFENSE.md +414 -0
  12. cli_mem-0.5.1/docs/decisions/004-per-repo-jsonl.md +27 -0
  13. cli_mem-0.5.1/docs/decisions/005-derived-index-jsonl-source-of-truth.md +67 -0
  14. cli_mem-0.5.1/docs/decisions/006-app-intents-shortcuts-spotlight-deferred.md +41 -0
  15. cli_mem-0.5.1/docs/decisions/007-mcp-over-stdio-never-http.md +45 -0
  16. cli_mem-0.5.1/docs/decisions/008-stdlib-tui.md +53 -0
  17. cli_mem-0.5.1/docs/decisions/009-ranking-learns-from-selections.md +64 -0
  18. cli_mem-0.5.1/docs/decisions/010-keychain-for-variable-values.md +78 -0
  19. cli_mem-0.5.1/docs/decisions/011-concept-map-over-embeddings.md +61 -0
  20. cli_mem-0.5.1/docs/decisions/012-promote-mines-sequences-not-commands.md +96 -0
  21. {cli_mem-0.4.0 → cli_mem-0.5.1}/install.sh +21 -4
  22. cli_mem-0.5.1/pyproject.toml +99 -0
  23. {cli_mem-0.4.0 → cli_mem-0.5.1}/src/mem/__init__.py +1 -1
  24. cli_mem-0.5.1/src/mem/_entry.py +39 -0
  25. cli_mem-0.5.1/src/mem/_fsutil.py +157 -0
  26. {cli_mem-0.4.0 → cli_mem-0.5.1}/src/mem/capture.py +108 -25
  27. cli_mem-0.5.1/src/mem/cli.py +2134 -0
  28. cli_mem-0.5.1/src/mem/concepts.json +269 -0
  29. cli_mem-0.5.1/src/mem/concepts.py +338 -0
  30. cli_mem-0.5.1/src/mem/fix.py +819 -0
  31. {cli_mem-0.4.0 → cli_mem-0.5.1}/src/mem/groups.py +54 -26
  32. cli_mem-0.5.1/src/mem/history.py +577 -0
  33. cli_mem-0.5.1/src/mem/hooks/mem.bash +152 -0
  34. cli_mem-0.5.1/src/mem/hooks/mem.fish +61 -0
  35. cli_mem-0.5.1/src/mem/hooks/mem.zsh +80 -0
  36. cli_mem-0.5.1/src/mem/keychain.py +347 -0
  37. cli_mem-0.5.1/src/mem/mcp.py +828 -0
  38. cli_mem-0.5.1/src/mem/models.py +251 -0
  39. cli_mem-0.5.1/src/mem/patterns.py +522 -0
  40. cli_mem-0.5.1/src/mem/picks.py +229 -0
  41. cli_mem-0.5.1/src/mem/promote.py +1035 -0
  42. cli_mem-0.5.1/src/mem/ranking.py +246 -0
  43. cli_mem-0.5.1/src/mem/render.py +82 -0
  44. cli_mem-0.5.1/src/mem/search.py +356 -0
  45. cli_mem-0.5.1/src/mem/storage.py +1507 -0
  46. cli_mem-0.5.1/src/mem/tui.py +817 -0
  47. cli_mem-0.5.1/src/mem/variables.py +775 -0
  48. cli_mem-0.5.1/tests/conftest.py +326 -0
  49. cli_mem-0.5.1/tests/test_capture.py +658 -0
  50. cli_mem-0.5.1/tests/test_cli.py +1040 -0
  51. cli_mem-0.5.1/tests/test_concepts.py +593 -0
  52. cli_mem-0.5.1/tests/test_e2e.py +714 -0
  53. cli_mem-0.5.1/tests/test_fix.py +973 -0
  54. {cli_mem-0.4.0 → cli_mem-0.5.1}/tests/test_groups.py +203 -24
  55. cli_mem-0.5.1/tests/test_hooks_e2e.py +1162 -0
  56. cli_mem-0.5.1/tests/test_import.py +1160 -0
  57. cli_mem-0.5.1/tests/test_keychain.py +816 -0
  58. cli_mem-0.5.1/tests/test_mcp.py +2092 -0
  59. {cli_mem-0.4.0 → cli_mem-0.5.1}/tests/test_patterns.py +473 -29
  60. cli_mem-0.5.1/tests/test_picks.py +283 -0
  61. cli_mem-0.5.1/tests/test_promote.py +906 -0
  62. cli_mem-0.5.1/tests/test_search.py +555 -0
  63. cli_mem-0.5.1/tests/test_storage.py +938 -0
  64. cli_mem-0.5.1/tests/test_storage_destructive.py +1519 -0
  65. cli_mem-0.5.1/tests/test_tui.py +1088 -0
  66. cli_mem-0.5.1/tests/test_variables.py +958 -0
  67. cli_mem-0.4.0/.github/workflows/ci.yml +0 -29
  68. cli_mem-0.4.0/.github/workflows/release.yml +0 -91
  69. cli_mem-0.4.0/PKG-INFO +0 -410
  70. cli_mem-0.4.0/README.md +0 -381
  71. cli_mem-0.4.0/command-groups/security/recon.json +0 -35
  72. cli_mem-0.4.0/docs/decisions/004-per-repo-jsonl.md +0 -24
  73. cli_mem-0.4.0/hooks/mem.bash +0 -42
  74. cli_mem-0.4.0/hooks/mem.fish +0 -13
  75. cli_mem-0.4.0/hooks/mem.zsh +0 -28
  76. cli_mem-0.4.0/pyproject.toml +0 -47
  77. cli_mem-0.4.0/src/mem/cli.py +0 -1385
  78. cli_mem-0.4.0/src/mem/models.py +0 -137
  79. cli_mem-0.4.0/src/mem/patterns.py +0 -316
  80. cli_mem-0.4.0/src/mem/search.py +0 -173
  81. cli_mem-0.4.0/src/mem/storage.py +0 -419
  82. cli_mem-0.4.0/src/mem/variables.py +0 -480
  83. cli_mem-0.4.0/tests/conftest.py +0 -47
  84. cli_mem-0.4.0/tests/test_capture.py +0 -344
  85. cli_mem-0.4.0/tests/test_search.py +0 -110
  86. cli_mem-0.4.0/tests/test_storage.py +0 -121
  87. {cli_mem-0.4.0 → cli_mem-0.5.1}/.gitignore +0 -0
  88. {cli_mem-0.4.0 → cli_mem-0.5.1}/LICENSE +0 -0
  89. {cli_mem-0.4.0 → cli_mem-0.5.1}/assets/.gitkeep +0 -0
  90. {cli_mem-0.4.0 → cli_mem-0.5.1}/command-groups/databases/postgres-debug.json +0 -0
  91. {cli_mem-0.4.0 → cli_mem-0.5.1}/command-groups/databases/redis-ops.json +0 -0
  92. {cli_mem-0.4.0 → cli_mem-0.5.1}/command-groups/devops/docker-cleanup.json +0 -0
  93. {cli_mem-0.4.0 → cli_mem-0.5.1}/command-groups/devops/incident-response.json +0 -0
  94. {cli_mem-0.4.0 → cli_mem-0.5.1}/command-groups/devops/k8s-debug.json +0 -0
  95. {cli_mem-0.4.0 → cli_mem-0.5.1}/command-groups/git/cleanup.json +0 -0
  96. {cli_mem-0.4.0 → cli_mem-0.5.1}/command-groups/git/forensics.json +0 -0
  97. {cli_mem-0.4.0 → cli_mem-0.5.1}/command-groups/networking/dns-debug.json +0 -0
  98. {cli_mem-0.4.0 → cli_mem-0.5.1}/command-groups/networking/network-debug.json +0 -0
  99. {cli_mem-0.4.0 → cli_mem-0.5.1}/command-groups/performance/system-profile.json +0 -0
  100. {cli_mem-0.4.0 → cli_mem-0.5.1}/command-groups/security/headers-audit.json +0 -0
  101. {cli_mem-0.4.0 → cli_mem-0.5.1}/command-groups/security/ssl-audit.json +0 -0
  102. {cli_mem-0.4.0 → cli_mem-0.5.1}/docs/decisions/001-jsonl-over-sqlite.md +0 -0
  103. {cli_mem-0.4.0 → cli_mem-0.5.1}/docs/decisions/002-apple-fm-sdk-for-patterns.md +0 -0
  104. {cli_mem-0.4.0 → cli_mem-0.5.1}/docs/decisions/003-no-daemon.md +0 -0
  105. {cli_mem-0.4.0 → cli_mem-0.5.1}/src/mem/_generable.py +0 -0
@@ -0,0 +1,86 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [master]
6
+ # Deliberately unfiltered by base branch. With a `branches: [master]` filter,
7
+ # a pull request targeting anything else gets no CI at all — which silently
8
+ # excluded every stacked PR in this series, the exact review model in use.
9
+ pull_request:
10
+
11
+ concurrency:
12
+ group: ci-${{ github.ref }}
13
+ cancel-in-progress: true
14
+
15
+ jobs:
16
+ test:
17
+ runs-on: macos-latest
18
+ strategy:
19
+ fail-fast: false
20
+ matrix:
21
+ # Must cover every version pyproject declares support for. Claiming
22
+ # >=3.10 while only testing 3.11/3.12 is an untested promise.
23
+ python-version: ["3.10", "3.11", "3.12", "3.13"]
24
+ steps:
25
+ - uses: actions/checkout@v7
26
+
27
+ - uses: actions/setup-python@v7
28
+ with:
29
+ python-version: ${{ matrix.python-version }}
30
+
31
+ - name: Install
32
+ run: pip install -e ".[dev]"
33
+
34
+ - name: Lint
35
+ run: ruff check .
36
+
37
+ - name: Format check
38
+ run: ruff format --check .
39
+
40
+ - name: Test
41
+ run: pytest -v
42
+
43
+ build:
44
+ runs-on: macos-latest
45
+ steps:
46
+ - uses: actions/checkout@v7
47
+
48
+ - uses: actions/setup-python@v7
49
+ with:
50
+ python-version: "3.12"
51
+
52
+ # Catches packaging breakage at PR time instead of at tag time, when the
53
+ # release workflow is the only thing standing between a bad build and PyPI.
54
+ - name: Build distributions
55
+ run: |
56
+ pip install build twine
57
+ python -m build
58
+ twine check --strict dist/*
59
+
60
+ - name: Verify the installed console script is ours
61
+ run: |
62
+ python -m venv /tmp/verify
63
+ /tmp/verify/bin/pip install --quiet dist/*.whl
64
+ OUT="$(/tmp/verify/bin/mem --version)"
65
+ echo "$OUT"
66
+ case "$OUT" in
67
+ "mem, version "*) ;;
68
+ *) echo "::error::unexpected --version output: $OUT"; exit 1 ;;
69
+ esac
70
+
71
+ # The hooks are the entry point for every piece of data mem stores, and
72
+ # they only reach users through the wheel. They used to be looked up at a
73
+ # repo-relative path that no installed wheel has, so `mem init` silently
74
+ # served a stale second copy inlined in cli.py — and the test suite,
75
+ # which always ran from a source checkout, never noticed. Only an
76
+ # install-then-run check can catch that class of bug.
77
+ - name: Verify the wheel ships the shell hooks
78
+ run: |
79
+ for shell in zsh bash fish; do
80
+ OUT="$(/tmp/verify/bin/mem init "$shell")"
81
+ if ! diff <(printf '%s\n' "$OUT") "src/mem/hooks/mem.$shell"; then
82
+ echo "::error::mem init $shell does not match src/mem/hooks/mem.$shell"
83
+ exit 1
84
+ fi
85
+ done
86
+ echo "all three hooks served from the installed wheel"
@@ -0,0 +1,151 @@
1
+ name: Release
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - "v*"
7
+
8
+ permissions:
9
+ contents: write
10
+
11
+ # A tag pushed twice (or force-moved) must not race two publishes against PyPI.
12
+ concurrency:
13
+ group: release-${{ github.ref }}
14
+ cancel-in-progress: false
15
+
16
+ jobs:
17
+ release:
18
+ runs-on: macos-latest
19
+ steps:
20
+ - uses: actions/checkout@v7
21
+
22
+ - uses: actions/setup-python@v7
23
+ with:
24
+ python-version: "3.12"
25
+
26
+ - name: Verify version matches tag
27
+ run: |
28
+ TAG="${{ github.ref_name }}"
29
+ TAG_VERSION="${TAG#v}"
30
+ PKG_VERSION=$(python -c "import tomllib; print(tomllib.load(open('pyproject.toml','rb'))['project']['version'])")
31
+ INIT_VERSION=$(python -c "exec(open('src/mem/__init__.py').read()); print(__version__)")
32
+ echo "Tag: $TAG_VERSION | pyproject.toml: $PKG_VERSION | __init__.py: $INIT_VERSION"
33
+ if [ "$TAG_VERSION" != "$PKG_VERSION" ] || [ "$TAG_VERSION" != "$INIT_VERSION" ]; then
34
+ echo "::error::Version mismatch! Tag=$TAG_VERSION pyproject.toml=$PKG_VERSION __init__.py=$INIT_VERSION"
35
+ echo "Fix: update both pyproject.toml and src/mem/__init__.py, commit, then re-tag."
36
+ exit 1
37
+ fi
38
+
39
+ - name: Build
40
+ run: |
41
+ pip install build twine
42
+ python -m build
43
+ twine check --strict dist/*
44
+
45
+ # Everything that can fail must fail BEFORE the upload step: a release
46
+ # published to PyPI cannot be replaced, only yanked.
47
+ - name: Smoke-test the built wheel
48
+ run: |
49
+ set -euo pipefail
50
+ python -m venv /tmp/smoke
51
+ /tmp/smoke/bin/pip install --quiet dist/*.whl
52
+ OUT="$(/tmp/smoke/bin/mem --version)"
53
+ echo "$OUT"
54
+ EXPECTED="mem, version ${GITHUB_REF_NAME#v}"
55
+ if [ "$OUT" != "$EXPECTED" ]; then
56
+ echo "::error::wheel reports '$OUT', expected '$EXPECTED'"
57
+ exit 1
58
+ fi
59
+ # The hooks reach users only through the wheel, and they used to be
60
+ # looked up at a repo-relative path no installed wheel has — so
61
+ # `mem init` silently served a stale inlined copy and the test suite,
62
+ # always running from a checkout, never noticed. CI checks this on
63
+ # every PR; checking it again here costs two seconds and covers a tag
64
+ # pushed at a commit that somehow skipped it.
65
+ # `$(...)` strips every trailing newline and printf puts exactly one
66
+ # back, which normalises the extra newline click.echo appends.
67
+ for shell in zsh bash fish; do
68
+ OUT="$(/tmp/smoke/bin/mem init "$shell")"
69
+ diff <(printf '%s\n' "$OUT") "src/mem/hooks/mem.$shell" \
70
+ || { echo "::error::mem init $shell does not match the source hook"; exit 1; }
71
+ done
72
+
73
+ # The console script dispatches through mem._entry now; a broken
74
+ # dispatch breaks every command, and --version alone would not show it.
75
+ /tmp/smoke/bin/mem tui --help > /dev/null
76
+ echo "wheel smoke test passed"
77
+
78
+ # TODO: migrate to PyPI Trusted Publishing (pypa/gh-action-pypi-publish
79
+ # with `id-token: write`) to retire the long-lived PYPI_TOKEN secret.
80
+ # Requires configuring the publisher on PyPI first, so it is a deliberate
81
+ # follow-up rather than a silent swap.
82
+ - name: Publish to PyPI
83
+ env:
84
+ TWINE_USERNAME: __token__
85
+ TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }}
86
+ run: twine upload --skip-existing dist/*
87
+
88
+ - name: Create GitHub Release
89
+ env:
90
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
91
+ run: |
92
+ set -euo pipefail
93
+ TAG="${{ github.ref_name }}"
94
+ if gh release view "$TAG" >/dev/null 2>&1; then
95
+ echo "Release $TAG already exists — uploading artifacts only."
96
+ gh release upload "$TAG" dist/* --clobber
97
+ else
98
+ gh release create "$TAG" --title "$TAG" --generate-notes dist/*
99
+ fi
100
+
101
+ - name: Update Homebrew tap
102
+ env:
103
+ TAP_TOKEN: ${{ secrets.TAP_TOKEN }}
104
+ run: |
105
+ set -euo pipefail
106
+
107
+ TAG="${{ github.ref_name }}"
108
+ TARBALL_URL="https://github.com/${{ github.repository }}/archive/refs/tags/${TAG}.tar.gz"
109
+
110
+ # -f so an HTTP error is an error: without it curl pipes the error page
111
+ # into shasum and the tap gets a checksum of "404: Not Found".
112
+ SHA=$(curl -fsSL --retry 5 --retry-delay 2 "$TARBALL_URL" | shasum -a 256 | cut -d' ' -f1)
113
+ if [ ${#SHA} -ne 64 ]; then
114
+ echo "::error::refusing to publish a malformed sha256: '$SHA'"
115
+ exit 1
116
+ fi
117
+ echo "Tag: $TAG"
118
+ echo "SHA256: $SHA"
119
+
120
+ # Clone the tap repo
121
+ git clone "https://x-access-token:${TAP_TOKEN}@github.com/matinsaurralde/homebrew-tap.git" /tmp/tap
122
+ cd /tmp/tap
123
+
124
+ # Update only the release url and sha256 (lines 7-8), not dependency resources.
125
+ # Uses awk to replace only the FIRST url/sha256 pair matching the release pattern.
126
+ awk -v tag_url="$TARBALL_URL" -v sha="$SHA" '
127
+ /url.*github.com\/matinsaurralde\/mem\/archive/ {
128
+ print " url \"" tag_url "\""
129
+ found_url = 1
130
+ next
131
+ }
132
+ found_url && /sha256/ {
133
+ print " sha256 \"" sha "\""
134
+ found_url = 0
135
+ next
136
+ }
137
+ { print }
138
+ ' Formula/mem.rb > Formula/mem.rb.tmp
139
+ mv Formula/mem.rb.tmp Formula/mem.rb
140
+
141
+ if ! grep -q "$SHA" Formula/mem.rb; then
142
+ echo "::error::awk did not write the new sha256 into Formula/mem.rb"
143
+ exit 1
144
+ fi
145
+
146
+ git config user.name "github-actions[bot]"
147
+ git config user.email "github-actions[bot]@users.noreply.github.com"
148
+ git add Formula/mem.rb
149
+ git diff --cached --quiet && echo "No changes" && exit 0
150
+ git commit -m "mem ${TAG}"
151
+ git push
@@ -21,7 +21,7 @@ Technical overview of how mem works under the hood.
21
21
  │ mem list mem session mem stats │
22
22
  │ mem forget mem export mem import │
23
23
  │ mem group * mem saved * mem vars * │
24
- │ mem init <shell> │
24
+ │ mem init <shell> mem agent * mem mcp │
25
25
  │ mem _capture (hidden) mem _sync (hidden) │
26
26
  └───┬────────────┬──────────┬──────────┬──────────────────┘
27
27
  │ │ │ │
@@ -55,14 +55,16 @@ Technical overview of how mem works under the hood.
55
55
  │ ~/.mem/ (filesystem) │
56
56
  │ │
57
57
  │ repos/ sessions/ patterns/ │
58
- │ myapp.jsonl 2026-03-05.jsonl kubectl.json │
59
- │ infra.jsonl 2026-03-06.jsonl docker.json │
58
+ │ myapp-3f9a1c07.jsonl 2026-03-05.jsonl kubectl.json │
59
+ │ infra-b21d4e88.jsonl 2026-03-06.jsonl docker.json │
60
60
  │ _global.jsonl git.json │
61
61
  │ │
62
62
  │ groups/ │
63
63
  │ repos/ │
64
64
  │ myapp.json vars.json .sync_counter │
65
65
  │ _global.json .session_state.json │
66
+ │ │
67
+ │ agent.json agent-audit.jsonl │
66
68
  └─────────────────────────────────────────────────────────┘
67
69
  ```
68
70
 
@@ -90,7 +92,7 @@ capture.capture_command()
90
92
  │ builds CapturedCommand with all metadata
91
93
  ▼
92
94
  storage.append_command()
93
- │ appends one JSON line to ~/.mem/repos/<repo>.jsonl
95
+ │ appends one JSON line to ~/.mem/repos/<repo-slug>-<hash>.jsonl
94
96
  ▼
95
97
  capture.SessionTracker.update()
96
98
  │ checks idle time and repo change
@@ -115,20 +117,26 @@ cli.cli() dispatches to search
115
117
  ▼
116
118
  search.search("kubectl", current_repo="infra")
117
119
  │
118
- ├── read ~/.mem/repos/infra.jsonl (current repo first)
120
+ ├── read ~/.mem/repos/infra-<hash>.jsonl (current repo first)
119
121
  ├── read ~/.mem/repos/_global.jsonl
120
122
  ├── read other repo files
121
123
  │
122
124
  ▼
123
- Filter: substring match "kubectl" in command text
125
+ Prefilter: needle test on the raw line's command span (skips json.loads)
124
126
  │
125
127
  ▼
126
- Score each match:
127
- score = (frequency × 0.4) + (recency × 0.4) + (context × 0.2)
128
+ Filter: every query term must appear in the command text
129
+ │ (if nothing matches, expand through the concept map and retry)
130
+ ▼
131
+ Score each match (mem/ranking.py — stdlib only, shared with the finder):
132
+ score = (picks × 0.40) + (frequency × 0.21) + (recency × 0.21)
133
+ + (prefix × 0.09) + (context × 0.09)
128
134
  │
129
- ├── frequency: count of identical commands
130
- ├── recency: exp(-days × ln2/7), half-life 7 days
131
- └── context: 1.0 same repo, 0.5 same prefix, 0.0 other
135
+ ├── picks: 1 - 2^-w over the finder's decayed selections, 21d half-life
136
+ ├── frequency: log1p(n)/log1p(50), capped at 1
137
+ ├── recency: exp(-days × ln2/7), half-life 7 days
138
+ ├── prefix: 1.0 if the command starts with the query
139
+ └── context: 1.0 same repo, 0.5 sibling, 0.0 other
132
140
  │
133
141
  ▼
134
142
  Deduplicate by command string (keep highest score)
@@ -232,7 +240,7 @@ Parse inline VAR=VALUE arguments
232
240
  Resolve all variables upfront (before any execution):
233
241
  1. Inline arguments ← highest priority
234
242
  2. Shell environment (os.environ)
235
- 3. Persistent store (~/.mem/vars.json)
243
+ 3. Persistent store (macOS Keychain, indexed by ~/.mem/vars.json)
236
244
  4. Default value (from --var at save time)
237
245
  5. Interactive prompt ← last resort
238
246
  ▼
@@ -245,7 +253,7 @@ Execute commands with substituted values
245
253
 
246
254
  ## JSONL Schemas
247
255
 
248
- ### Command Entry (`repos/<repo>.jsonl`)
256
+ ### Command Entry (`repos/<repo-slug>-<hash>.jsonl`)
249
257
 
250
258
  ```json
251
259
  {
@@ -322,17 +330,91 @@ Execute commands with substituted values
322
330
  }
323
331
  ```
324
332
 
325
- ### Variable Store (`vars.json`)
333
+ ### Variable Store (`vars.json` + macOS Keychain)
334
+
335
+ An index, not a vault. The values are Keychain items under the service
336
+ `mem-cli-vars` with the variable name as the account
337
+ ([ADR-010](docs/decisions/010-keychain-for-variable-values.md)); `value` is
338
+ `null` for every entry mem writes, and a non-null one is a pre-ADR-010
339
+ plaintext value awaiting migration.
326
340
 
327
341
  ```json
328
342
  {
329
343
  "vars": {
330
- "API_TOKEN": { "value": "sk-abc123...", "last_used": 1709600000 },
331
- "DB_HOST": { "value": "staging.db.internal", "last_used": 1709500000 }
344
+ "API_TOKEN": { "value": null, "last_used": 1709600000, "backend": "keychain" },
345
+ "DB_HOST": { "value": null, "last_used": 1709500000, "backend": "keychain" }
332
346
  }
333
347
  }
334
348
  ```
335
349
 
350
+ ### Agent Access Flag (`agent.json`)
351
+
352
+ ```json
353
+ {
354
+ "enabled": false,
355
+ "updated_at": 1709600000
356
+ }
357
+ ```
358
+
359
+ Absent, unreadable or unparseable all mean `enabled: false`. A privacy switch
360
+ must fail closed — the one thing it may never do is grant access because it
361
+ could not read the answer.
362
+
363
+ ### Agent Audit Entry (`agent-audit.jsonl`)
364
+
365
+ ```json
366
+ {
367
+ "ts": 1709600000,
368
+ "tool": "search_history",
369
+ "arguments": { "query": "kubectl prod" },
370
+ "results": 4,
371
+ "ok": true,
372
+ "error": null
373
+ }
374
+ ```
375
+
376
+ `arguments` is redacted before it is written: an agent can put a secret in a
377
+ query string, and an audit log that records secrets is a second copy of the
378
+ thing the audit exists to protect. The file is append-only and is never
379
+ rotated; `mem forget` is the only thing that removes lines from it.
380
+
381
+ ## MCP Server (stdio)
382
+
383
+ ```
384
+ Claude Code / Claude Desktop
385
+ │ spawns `mem mcp`, keeps the pipe open
386
+ ▼
387
+ stdin ──→ newline-delimited JSON-RPC 2.0 ──→ mem.mcp.serve()
388
+ │
389
+ ┌───────────────┼───────────────┐
390
+ ▼ ▼ ▼
391
+ agent.json search / groups audit append
392
+ (opt-in gate) (read-only) (agent-audit)
393
+ │
394
+ ▼
395
+ redact_secrets() ← single choke point
396
+ │
397
+ stdout ←── one JSON-RPC frame per line ←──────────┘
398
+ ```
399
+
400
+ Design points:
401
+
402
+ - **Stdio only.** No socket, no listener, no localhost port. The HTTP/SSE
403
+ transport is not implemented — "only on loopback" is still a listener.
404
+ - **No MCP SDK dependency.** The official Python SDK pulls uvicorn, starlette
405
+ and httpx into a project whose first principle is that it never imports a
406
+ networking stack. The protocol is hand-rolled in `src/mem/mcp.py`.
407
+ - **stdout is the wire.** `serve()` rebinds `sys.stdout` to stderr for its
408
+ whole lifetime, so a stray `print`, a Rich console write, or a
409
+ corrupted-JSONL warning becomes a diagnostic rather than a corrupt frame.
410
+ - **Four read-only tools**, and nothing that executes a command.
411
+ - **The access flag is read per request**, so `mem agent disable` takes effect
412
+ without restarting the client.
413
+
414
+ `backend` is derived from the absence of `value` on every read, never trusted
415
+ from the file — so an entry cannot claim to be encrypted while holding a
416
+ secret.
417
+
336
418
  ## Session Boundary Detection
337
419
 
338
420
  A new session begins when either condition is met:
@@ -347,3 +429,11 @@ See `docs/decisions/` for detailed ADRs:
347
429
  - [002: Apple FM SDK for Patterns](docs/decisions/002-apple-fm-sdk-for-patterns.md)
348
430
  - [003: No Daemon, Background Subprocess Instead](docs/decisions/003-no-daemon.md)
349
431
  - [004: Per-Repository JSONL Files](docs/decisions/004-per-repo-jsonl.md)
432
+ - [005: A Derived Index, With the JSONL as the Source of Truth](docs/decisions/005-derived-index-jsonl-source-of-truth.md)
433
+ - [006: App Intents, Shortcuts and Spotlight — Evaluated and Deferred](docs/decisions/006-app-intents-shortcuts-spotlight-deferred.md)
434
+ - [007: MCP Over Stdio, Never HTTP](docs/decisions/007-mcp-over-stdio-never-http.md)
435
+ - [008: The Interactive TUI Is Built on the Standard Library](docs/decisions/008-stdlib-tui.md)
436
+ - [009: Ranking Learns From What You Select](docs/decisions/009-ranking-learns-from-selections.md)
437
+ - [010: The Keychain for Variable Values](docs/decisions/010-keychain-for-variable-values.md)
438
+ - [011: A Concept Map, Not Embeddings](docs/decisions/011-concept-map-over-embeddings.md)
439
+ - [012: Promote Mines Sequences, and Ranking Is What Makes It Trustworthy](docs/decisions/012-promote-mines-sequences-not-commands.md)