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.
- cli_mem-0.5.1/.github/workflows/ci.yml +86 -0
- cli_mem-0.5.1/.github/workflows/release.yml +151 -0
- {cli_mem-0.4.0 → cli_mem-0.5.1}/ARCHITECTURE.md +106 -16
- cli_mem-0.5.1/CHANGELOG.md +278 -0
- {cli_mem-0.4.0 → cli_mem-0.5.1}/PHILOSOPHY.md +55 -6
- cli_mem-0.5.1/PKG-INFO +781 -0
- cli_mem-0.5.1/README.md +751 -0
- cli_mem-0.5.1/command-groups/security/recon.json +23 -0
- cli_mem-0.5.1/docs/AUDIT-2026-09.md +849 -0
- cli_mem-0.5.1/docs/BACKLOG.md +17 -0
- cli_mem-0.5.1/docs/DEFENSE.md +414 -0
- cli_mem-0.5.1/docs/decisions/004-per-repo-jsonl.md +27 -0
- cli_mem-0.5.1/docs/decisions/005-derived-index-jsonl-source-of-truth.md +67 -0
- cli_mem-0.5.1/docs/decisions/006-app-intents-shortcuts-spotlight-deferred.md +41 -0
- cli_mem-0.5.1/docs/decisions/007-mcp-over-stdio-never-http.md +45 -0
- cli_mem-0.5.1/docs/decisions/008-stdlib-tui.md +53 -0
- cli_mem-0.5.1/docs/decisions/009-ranking-learns-from-selections.md +64 -0
- cli_mem-0.5.1/docs/decisions/010-keychain-for-variable-values.md +78 -0
- cli_mem-0.5.1/docs/decisions/011-concept-map-over-embeddings.md +61 -0
- cli_mem-0.5.1/docs/decisions/012-promote-mines-sequences-not-commands.md +96 -0
- {cli_mem-0.4.0 → cli_mem-0.5.1}/install.sh +21 -4
- cli_mem-0.5.1/pyproject.toml +99 -0
- {cli_mem-0.4.0 → cli_mem-0.5.1}/src/mem/__init__.py +1 -1
- cli_mem-0.5.1/src/mem/_entry.py +39 -0
- cli_mem-0.5.1/src/mem/_fsutil.py +157 -0
- {cli_mem-0.4.0 → cli_mem-0.5.1}/src/mem/capture.py +108 -25
- cli_mem-0.5.1/src/mem/cli.py +2134 -0
- cli_mem-0.5.1/src/mem/concepts.json +269 -0
- cli_mem-0.5.1/src/mem/concepts.py +338 -0
- cli_mem-0.5.1/src/mem/fix.py +819 -0
- {cli_mem-0.4.0 → cli_mem-0.5.1}/src/mem/groups.py +54 -26
- cli_mem-0.5.1/src/mem/history.py +577 -0
- cli_mem-0.5.1/src/mem/hooks/mem.bash +152 -0
- cli_mem-0.5.1/src/mem/hooks/mem.fish +61 -0
- cli_mem-0.5.1/src/mem/hooks/mem.zsh +80 -0
- cli_mem-0.5.1/src/mem/keychain.py +347 -0
- cli_mem-0.5.1/src/mem/mcp.py +828 -0
- cli_mem-0.5.1/src/mem/models.py +251 -0
- cli_mem-0.5.1/src/mem/patterns.py +522 -0
- cli_mem-0.5.1/src/mem/picks.py +229 -0
- cli_mem-0.5.1/src/mem/promote.py +1035 -0
- cli_mem-0.5.1/src/mem/ranking.py +246 -0
- cli_mem-0.5.1/src/mem/render.py +82 -0
- cli_mem-0.5.1/src/mem/search.py +356 -0
- cli_mem-0.5.1/src/mem/storage.py +1507 -0
- cli_mem-0.5.1/src/mem/tui.py +817 -0
- cli_mem-0.5.1/src/mem/variables.py +775 -0
- cli_mem-0.5.1/tests/conftest.py +326 -0
- cli_mem-0.5.1/tests/test_capture.py +658 -0
- cli_mem-0.5.1/tests/test_cli.py +1040 -0
- cli_mem-0.5.1/tests/test_concepts.py +593 -0
- cli_mem-0.5.1/tests/test_e2e.py +714 -0
- cli_mem-0.5.1/tests/test_fix.py +973 -0
- {cli_mem-0.4.0 → cli_mem-0.5.1}/tests/test_groups.py +203 -24
- cli_mem-0.5.1/tests/test_hooks_e2e.py +1162 -0
- cli_mem-0.5.1/tests/test_import.py +1160 -0
- cli_mem-0.5.1/tests/test_keychain.py +816 -0
- cli_mem-0.5.1/tests/test_mcp.py +2092 -0
- {cli_mem-0.4.0 → cli_mem-0.5.1}/tests/test_patterns.py +473 -29
- cli_mem-0.5.1/tests/test_picks.py +283 -0
- cli_mem-0.5.1/tests/test_promote.py +906 -0
- cli_mem-0.5.1/tests/test_search.py +555 -0
- cli_mem-0.5.1/tests/test_storage.py +938 -0
- cli_mem-0.5.1/tests/test_storage_destructive.py +1519 -0
- cli_mem-0.5.1/tests/test_tui.py +1088 -0
- cli_mem-0.5.1/tests/test_variables.py +958 -0
- cli_mem-0.4.0/.github/workflows/ci.yml +0 -29
- cli_mem-0.4.0/.github/workflows/release.yml +0 -91
- cli_mem-0.4.0/PKG-INFO +0 -410
- cli_mem-0.4.0/README.md +0 -381
- cli_mem-0.4.0/command-groups/security/recon.json +0 -35
- cli_mem-0.4.0/docs/decisions/004-per-repo-jsonl.md +0 -24
- cli_mem-0.4.0/hooks/mem.bash +0 -42
- cli_mem-0.4.0/hooks/mem.fish +0 -13
- cli_mem-0.4.0/hooks/mem.zsh +0 -28
- cli_mem-0.4.0/pyproject.toml +0 -47
- cli_mem-0.4.0/src/mem/cli.py +0 -1385
- cli_mem-0.4.0/src/mem/models.py +0 -137
- cli_mem-0.4.0/src/mem/patterns.py +0 -316
- cli_mem-0.4.0/src/mem/search.py +0 -173
- cli_mem-0.4.0/src/mem/storage.py +0 -419
- cli_mem-0.4.0/src/mem/variables.py +0 -480
- cli_mem-0.4.0/tests/conftest.py +0 -47
- cli_mem-0.4.0/tests/test_capture.py +0 -344
- cli_mem-0.4.0/tests/test_search.py +0 -110
- cli_mem-0.4.0/tests/test_storage.py +0 -121
- {cli_mem-0.4.0 → cli_mem-0.5.1}/.gitignore +0 -0
- {cli_mem-0.4.0 → cli_mem-0.5.1}/LICENSE +0 -0
- {cli_mem-0.4.0 → cli_mem-0.5.1}/assets/.gitkeep +0 -0
- {cli_mem-0.4.0 → cli_mem-0.5.1}/command-groups/databases/postgres-debug.json +0 -0
- {cli_mem-0.4.0 → cli_mem-0.5.1}/command-groups/databases/redis-ops.json +0 -0
- {cli_mem-0.4.0 → cli_mem-0.5.1}/command-groups/devops/docker-cleanup.json +0 -0
- {cli_mem-0.4.0 → cli_mem-0.5.1}/command-groups/devops/incident-response.json +0 -0
- {cli_mem-0.4.0 → cli_mem-0.5.1}/command-groups/devops/k8s-debug.json +0 -0
- {cli_mem-0.4.0 → cli_mem-0.5.1}/command-groups/git/cleanup.json +0 -0
- {cli_mem-0.4.0 → cli_mem-0.5.1}/command-groups/git/forensics.json +0 -0
- {cli_mem-0.4.0 → cli_mem-0.5.1}/command-groups/networking/dns-debug.json +0 -0
- {cli_mem-0.4.0 → cli_mem-0.5.1}/command-groups/networking/network-debug.json +0 -0
- {cli_mem-0.4.0 → cli_mem-0.5.1}/command-groups/performance/system-profile.json +0 -0
- {cli_mem-0.4.0 → cli_mem-0.5.1}/command-groups/security/headers-audit.json +0 -0
- {cli_mem-0.4.0 → cli_mem-0.5.1}/command-groups/security/ssl-audit.json +0 -0
- {cli_mem-0.4.0 → cli_mem-0.5.1}/docs/decisions/001-jsonl-over-sqlite.md +0 -0
- {cli_mem-0.4.0 → cli_mem-0.5.1}/docs/decisions/002-apple-fm-sdk-for-patterns.md +0 -0
- {cli_mem-0.4.0 → cli_mem-0.5.1}/docs/decisions/003-no-daemon.md +0 -0
- {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
|
|
59
|
-
│ infra.jsonl
|
|
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
|
|
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
|
-
|
|
125
|
+
Prefilter: needle test on the raw line's command span (skips json.loads)
|
|
124
126
|
│
|
|
125
127
|
▼
|
|
126
|
-
|
|
127
|
-
|
|
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
|
-
├──
|
|
130
|
-
├──
|
|
131
|
-
|
|
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":
|
|
331
|
-
"DB_HOST": { "value":
|
|
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)
|