msgsearch 0.2.0__tar.gz → 0.2.2__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 (54) hide show
  1. {msgsearch-0.2.0 → msgsearch-0.2.2}/ARCHITECTURE.md +1 -1
  2. {msgsearch-0.2.0 → msgsearch-0.2.2}/CHANGELOG.md +34 -0
  3. {msgsearch-0.2.0 → msgsearch-0.2.2}/PKG-INFO +101 -23
  4. {msgsearch-0.2.0 → msgsearch-0.2.2}/README.md +100 -22
  5. {msgsearch-0.2.0 → msgsearch-0.2.2}/pyproject.toml +1 -1
  6. {msgsearch-0.2.0 → msgsearch-0.2.2}/src/msgsearch/__init__.py +1 -1
  7. {msgsearch-0.2.0 → msgsearch-0.2.2}/src/msgsearch/cli.py +58 -0
  8. {msgsearch-0.2.0 → msgsearch-0.2.2}/src/msgsearch/config.py +1 -1
  9. {msgsearch-0.2.0 → msgsearch-0.2.2}/src/msgsearch/doctor.py +1 -1
  10. {msgsearch-0.2.0 → msgsearch-0.2.2}/src/msgsearch/embedder.py +1 -1
  11. {msgsearch-0.2.0 → msgsearch-0.2.2}/tests/test_cli.py +23 -0
  12. {msgsearch-0.2.0 → msgsearch-0.2.2}/.claude/agents/corpus-explorer.md +0 -0
  13. {msgsearch-0.2.0 → msgsearch-0.2.2}/.claude/agents/retrieval-critic.md +0 -0
  14. {msgsearch-0.2.0 → msgsearch-0.2.2}/.claude/commands/bench.md +0 -0
  15. {msgsearch-0.2.0 → msgsearch-0.2.2}/.claude/commands/go.md +0 -0
  16. {msgsearch-0.2.0 → msgsearch-0.2.2}/.claude/commands/recon.md +0 -0
  17. {msgsearch-0.2.0 → msgsearch-0.2.2}/.claude/hooks/verify-retrieval.py +0 -0
  18. {msgsearch-0.2.0 → msgsearch-0.2.2}/.claude/settings.json +0 -0
  19. {msgsearch-0.2.0 → msgsearch-0.2.2}/.github/workflows/ci.yml +0 -0
  20. {msgsearch-0.2.0 → msgsearch-0.2.2}/.github/workflows/release.yml +0 -0
  21. {msgsearch-0.2.0 → msgsearch-0.2.2}/.gitignore +0 -0
  22. {msgsearch-0.2.0 → msgsearch-0.2.2}/AGENTS.md +0 -0
  23. {msgsearch-0.2.0 → msgsearch-0.2.2}/CLAUDE.md +0 -0
  24. {msgsearch-0.2.0 → msgsearch-0.2.2}/CONTRIBUTING.md +0 -0
  25. {msgsearch-0.2.0 → msgsearch-0.2.2}/LICENSE +0 -0
  26. {msgsearch-0.2.0 → msgsearch-0.2.2}/eval/__init__.py +0 -0
  27. {msgsearch-0.2.0 → msgsearch-0.2.2}/eval/bench.py +0 -0
  28. {msgsearch-0.2.0 → msgsearch-0.2.2}/eval/gold.jsonl +0 -0
  29. {msgsearch-0.2.0 → msgsearch-0.2.2}/eval/label.py +0 -0
  30. {msgsearch-0.2.0 → msgsearch-0.2.2}/eval/results/baseline.json +0 -0
  31. {msgsearch-0.2.0 → msgsearch-0.2.2}/eval/results/retriever.json +0 -0
  32. {msgsearch-0.2.0 → msgsearch-0.2.2}/eval/results/retriever_bm25.json +0 -0
  33. {msgsearch-0.2.0 → msgsearch-0.2.2}/eval/results/retriever_dense.json +0 -0
  34. {msgsearch-0.2.0 → msgsearch-0.2.2}/eval/results/retriever_norerank.json +0 -0
  35. {msgsearch-0.2.0 → msgsearch-0.2.2}/eval/retriever.py +0 -0
  36. {msgsearch-0.2.0 → msgsearch-0.2.2}/eval/retriever_bm25.py +0 -0
  37. {msgsearch-0.2.0 → msgsearch-0.2.2}/eval/retriever_dense.py +0 -0
  38. {msgsearch-0.2.0 → msgsearch-0.2.2}/eval/retriever_norerank.py +0 -0
  39. {msgsearch-0.2.0 → msgsearch-0.2.2}/src/msgsearch/attributed_body.py +0 -0
  40. {msgsearch-0.2.0 → msgsearch-0.2.2}/src/msgsearch/chunk.py +0 -0
  41. {msgsearch-0.2.0 → msgsearch-0.2.2}/src/msgsearch/contacts.py +0 -0
  42. {msgsearch-0.2.0 → msgsearch-0.2.2}/src/msgsearch/explore.py +0 -0
  43. {msgsearch-0.2.0 → msgsearch-0.2.2}/src/msgsearch/extract.py +0 -0
  44. {msgsearch-0.2.0 → msgsearch-0.2.2}/src/msgsearch/index.py +0 -0
  45. {msgsearch-0.2.0 → msgsearch-0.2.2}/src/msgsearch/py.typed +0 -0
  46. {msgsearch-0.2.0 → msgsearch-0.2.2}/src/msgsearch/search.py +0 -0
  47. {msgsearch-0.2.0 → msgsearch-0.2.2}/src/msgsearch/sync.py +0 -0
  48. {msgsearch-0.2.0 → msgsearch-0.2.2}/src/msgsearch/tagging.py +0 -0
  49. {msgsearch-0.2.0 → msgsearch-0.2.2}/tests/test_chunk.py +0 -0
  50. {msgsearch-0.2.0 → msgsearch-0.2.2}/tests/test_contacts.py +0 -0
  51. {msgsearch-0.2.0 → msgsearch-0.2.2}/tests/test_index.py +0 -0
  52. {msgsearch-0.2.0 → msgsearch-0.2.2}/tests/test_search.py +0 -0
  53. {msgsearch-0.2.0 → msgsearch-0.2.2}/tests/test_sync.py +0 -0
  54. {msgsearch-0.2.0 → msgsearch-0.2.2}/tests/test_tagging.py +0 -0
@@ -125,7 +125,7 @@ Both run locally on Metal via MPS.
125
125
 
126
126
  | role | model | notes |
127
127
  |---|---|---|
128
- | embedding | `google/embeddinggemma-300m` | 768-dim. **Gated** — needs Gemma licence acceptance and `hf auth login`. Matryoshka truncation to 512/256/128 available if the array grows. |
128
+ | embedding | `google/embeddinggemma-300m` | 768-dim. **Gated** — needs Gemma licence acceptance and `msgsearch login`. Matryoshka truncation to 512/256/128 available if the array grows. |
129
129
  | rerank | `Qwen/Qwen3-Reranker-0.6B` | matches qmd's choice |
130
130
 
131
131
  Rerankers measured over 50 candidates on the one query available:
@@ -3,6 +3,40 @@
3
3
  Notable changes to msgsearch. Retrieval changes carry the measurement that
4
4
  justified them; see `ARCHITECTURE.md` for the full evaluation.
5
5
 
6
+ ## [0.2.2] — 2026-09-06
7
+
8
+ ### Added
9
+ - `msgsearch login`, which authenticates with HuggingFace. The instructions
10
+ previously said to run `hf auth login`, but installing msgsearch does not put
11
+ `hf` on your PATH -- pipx exposes only the entry points a package declares, so
12
+ that step failed with "command not found" for everyone who installed normally.
13
+ It also verifies afterwards that the gated model is actually reachable, since
14
+ being logged in and having accepted the model licence are different things
15
+ whose failures look identical.
16
+
17
+ ### Changed
18
+ - Full Disk Access instructions are now step by step, including that macOS grants
19
+ it to the application rather than the shell and that the application must be
20
+ restarted before it takes effect.
21
+
22
+ ## [0.2.1] — 2026-09-06
23
+
24
+ Documentation only. PyPI freezes a project's description at publish time, so
25
+ correcting the README required a release.
26
+
27
+ ### Fixed
28
+ - The install instructions recommended `pipx install --python <arm64 python>`,
29
+ which does not work on Apple silicon with an Intel Homebrew. Python from
30
+ python.org is a universal binary that runs as whichever architecture its
31
+ parent process is, so an Intel-built pipx launches it as x86_64 whichever
32
+ interpreter you name, and PyTorch has no x86_64 macOS wheels. The working
33
+ recipe creates the venv under `arch -arm64`.
34
+
35
+ ### Added
36
+ - A quickstart covering install through first search in one block, and an
37
+ explicit note that Full Disk Access and Contacts are separate permissions
38
+ granted to the application rather than the shell.
39
+
6
40
  ## [0.2.0] — 2026-09-06
7
41
 
8
42
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: msgsearch
3
- Version: 0.2.0
3
+ Version: 0.2.2
4
4
  Summary: Search your iMessage history by meaning, entirely on your own machine.
5
5
  Project-URL: Homepage, https://github.com/dhruv1707/msgsearch
6
6
  Project-URL: Documentation, https://github.com/dhruv1707/msgsearch/blob/main/README.md
@@ -63,46 +63,124 @@ up to make committing it difficult. Treat that directory the way you would treat
63
63
  password manager's database. Both models run locally, so nothing is uploaded, but
64
64
  what lands on your disk is unencrypted.
65
65
 
66
- ## Requirements
66
+ ## Quickstart
67
+
68
+ ```bash
69
+ # 1. install (needs an arm64 Python — see Install if this errors)
70
+ pipx install msgsearch
71
+
72
+ # 2. get the embedding model (accept the licence in a browser first, then:)
73
+ msgsearch login
74
+
75
+ # 3. snapshot your messages [needs Full Disk Access]
76
+ msgsearch sync
77
+
78
+ # 4. check the setup, and fix whatever it names
79
+ msgsearch doctor
80
+
81
+ # 5. build the index [~30 min per 100k messages, once]
82
+ msgsearch index
83
+
84
+ # 6. search
85
+ msgsearch search "that restaurant we talked about"
86
+ ```
87
+
88
+ Afterwards, one command keeps it current — only genuinely new text is embedded,
89
+ so it takes seconds:
67
90
 
68
- - macOS with an **arm64** Python 3.10 or newer. PyTorch stopped publishing
69
- x86_64 macOS wheels, so an Intel-built interpreter cannot install torch and
70
- cannot use the GPU — and Homebrew's `/usr/local` Python is an Intel build even
71
- on Apple silicon. Check with:
91
+ ```bash
92
+ msgsearch sync --index
93
+ ```
72
94
 
73
- ```bash
74
- python3 -c "import platform; print(platform.machine())" # must say arm64
75
- ```
95
+ ### Granting Full Disk Access
76
96
 
77
- If it says `x86_64`, point the installer at an arm64 interpreter explicitly:
97
+ Step 3 fails without it. macOS grants this to the **application**, not to your
98
+ shell, so the grant goes to whatever program you type commands into.
78
99
 
79
- ```bash
80
- pipx install --python /Library/Frameworks/Python.framework/Versions/3.12/bin/python3 msgsearch
81
- ```
100
+ 1. Open **System Settings → Privacy & Security → Full Disk Access**
101
+ 2. Click **+**
102
+ 3. Press **⌘⇧G** and paste `/Applications/Utilities/Terminal.app`, then Open
103
+ (if you use iTerm, VS Code or another terminal, choose that instead)
104
+ 4. Make sure its toggle is **on**
105
+ 5. **Quit and reopen that application** — the permission is only read at launch
82
106
 
83
- Architecture is not something package metadata can express, so this fails at
84
- dependency resolution rather than with a helpful message. `msgsearch doctor`
85
- checks it.
107
+ Resolving contact names needs a *separate* permission, **Contacts**, granted the
108
+ same way in the same place. It is optional; without it speakers appear as phone
109
+ numbers. Full Disk Access does not include it.
110
+
111
+ ## Requirements
112
+
113
+ - **macOS on Apple silicon.** PyTorch no longer publishes x86_64 macOS wheels.
114
+ - **Python 3.10 or newer, running as arm64.** See the note below — this is the
115
+ one thing that reliably goes wrong.
86
116
  - About 3 GB of disk for dependencies and model weights.
87
117
 
88
118
  ## Install
89
119
 
90
120
  ```bash
91
- uvx msgsearch doctor # run it without installing anything
92
- pipx install msgsearch # or install the command globally
121
+ pipx install msgsearch
122
+ msgsearch doctor
93
123
  ```
94
124
 
95
- Either pulls PyTorch, which is around 1 GB, so the first run is slow. Everything
96
- after that is local and fast.
125
+ This pulls PyTorch, around 1 GB, so the first install is slow. Everything after
126
+ that is local and fast.
127
+
128
+ <details>
129
+ <summary><b>If that fails with "No matching distribution found for torch"</b></summary>
97
130
 
98
- To work on it instead, clone and install in place:
131
+ You have an Intel-built Python, which is common on Apple silicon after migrating
132
+ from an Intel Mac. Check:
133
+
134
+ ```bash
135
+ python3 -c "import platform; print(platform.machine())" # must say arm64
136
+ ```
137
+
138
+ The confusing part is that `pipx install --python /path/to/arm64/python` often
139
+ **does not fix it**. Python from python.org is a *universal* binary that runs as
140
+ whichever architecture its parent process is, and if pipx itself was installed by
141
+ an Intel Homebrew (`/usr/local/...`), it launches that Python as x86_64. pip then
142
+ looks for x86_64 wheels that PyTorch does not publish.
143
+
144
+ Install into a venv created explicitly under arm64 instead:
145
+
146
+ ```bash
147
+ arch -arm64 /Library/Frameworks/Python.framework/Versions/3.12/bin/python3 \
148
+ -m venv ~/.msgsearch-venv
149
+ ~/.msgsearch-venv/bin/pip install msgsearch
150
+ ln -sf ~/.msgsearch-venv/bin/msgsearch /usr/local/bin/msgsearch
151
+ ```
152
+
153
+ Adjust the interpreter path to any arm64 Python 3.10+. To undo:
154
+ `rm /usr/local/bin/msgsearch && rm -rf ~/.msgsearch-venv`.
155
+
156
+ Architecture cannot be expressed in package metadata, which is why this surfaces
157
+ as a dependency-resolution wall rather than a useful error. `msgsearch doctor`
158
+ reports it in one line.
159
+ </details>
160
+
161
+ To work on msgsearch rather than use it:
99
162
 
100
163
  ```bash
101
164
  git clone https://github.com/dhruv1707/msgsearch && cd msgsearch
102
- python3 -m venv .venv # must be an arm64 interpreter
165
+ arch -arm64 python3 -m venv .venv # must be an arm64 interpreter
103
166
  ./.venv/bin/python -m pip install -e ".[dev]"
104
167
  ```
105
168
 
169
+ ## Setup
170
+
171
+ Get access to the embedding model. `google/embeddinggemma-300m` is gated, so:
172
+
173
+ 1. Accept the licence at <https://huggingface.co/google/embeddinggemma-300m>
174
+ 2. Create a read token at <https://huggingface.co/settings/tokens>
175
+ 3. Run `msgsearch login` and paste it
176
+
177
+ `msgsearch login` checks afterwards that you can actually reach the model, since
178
+ being logged in and having accepted the licence are different things — and the
179
+ failure for the second looks identical to the first.
180
+
181
+ Any sentence-transformers model works instead, for example
182
+ `MSGSEARCH_EMBED_MODEL=BAAI/bge-small-en-v1.5`.
183
+
106
184
  Take a snapshot of the Messages database. **Never point this tool at
107
185
  `~/Library/Messages`**: that file is live, Messages.app holds locks on it, and it
108
186
  is irreplaceable.
@@ -126,7 +204,7 @@ accept the Gemma licence at
126
204
  <https://huggingface.co/google/embeddinggemma-300m> and authenticate:
127
205
 
128
206
  ```bash
129
- ./.venv/bin/hf auth login
207
+ ./.venv/bin/msgsearch login
130
208
  ```
131
209
 
132
210
  Any sentence-transformers model works instead if you would rather not, for example
@@ -17,46 +17,124 @@ up to make committing it difficult. Treat that directory the way you would treat
17
17
  password manager's database. Both models run locally, so nothing is uploaded, but
18
18
  what lands on your disk is unencrypted.
19
19
 
20
- ## Requirements
20
+ ## Quickstart
21
+
22
+ ```bash
23
+ # 1. install (needs an arm64 Python — see Install if this errors)
24
+ pipx install msgsearch
25
+
26
+ # 2. get the embedding model (accept the licence in a browser first, then:)
27
+ msgsearch login
28
+
29
+ # 3. snapshot your messages [needs Full Disk Access]
30
+ msgsearch sync
31
+
32
+ # 4. check the setup, and fix whatever it names
33
+ msgsearch doctor
34
+
35
+ # 5. build the index [~30 min per 100k messages, once]
36
+ msgsearch index
37
+
38
+ # 6. search
39
+ msgsearch search "that restaurant we talked about"
40
+ ```
41
+
42
+ Afterwards, one command keeps it current — only genuinely new text is embedded,
43
+ so it takes seconds:
21
44
 
22
- - macOS with an **arm64** Python 3.10 or newer. PyTorch stopped publishing
23
- x86_64 macOS wheels, so an Intel-built interpreter cannot install torch and
24
- cannot use the GPU — and Homebrew's `/usr/local` Python is an Intel build even
25
- on Apple silicon. Check with:
45
+ ```bash
46
+ msgsearch sync --index
47
+ ```
26
48
 
27
- ```bash
28
- python3 -c "import platform; print(platform.machine())" # must say arm64
29
- ```
49
+ ### Granting Full Disk Access
30
50
 
31
- If it says `x86_64`, point the installer at an arm64 interpreter explicitly:
51
+ Step 3 fails without it. macOS grants this to the **application**, not to your
52
+ shell, so the grant goes to whatever program you type commands into.
32
53
 
33
- ```bash
34
- pipx install --python /Library/Frameworks/Python.framework/Versions/3.12/bin/python3 msgsearch
35
- ```
54
+ 1. Open **System Settings → Privacy & Security → Full Disk Access**
55
+ 2. Click **+**
56
+ 3. Press **⌘⇧G** and paste `/Applications/Utilities/Terminal.app`, then Open
57
+ (if you use iTerm, VS Code or another terminal, choose that instead)
58
+ 4. Make sure its toggle is **on**
59
+ 5. **Quit and reopen that application** — the permission is only read at launch
36
60
 
37
- Architecture is not something package metadata can express, so this fails at
38
- dependency resolution rather than with a helpful message. `msgsearch doctor`
39
- checks it.
61
+ Resolving contact names needs a *separate* permission, **Contacts**, granted the
62
+ same way in the same place. It is optional; without it speakers appear as phone
63
+ numbers. Full Disk Access does not include it.
64
+
65
+ ## Requirements
66
+
67
+ - **macOS on Apple silicon.** PyTorch no longer publishes x86_64 macOS wheels.
68
+ - **Python 3.10 or newer, running as arm64.** See the note below — this is the
69
+ one thing that reliably goes wrong.
40
70
  - About 3 GB of disk for dependencies and model weights.
41
71
 
42
72
  ## Install
43
73
 
44
74
  ```bash
45
- uvx msgsearch doctor # run it without installing anything
46
- pipx install msgsearch # or install the command globally
75
+ pipx install msgsearch
76
+ msgsearch doctor
47
77
  ```
48
78
 
49
- Either pulls PyTorch, which is around 1 GB, so the first run is slow. Everything
50
- after that is local and fast.
79
+ This pulls PyTorch, around 1 GB, so the first install is slow. Everything after
80
+ that is local and fast.
81
+
82
+ <details>
83
+ <summary><b>If that fails with "No matching distribution found for torch"</b></summary>
51
84
 
52
- To work on it instead, clone and install in place:
85
+ You have an Intel-built Python, which is common on Apple silicon after migrating
86
+ from an Intel Mac. Check:
87
+
88
+ ```bash
89
+ python3 -c "import platform; print(platform.machine())" # must say arm64
90
+ ```
91
+
92
+ The confusing part is that `pipx install --python /path/to/arm64/python` often
93
+ **does not fix it**. Python from python.org is a *universal* binary that runs as
94
+ whichever architecture its parent process is, and if pipx itself was installed by
95
+ an Intel Homebrew (`/usr/local/...`), it launches that Python as x86_64. pip then
96
+ looks for x86_64 wheels that PyTorch does not publish.
97
+
98
+ Install into a venv created explicitly under arm64 instead:
99
+
100
+ ```bash
101
+ arch -arm64 /Library/Frameworks/Python.framework/Versions/3.12/bin/python3 \
102
+ -m venv ~/.msgsearch-venv
103
+ ~/.msgsearch-venv/bin/pip install msgsearch
104
+ ln -sf ~/.msgsearch-venv/bin/msgsearch /usr/local/bin/msgsearch
105
+ ```
106
+
107
+ Adjust the interpreter path to any arm64 Python 3.10+. To undo:
108
+ `rm /usr/local/bin/msgsearch && rm -rf ~/.msgsearch-venv`.
109
+
110
+ Architecture cannot be expressed in package metadata, which is why this surfaces
111
+ as a dependency-resolution wall rather than a useful error. `msgsearch doctor`
112
+ reports it in one line.
113
+ </details>
114
+
115
+ To work on msgsearch rather than use it:
53
116
 
54
117
  ```bash
55
118
  git clone https://github.com/dhruv1707/msgsearch && cd msgsearch
56
- python3 -m venv .venv # must be an arm64 interpreter
119
+ arch -arm64 python3 -m venv .venv # must be an arm64 interpreter
57
120
  ./.venv/bin/python -m pip install -e ".[dev]"
58
121
  ```
59
122
 
123
+ ## Setup
124
+
125
+ Get access to the embedding model. `google/embeddinggemma-300m` is gated, so:
126
+
127
+ 1. Accept the licence at <https://huggingface.co/google/embeddinggemma-300m>
128
+ 2. Create a read token at <https://huggingface.co/settings/tokens>
129
+ 3. Run `msgsearch login` and paste it
130
+
131
+ `msgsearch login` checks afterwards that you can actually reach the model, since
132
+ being logged in and having accepted the licence are different things — and the
133
+ failure for the second looks identical to the first.
134
+
135
+ Any sentence-transformers model works instead, for example
136
+ `MSGSEARCH_EMBED_MODEL=BAAI/bge-small-en-v1.5`.
137
+
60
138
  Take a snapshot of the Messages database. **Never point this tool at
61
139
  `~/Library/Messages`**: that file is live, Messages.app holds locks on it, and it
62
140
  is irreplaceable.
@@ -80,7 +158,7 @@ accept the Gemma licence at
80
158
  <https://huggingface.co/google/embeddinggemma-300m> and authenticate:
81
159
 
82
160
  ```bash
83
- ./.venv/bin/hf auth login
161
+ ./.venv/bin/msgsearch login
84
162
  ```
85
163
 
86
164
  Any sentence-transformers model works instead if you would rather not, for example
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "msgsearch"
7
- version = "0.2.0"
7
+ version = "0.2.2"
8
8
  description = "Search your iMessage history by meaning, entirely on your own machine."
9
9
  readme = "README.md"
10
10
  license = { file = "LICENSE" }
@@ -13,6 +13,6 @@ The pipeline, in the order data flows through it:
13
13
  Nothing here sends message text anywhere. Both models run locally.
14
14
  """
15
15
 
16
- __version__ = "0.2.0"
16
+ __version__ = "0.2.2"
17
17
 
18
18
  __all__ = ["__version__"]
@@ -156,6 +156,63 @@ def _run_search(args) -> int:
156
156
  return 0
157
157
 
158
158
 
159
+ def _add_login_command(subparsers) -> None:
160
+ parser = subparsers.add_parser(
161
+ "login",
162
+ help="authenticate with HuggingFace to download the embedding model",
163
+ description="The default embedding model is gated, so it needs a "
164
+ "HuggingFace account that has accepted its licence. This wraps the same "
165
+ "login the huggingface_hub library provides -- msgsearch owns the "
166
+ "command because installing msgsearch does not put `hf` on your PATH.",
167
+ )
168
+ parser.add_argument(
169
+ "--token",
170
+ metavar="TOKEN",
171
+ help="read token from https://huggingface.co/settings/tokens "
172
+ "(prompted for if omitted)",
173
+ )
174
+ parser.set_defaults(handler=_run_login)
175
+
176
+
177
+ def _run_login(args) -> int:
178
+ from getpass import getpass
179
+
180
+ from huggingface_hub import login
181
+
182
+ from . import config
183
+
184
+ token = args.token
185
+ if not token:
186
+ print(f"A read token is needed to download {config.EMBED_MODEL}.")
187
+ print("Create one at https://huggingface.co/settings/tokens")
188
+ print(f"and accept the licence at https://huggingface.co/{config.EMBED_MODEL}")
189
+ try:
190
+ token = getpass("\nToken (input hidden): ").strip()
191
+ except (EOFError, KeyboardInterrupt):
192
+ print()
193
+ return 130
194
+ if not token:
195
+ print("No token given.", file=sys.stderr)
196
+ return 1
197
+
198
+ try:
199
+ login(token=token)
200
+ except Exception as error:
201
+ print(f"Login failed: {error}", file=sys.stderr)
202
+ return 1
203
+
204
+ # Logging in successfully is not the same as having access to a gated model,
205
+ # and that difference is exactly what produces a baffling 401 later.
206
+ from .doctor import check_model
207
+
208
+ result = check_model()
209
+ print(f"\nlogged in. model: {result.detail}")
210
+ if result.fix:
211
+ print(result.fix)
212
+ return 1
213
+ return 0
214
+
215
+
159
216
  def _add_contacts_command(subparsers) -> None:
160
217
  parser = subparsers.add_parser(
161
218
  "contacts",
@@ -325,6 +382,7 @@ def build_parser() -> argparse.ArgumentParser:
325
382
  _add_doctor_command(subparsers)
326
383
  _add_explore_command(subparsers)
327
384
  _add_index_command(subparsers)
385
+ _add_login_command(subparsers)
328
386
  _add_search_command(subparsers)
329
387
  _add_sync_command(subparsers)
330
388
  return parser
@@ -76,7 +76,7 @@ PASSAGE_STRIDE = 1
76
76
 
77
77
  # --- models ------------------------------------------------------------------
78
78
 
79
- # Gated on HuggingFace: accept the model terms and `hf auth login` once, or this
79
+ # Gated on HuggingFace: accept the model terms and run `msgsearch login` once,
80
80
  # 401s. Must match the model the index was built with -- search.py checks.
81
81
  EMBED_MODEL = os.environ.get("MSGSEARCH_EMBED_MODEL", "google/embeddinggemma-300m")
82
82
 
@@ -124,7 +124,7 @@ def check_model() -> Check:
124
124
  FAIL,
125
125
  f"{name} is gated and this machine is not authorised",
126
126
  f"Accept the licence at https://huggingface.co/{name}, then run "
127
- "'hf auth login'. An expired token gives this same error, so log in "
127
+ "'msgsearch login'. An expired token gives this same error, so log in "
128
128
  "again even if you have before. Or set "
129
129
  "MSGSEARCH_EMBED_MODEL=BAAI/bge-small-en-v1.5 to use an ungated model.",
130
130
  )
@@ -37,7 +37,7 @@ def _load_failure_hint(model_name: str, error: Exception) -> str:
37
37
  f"is not authorised.\n\n"
38
38
  f" 1. Accept the licence at https://huggingface.co/{model_name}\n"
39
39
  f" 2. Create a read token at https://huggingface.co/settings/tokens\n"
40
- f" 3. Run: hf auth login\n\n"
40
+ f" 3. Run: msgsearch login\n\n"
41
41
  f"Note that an *expired* token produces this same error, so re-run step 3 "
42
42
  f"even if you have logged in before. To use an ungated model instead:\n\n"
43
43
  f" MSGSEARCH_EMBED_MODEL=BAAI/bge-small-en-v1.5\n\n"
@@ -75,3 +75,26 @@ class TestEntryPoint(unittest.TestCase):
75
75
 
76
76
  if __name__ == "__main__":
77
77
  unittest.main()
78
+
79
+
80
+ class TestLoginCommand(unittest.TestCase):
81
+ """Authentication is a msgsearch command because `hf` is not on the PATH.
82
+
83
+ Installing msgsearch exposes only the entry points msgsearch declares, so
84
+ telling people to run `hf auth login` failed with "command not found" for
85
+ everyone who installed it normally rather than from a checkout.
86
+ """
87
+
88
+ def setUp(self):
89
+ self.parser = cli.build_parser()
90
+
91
+ def test_login_is_a_command(self):
92
+ args = self.parser.parse_args(["login"])
93
+ self.assertTrue(callable(args.handler))
94
+
95
+ def test_token_can_be_passed_non_interactively(self):
96
+ args = self.parser.parse_args(["login", "--token", "hf_example"])
97
+ self.assertEqual(args.token, "hf_example")
98
+
99
+ def test_token_is_optional_so_it_can_prompt(self):
100
+ self.assertIsNone(self.parser.parse_args(["login"]).token)
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes