majordomo-cli 0.1.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.
Files changed (76) hide show
  1. majordomo_cli-0.1.0/PKG-INFO +328 -0
  2. majordomo_cli-0.1.0/README.md +295 -0
  3. majordomo_cli-0.1.0/majordomo/__init__.py +30 -0
  4. majordomo_cli-0.1.0/majordomo/activity.py +669 -0
  5. majordomo_cli-0.1.0/majordomo/agent.py +217 -0
  6. majordomo_cli-0.1.0/majordomo/asr.py +396 -0
  7. majordomo_cli-0.1.0/majordomo/brief.py +83 -0
  8. majordomo_cli-0.1.0/majordomo/chat.py +768 -0
  9. majordomo_cli-0.1.0/majordomo/cli.py +1446 -0
  10. majordomo_cli-0.1.0/majordomo/config.example.yml +229 -0
  11. majordomo_cli-0.1.0/majordomo/config.py +561 -0
  12. majordomo_cli-0.1.0/majordomo/context.py +139 -0
  13. majordomo_cli-0.1.0/majordomo/coordinator.py +255 -0
  14. majordomo_cli-0.1.0/majordomo/documents.py +165 -0
  15. majordomo_cli-0.1.0/majordomo/dotenv.py +93 -0
  16. majordomo_cli-0.1.0/majordomo/firstrun.py +416 -0
  17. majordomo_cli-0.1.0/majordomo/hook.py +186 -0
  18. majordomo_cli-0.1.0/majordomo/install.py +229 -0
  19. majordomo_cli-0.1.0/majordomo/jsonlog.py +50 -0
  20. majordomo_cli-0.1.0/majordomo/keys.py +321 -0
  21. majordomo_cli-0.1.0/majordomo/llm.py +680 -0
  22. majordomo_cli-0.1.0/majordomo/memory.py +530 -0
  23. majordomo_cli-0.1.0/majordomo/models.py +186 -0
  24. majordomo_cli-0.1.0/majordomo/panel.py +282 -0
  25. majordomo_cli-0.1.0/majordomo/paths.py +79 -0
  26. majordomo_cli-0.1.0/majordomo/prompts.py +672 -0
  27. majordomo_cli-0.1.0/majordomo/render.py +182 -0
  28. majordomo_cli-0.1.0/majordomo/resume.py +102 -0
  29. majordomo_cli-0.1.0/majordomo/router.py +82 -0
  30. majordomo_cli-0.1.0/majordomo/scaffold.py +233 -0
  31. majordomo_cli-0.1.0/majordomo/session.py +471 -0
  32. majordomo_cli-0.1.0/majordomo/speechgate.py +100 -0
  33. majordomo_cli-0.1.0/majordomo/state.py +223 -0
  34. majordomo_cli-0.1.0/majordomo/tools.py +668 -0
  35. majordomo_cli-0.1.0/majordomo/tray.py +84 -0
  36. majordomo_cli-0.1.0/majordomo/trigger.py +235 -0
  37. majordomo_cli-0.1.0/majordomo/tts.py +353 -0
  38. majordomo_cli-0.1.0/majordomo/workers/__init__.py +41 -0
  39. majordomo_cli-0.1.0/majordomo/workers/github.py +232 -0
  40. majordomo_cli-0.1.0/majordomo/workers/gmail.py +328 -0
  41. majordomo_cli-0.1.0/majordomo/workers/sessions.py +197 -0
  42. majordomo_cli-0.1.0/majordomo_cli.egg-info/PKG-INFO +328 -0
  43. majordomo_cli-0.1.0/majordomo_cli.egg-info/SOURCES.txt +74 -0
  44. majordomo_cli-0.1.0/majordomo_cli.egg-info/dependency_links.txt +1 -0
  45. majordomo_cli-0.1.0/majordomo_cli.egg-info/entry_points.txt +2 -0
  46. majordomo_cli-0.1.0/majordomo_cli.egg-info/requires.txt +19 -0
  47. majordomo_cli-0.1.0/majordomo_cli.egg-info/top_level.txt +1 -0
  48. majordomo_cli-0.1.0/pyproject.toml +79 -0
  49. majordomo_cli-0.1.0/setup.cfg +4 -0
  50. majordomo_cli-0.1.0/tests/test_activity.py +774 -0
  51. majordomo_cli-0.1.0/tests/test_agent_and_tools.py +752 -0
  52. majordomo_cli-0.1.0/tests/test_asr_and_keys.py +815 -0
  53. majordomo_cli-0.1.0/tests/test_brief_and_cli.py +745 -0
  54. majordomo_cli-0.1.0/tests/test_chat_and_context.py +1982 -0
  55. majordomo_cli-0.1.0/tests/test_cli_assistant.py +830 -0
  56. majordomo_cli-0.1.0/tests/test_config.py +193 -0
  57. majordomo_cli-0.1.0/tests/test_coordinator.py +581 -0
  58. majordomo_cli-0.1.0/tests/test_documents.py +307 -0
  59. majordomo_cli-0.1.0/tests/test_dotenv.py +104 -0
  60. majordomo_cli-0.1.0/tests/test_firstrun.py +505 -0
  61. majordomo_cli-0.1.0/tests/test_hook.py +273 -0
  62. majordomo_cli-0.1.0/tests/test_install.py +249 -0
  63. majordomo_cli-0.1.0/tests/test_jsonlog.py +107 -0
  64. majordomo_cli-0.1.0/tests/test_llm.py +1032 -0
  65. majordomo_cli-0.1.0/tests/test_memory.py +374 -0
  66. majordomo_cli-0.1.0/tests/test_panel.py +261 -0
  67. majordomo_cli-0.1.0/tests/test_render.py +253 -0
  68. majordomo_cli-0.1.0/tests/test_resume.py +98 -0
  69. majordomo_cli-0.1.0/tests/test_router.py +70 -0
  70. majordomo_cli-0.1.0/tests/test_scaffold.py +75 -0
  71. majordomo_cli-0.1.0/tests/test_speechgate.py +111 -0
  72. majordomo_cli-0.1.0/tests/test_state.py +280 -0
  73. majordomo_cli-0.1.0/tests/test_tts.py +447 -0
  74. majordomo_cli-0.1.0/tests/test_workers_github.py +345 -0
  75. majordomo_cli-0.1.0/tests/test_workers_gmail.py +332 -0
  76. majordomo_cli-0.1.0/tests/test_workers_sessions.py +306 -0
@@ -0,0 +1,328 @@
1
+ Metadata-Version: 2.4
2
+ Name: majordomo-cli
3
+ Version: 0.1.0
4
+ Summary: A personal assistant for your terminal: talks with your dev context loaded, and acts on it
5
+ Author: Navaneeth Prabha
6
+ License-Expression: MIT
7
+ Keywords: assistant,agent,cli,llm,tts,speech,claude-code,github
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Environment :: Console
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Operating System :: Microsoft :: Windows
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.10
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Topic :: Utilities
17
+ Requires-Python: >=3.10
18
+ Description-Content-Type: text/markdown
19
+ Requires-Dist: httpx>=0.27
20
+ Requires-Dist: PyYAML>=6.0
21
+ Provides-Extra: nvidia
22
+ Requires-Dist: nvidia-riva-client>=2.14; extra == "nvidia"
23
+ Provides-Extra: voice
24
+ Requires-Dist: sounddevice>=0.4; extra == "voice"
25
+ Provides-Extra: documents
26
+ Requires-Dist: pypdf>=4.0; extra == "documents"
27
+ Requires-Dist: python-docx>=1.1; extra == "documents"
28
+ Provides-Extra: tray
29
+ Requires-Dist: pystray>=0.19; extra == "tray"
30
+ Requires-Dist: Pillow>=10.0; extra == "tray"
31
+ Provides-Extra: dev
32
+ Requires-Dist: pytest>=8.0; extra == "dev"
33
+
34
+ # Majordomo
35
+
36
+ > A majordomo is the head of a household staff who manages everyone and briefs the master.
37
+
38
+ A personal assistant that runs on your machine, knows what you have been building, and
39
+ does something about it. You talk to it, it answers with your context already loaded, and
40
+ it can act — read code, write files, run tests — asking before anything changes.
41
+
42
+ It is **not** a notification relay, and it is **not** a daemon. Your apps already ping you
43
+ "PR merged" / "new ticket". Majordomo does the opposite: it digests those streams and
44
+ surfaces only what needs a decision, when you ask it to.
45
+
46
+ ## Install, and the only command you need
47
+
48
+ ```
49
+ uv tool install majordomo-cli
50
+ mj
51
+ ```
52
+
53
+ That is the whole thing. The first `mj` configures itself — a model to talk to, and
54
+ optionally the Claude Code hooks and a spoken briefing on wake — and then drops you
55
+ straight into the conversation. Every `mj` after that just opens it, with a line telling
56
+ you what it already knows and where you left off.
57
+
58
+ ```
59
+ $ mj
60
+ Majordomo. /help for commands, `mj help` for the CLI, /exit to leave.
61
+ (6 memories indexed, 2 loaded in full, activity through 2026-09-26)
62
+ Last time: 20260926-1431 14 turns why is the fuser promoting context items
63
+
64
+ you › ▏
65
+ ```
66
+
67
+ **Why one command.** A tool has one job and a front door made of flags. An assistant is a
68
+ place you go, and its front door is itself. Sixteen subcommands meant knowing what you
69
+ wanted before you arrived — and meant sixteen names that could never change. One door is
70
+ one promise.
71
+
72
+ **Three names, and they differ on purpose.** You install `majordomo-cli`, you type `mj`,
73
+ and you import `majordomo`. `majordomo` was taken on PyPI by an unrelated package, and
74
+ plain `mj` is refused by it — untaken but not allowed, which the index will only tell you
75
+ at upload time. `[project.scripts]` is independent of all that, so the command is one
76
+ short word regardless.
77
+
78
+ `uv tool` (or `pipx`) rather than plain `pip`, because an app installed into whichever
79
+ virtualenv happened to be active disappears when you deactivate it. Plain `pip` still
80
+ works and is worth using if you want to import from this — `agent.run`, `brief.run`,
81
+ `context.build` are real library surface:
82
+
83
+ ```
84
+ pip install majordomo-cli
85
+ from majordomo import agent, brief, context
86
+ ```
87
+
88
+ Python 3.10+. Windows first; nothing is deliberately platform-locked but nothing else is
89
+ tested.
90
+
91
+ ### If you would rather not paste an API key
92
+
93
+ Setup offers a local model instead. If Ollama is running, everything can point at it: no
94
+ key, no network, nothing leaving the machine. Slower, and a small model struggles with the
95
+ briefing's rules, but it works end to end. See `api_key_env: ""` in the example config.
96
+
97
+ ### Configuration is optional
98
+
99
+ Every setting has a default, so there is nothing you must write:
100
+
101
+ ```
102
+ mj config what is in force right now, and which model does what
103
+ mj config --init write an annotated ~/.majordomo/config.yml to edit
104
+ mj setup go through first-run setup again
105
+ ```
106
+
107
+ API keys go in `~/.majordomo/.env`. The config file names environment *variables*, never
108
+ values, so it stays safe to commit.
109
+
110
+ ### Choosing your own models
111
+
112
+ Majordomo talks to one OpenAI-compatible `/chat/completions` endpoint, so **any** provider
113
+ speaking that shape works — OpenRouter, OpenAI, Groq, Together, vLLM, or Ollama on your own
114
+ machine. Point `brain.base_url` and `brain.api_key_env` at it:
115
+
116
+ ```yaml
117
+ brain:
118
+ provider: ollama
119
+ base_url: http://localhost:11434/v1
120
+ api_key_env: "" # empty means "needs no key" — see below
121
+ chat_model: qwen2.5:14b
122
+ agent_model: qwen2.5-coder:14b
123
+ fallback_model: "" # nothing to fall back to
124
+ ```
125
+
126
+ An **empty** `api_key_env` sends no `Authorization` header at all, which is the only
127
+ correct spelling for a local server. Naming a variable is a promise that it holds
128
+ something, so a name that is unset is still an error — right for a hosted provider, and
129
+ exactly wrong for one on your own machine.
130
+
131
+ Anthropic's own API is not OpenAI-shaped, so reach Claude models through OpenRouter
132
+ (`anthropic/claude-sonnet-4.5`) rather than pointing `base_url` at it.
133
+
134
+ There are **six model roles**, because they want genuinely different things and one model
135
+ is rarely best at all of them:
136
+
137
+ | Role | Job | Pick it for |
138
+ |---|---|---|
139
+ | `worker_model` | compress one source's raw payload | speed; the job is mechanical |
140
+ | `fuser_model` | write the four spoken sentences | **instruction-following.** It must never turn context into a task |
141
+ | `reducer_model` | handle a payload too big for the worker | a long context window |
142
+ | `chat_model` | `mj ask`, `mj chat` | consistency — you are waiting on it, so a long tail hurts more than a slow median |
143
+ | `agent_model` | `mj do`, `/agent` | tool calling and code. Empty reuses `chat_model` |
144
+ | `fallback_model` | tried once when a role's model fails | being on a *different* provider from the rest |
145
+
146
+ `fuser_model` is the one worth testing properly. It receives a list of things needing you
147
+ and a list of things that don't, and must say "nothing needs you" when the first list is
148
+ empty. A model that promotes something from the second list makes the whole tool
149
+ untrustworthy — that is the single check to run against any candidate.
150
+
151
+ **The shipped defaults will go stale.** They are OpenRouter *free* model ids, and free ids
152
+ appear and vanish. When one does, the failure says which role was pointing at it and which
153
+ line to edit rather than leaving you with a 404:
154
+
155
+ ```
156
+ No endpoints found for dots-studio/dots-3-note-preview:free
157
+ … was rejected and will keep being rejected. Free model ids come and go, so
158
+ this is usually a shipped default that has aged out rather than anything you did.
159
+ Set by:
160
+ brain.worker_model compress each source
161
+ brain.chat_model mj ask, mj chat
162
+ Edit ~/.majordomo/config.yml — `mj config` lists all six roles.
163
+ ```
164
+
165
+ Setup does not ask you to pick models. Choosing between model ids is not a question
166
+ anyone can answer in their first minute, and it is the same "know what you want before you
167
+ arrive" problem the single front door removes.
168
+
169
+ [majordomo/config.example.yml](majordomo/config.example.yml) explains every setting.
170
+
171
+ Optional extras, none required for text:
172
+
173
+ | Extra | For |
174
+ |---|---|
175
+ | `majordomo-cli[nvidia]` | speech, in and out, via NVIDIA Riva (gRPC, hence separate) |
176
+ | `majordomo-cli[voice]` | microphone capture — the one dependency speech *input* costs |
177
+ | `majordomo-cli[documents]` | text out of PDFs and Word files |
178
+ | `majordomo-cli[tray]` | the resident tray icon |
179
+
180
+ Adding one later means reinstalling with it named, e.g.
181
+ `uv tool install --force "majordomo-cli[voice]"`. Every "not installed" message tells you the line.
182
+
183
+ ## The door
184
+
185
+ ```
186
+ mj the conversation. This is the product.
187
+ mj help list these commands
188
+ ```
189
+
190
+ Piped input is answered once and exits, so the door is scriptable too:
191
+
192
+ ```
193
+ $ echo "what did I ship this week?" | mj
194
+ ```
195
+
196
+ Inside the session:
197
+
198
+ | | |
199
+ |---|---|
200
+ | `/context` | what memory and activity are loaded, and how large it has grown |
201
+ | `/clear` | start fresh, keeping the loaded context |
202
+ | `/read PATH` | hand it a document — text, PDF or Word — to talk about |
203
+ | `/remember` | propose what from this conversation is worth keeping |
204
+ | `/agent TASK` | put the agent to work without leaving the conversation |
205
+ | `/build NAME` | scaffold a repo from this conversation |
206
+ | `/voice` | speak your next message instead of typing it |
207
+ | `/help` | the list |
208
+
209
+ These are cheap to rename precisely because nobody scripts against them — which is the
210
+ other half of why there is one door.
211
+
212
+ `/read` is the one that reaches the disk from a plain conversation. Chat has no tools by
213
+ design, and this is not a hole in that: the agent is confined to a project directory
214
+ because a *model* chooses the paths, and here you typed one. What does still apply is the
215
+ credential denylist — `.env`, `.ssh/`, `*.pem` are refused, because a read means the
216
+ contents reach a model provider — along with the same 8,000-character cap the agent's
217
+ reads get, so a long document cannot quietly swallow the conversation.
218
+
219
+ The document's text is also **fenced**, and this is the part worth knowing. The agent's
220
+ file reads come back as `role: "tool"`, a channel the model knows is machine output;
221
+ `/read` has no tool call to attach to, so its text can only arrive as a user turn — the
222
+ highest-trust channel there is. But "hand it a document" usually means a document somebody
223
+ *sent* you, so its author is generally not you. The text is therefore wrapped in
224
+ `<<<DOCUMENT name>>>` … `<<<END DOCUMENT>>>`, the system prompt says that region is
225
+ material to discuss and never instructions to follow, and three markers are defanged
226
+ inside it: the closing fence (a fence a document can close is not a fence), `NEEDS_AGENT:`
227
+ (or a document picks the task you get asked to approve) and `REMEMBER:` (or it writes
228
+ itself a memory that replays forever).
229
+
230
+ ## Commands
231
+
232
+ The stable set. Scripts and the scheduled trigger depend on these, so they are the ones
233
+ that will not move:
234
+
235
+ ```
236
+ mj brief fetch, fuse, print and speak what needs you
237
+ mj ask <question> one question, answered with your context loaded
238
+ mj do <task> the agent works in the current directory
239
+ mj chat the session, with --resume and --list
240
+ mj config what is configured, and which model does what
241
+ mj setup first-run setup, again
242
+ ```
243
+
244
+ Everything else — `sessions`, `resume`, `review`, `start`, `mic`, `activity`, `remember`,
245
+ `install-hooks`, `install-trigger`, `tray` — still works exactly as before and is
246
+ documented below, but is deliberately absent from `mj --help`. Treat it as internal: it
247
+ may be renamed or moved inside the session without notice.
248
+
249
+ ### Doing things
250
+
251
+ ```
252
+ mj do <task> the built-in agent works in the current directory
253
+ --directory, -C DIR work somewhere else
254
+ --yes approve every write and command without asking
255
+ mj review <path> open Claude Code on a repo with /code-review
256
+ mj start <idea> scaffold a repo, write a brief, open Claude Code
257
+ --dry-run print what would happen, create nothing
258
+ ```
259
+
260
+ `mj do` reads, greps and lists freely; **every write, edit and command is shown and
261
+ confirmed first**, and paths are confined to the directory you launched from. Credential
262
+ files — `.env`, `.ssh/`, `*.pem` and friends — are refused outright, including reads,
263
+ because a read means the contents reach a model provider.
264
+
265
+ ### Knowing things
266
+
267
+ ```
268
+ mj brief fetch, fuse, print and speak what needs you
269
+ --no-speak print only
270
+ --explain show which model handled each source, and why
271
+ mj activity what you have been doing on GitHub
272
+ --refresh fetch new events first
273
+ --days N how far back to look
274
+ mj remember <fact> write a memory; omit the fact to list them
275
+ --update NAME replace an existing one
276
+ --forget NAME delete one
277
+ mj sessions list live / idle / blocked coding sessions
278
+ mj resume <id> jump back into a session, on the right surface
279
+ ```
280
+
281
+ ### Wiring it in
282
+
283
+ Setup offers the two that touch your machine. These are the manual equivalents:
284
+
285
+ ```
286
+ mj config what is configured, and which model does what
287
+ --init write an annotated config file to edit
288
+ mj mic measure your microphone, to tune voice input
289
+ mj install-hooks make Claude Code report session state
290
+ mj install-trigger brief automatically on wake / boot / login
291
+ mj tray the resident tray icon
292
+ ```
293
+
294
+ Each has an `uninstall-` counterpart.
295
+
296
+ ## How it is put together
297
+
298
+ Two different shapes, and the difference is the most interesting thing here.
299
+
300
+ **The agent is a loop.** It picks the next action from a tool set and keeps going until it
301
+ is done or hits a turn cap. This is the only place a model decides what happens next — and
302
+ therefore the only place with a confirmation gate. Reads run freely, writes and commands
303
+ are shown and confirmed, paths are confined, credentials are refused outright.
304
+
305
+ **The briefing is a fixed pipeline.** Workers fetch from each source, a router decides
306
+ whether a payload needs reducing first, and a fuser writes the single summary. The code
307
+ chooses every step; models only produce text. If a source fails it degrades to a plain
308
+ list rather than failing.
309
+
310
+ **A conversation is a model, and the terminal is one consumer of it.**
311
+ [`session.py`](majordomo/session.py) owns turns, compaction and the file on disk;
312
+ [`chat.py`](majordomo/chat.py) owns the loop, the prompt and the keys. They were one
313
+ module, and every bug at the seam was the two disagreeing about the same conversation —
314
+ what was stored differing from what you were shown.
315
+
316
+ ## Development
317
+
318
+ ```
319
+ pytest
320
+ ```
321
+
322
+ 1106 tests. The suite covers the safety properties directly — path confinement, the
323
+ confirmation gate, what compaction keeps — because those are the parts where being wrong
324
+ is expensive rather than merely annoying.
325
+
326
+ ## License
327
+
328
+ MIT
@@ -0,0 +1,295 @@
1
+ # Majordomo
2
+
3
+ > A majordomo is the head of a household staff who manages everyone and briefs the master.
4
+
5
+ A personal assistant that runs on your machine, knows what you have been building, and
6
+ does something about it. You talk to it, it answers with your context already loaded, and
7
+ it can act — read code, write files, run tests — asking before anything changes.
8
+
9
+ It is **not** a notification relay, and it is **not** a daemon. Your apps already ping you
10
+ "PR merged" / "new ticket". Majordomo does the opposite: it digests those streams and
11
+ surfaces only what needs a decision, when you ask it to.
12
+
13
+ ## Install, and the only command you need
14
+
15
+ ```
16
+ uv tool install majordomo-cli
17
+ mj
18
+ ```
19
+
20
+ That is the whole thing. The first `mj` configures itself — a model to talk to, and
21
+ optionally the Claude Code hooks and a spoken briefing on wake — and then drops you
22
+ straight into the conversation. Every `mj` after that just opens it, with a line telling
23
+ you what it already knows and where you left off.
24
+
25
+ ```
26
+ $ mj
27
+ Majordomo. /help for commands, `mj help` for the CLI, /exit to leave.
28
+ (6 memories indexed, 2 loaded in full, activity through 2026-09-26)
29
+ Last time: 20260926-1431 14 turns why is the fuser promoting context items
30
+
31
+ you › ▏
32
+ ```
33
+
34
+ **Why one command.** A tool has one job and a front door made of flags. An assistant is a
35
+ place you go, and its front door is itself. Sixteen subcommands meant knowing what you
36
+ wanted before you arrived — and meant sixteen names that could never change. One door is
37
+ one promise.
38
+
39
+ **Three names, and they differ on purpose.** You install `majordomo-cli`, you type `mj`,
40
+ and you import `majordomo`. `majordomo` was taken on PyPI by an unrelated package, and
41
+ plain `mj` is refused by it — untaken but not allowed, which the index will only tell you
42
+ at upload time. `[project.scripts]` is independent of all that, so the command is one
43
+ short word regardless.
44
+
45
+ `uv tool` (or `pipx`) rather than plain `pip`, because an app installed into whichever
46
+ virtualenv happened to be active disappears when you deactivate it. Plain `pip` still
47
+ works and is worth using if you want to import from this — `agent.run`, `brief.run`,
48
+ `context.build` are real library surface:
49
+
50
+ ```
51
+ pip install majordomo-cli
52
+ from majordomo import agent, brief, context
53
+ ```
54
+
55
+ Python 3.10+. Windows first; nothing is deliberately platform-locked but nothing else is
56
+ tested.
57
+
58
+ ### If you would rather not paste an API key
59
+
60
+ Setup offers a local model instead. If Ollama is running, everything can point at it: no
61
+ key, no network, nothing leaving the machine. Slower, and a small model struggles with the
62
+ briefing's rules, but it works end to end. See `api_key_env: ""` in the example config.
63
+
64
+ ### Configuration is optional
65
+
66
+ Every setting has a default, so there is nothing you must write:
67
+
68
+ ```
69
+ mj config what is in force right now, and which model does what
70
+ mj config --init write an annotated ~/.majordomo/config.yml to edit
71
+ mj setup go through first-run setup again
72
+ ```
73
+
74
+ API keys go in `~/.majordomo/.env`. The config file names environment *variables*, never
75
+ values, so it stays safe to commit.
76
+
77
+ ### Choosing your own models
78
+
79
+ Majordomo talks to one OpenAI-compatible `/chat/completions` endpoint, so **any** provider
80
+ speaking that shape works — OpenRouter, OpenAI, Groq, Together, vLLM, or Ollama on your own
81
+ machine. Point `brain.base_url` and `brain.api_key_env` at it:
82
+
83
+ ```yaml
84
+ brain:
85
+ provider: ollama
86
+ base_url: http://localhost:11434/v1
87
+ api_key_env: "" # empty means "needs no key" — see below
88
+ chat_model: qwen2.5:14b
89
+ agent_model: qwen2.5-coder:14b
90
+ fallback_model: "" # nothing to fall back to
91
+ ```
92
+
93
+ An **empty** `api_key_env` sends no `Authorization` header at all, which is the only
94
+ correct spelling for a local server. Naming a variable is a promise that it holds
95
+ something, so a name that is unset is still an error — right for a hosted provider, and
96
+ exactly wrong for one on your own machine.
97
+
98
+ Anthropic's own API is not OpenAI-shaped, so reach Claude models through OpenRouter
99
+ (`anthropic/claude-sonnet-4.5`) rather than pointing `base_url` at it.
100
+
101
+ There are **six model roles**, because they want genuinely different things and one model
102
+ is rarely best at all of them:
103
+
104
+ | Role | Job | Pick it for |
105
+ |---|---|---|
106
+ | `worker_model` | compress one source's raw payload | speed; the job is mechanical |
107
+ | `fuser_model` | write the four spoken sentences | **instruction-following.** It must never turn context into a task |
108
+ | `reducer_model` | handle a payload too big for the worker | a long context window |
109
+ | `chat_model` | `mj ask`, `mj chat` | consistency — you are waiting on it, so a long tail hurts more than a slow median |
110
+ | `agent_model` | `mj do`, `/agent` | tool calling and code. Empty reuses `chat_model` |
111
+ | `fallback_model` | tried once when a role's model fails | being on a *different* provider from the rest |
112
+
113
+ `fuser_model` is the one worth testing properly. It receives a list of things needing you
114
+ and a list of things that don't, and must say "nothing needs you" when the first list is
115
+ empty. A model that promotes something from the second list makes the whole tool
116
+ untrustworthy — that is the single check to run against any candidate.
117
+
118
+ **The shipped defaults will go stale.** They are OpenRouter *free* model ids, and free ids
119
+ appear and vanish. When one does, the failure says which role was pointing at it and which
120
+ line to edit rather than leaving you with a 404:
121
+
122
+ ```
123
+ No endpoints found for dots-studio/dots-3-note-preview:free
124
+ … was rejected and will keep being rejected. Free model ids come and go, so
125
+ this is usually a shipped default that has aged out rather than anything you did.
126
+ Set by:
127
+ brain.worker_model compress each source
128
+ brain.chat_model mj ask, mj chat
129
+ Edit ~/.majordomo/config.yml — `mj config` lists all six roles.
130
+ ```
131
+
132
+ Setup does not ask you to pick models. Choosing between model ids is not a question
133
+ anyone can answer in their first minute, and it is the same "know what you want before you
134
+ arrive" problem the single front door removes.
135
+
136
+ [majordomo/config.example.yml](majordomo/config.example.yml) explains every setting.
137
+
138
+ Optional extras, none required for text:
139
+
140
+ | Extra | For |
141
+ |---|---|
142
+ | `majordomo-cli[nvidia]` | speech, in and out, via NVIDIA Riva (gRPC, hence separate) |
143
+ | `majordomo-cli[voice]` | microphone capture — the one dependency speech *input* costs |
144
+ | `majordomo-cli[documents]` | text out of PDFs and Word files |
145
+ | `majordomo-cli[tray]` | the resident tray icon |
146
+
147
+ Adding one later means reinstalling with it named, e.g.
148
+ `uv tool install --force "majordomo-cli[voice]"`. Every "not installed" message tells you the line.
149
+
150
+ ## The door
151
+
152
+ ```
153
+ mj the conversation. This is the product.
154
+ mj help list these commands
155
+ ```
156
+
157
+ Piped input is answered once and exits, so the door is scriptable too:
158
+
159
+ ```
160
+ $ echo "what did I ship this week?" | mj
161
+ ```
162
+
163
+ Inside the session:
164
+
165
+ | | |
166
+ |---|---|
167
+ | `/context` | what memory and activity are loaded, and how large it has grown |
168
+ | `/clear` | start fresh, keeping the loaded context |
169
+ | `/read PATH` | hand it a document — text, PDF or Word — to talk about |
170
+ | `/remember` | propose what from this conversation is worth keeping |
171
+ | `/agent TASK` | put the agent to work without leaving the conversation |
172
+ | `/build NAME` | scaffold a repo from this conversation |
173
+ | `/voice` | speak your next message instead of typing it |
174
+ | `/help` | the list |
175
+
176
+ These are cheap to rename precisely because nobody scripts against them — which is the
177
+ other half of why there is one door.
178
+
179
+ `/read` is the one that reaches the disk from a plain conversation. Chat has no tools by
180
+ design, and this is not a hole in that: the agent is confined to a project directory
181
+ because a *model* chooses the paths, and here you typed one. What does still apply is the
182
+ credential denylist — `.env`, `.ssh/`, `*.pem` are refused, because a read means the
183
+ contents reach a model provider — along with the same 8,000-character cap the agent's
184
+ reads get, so a long document cannot quietly swallow the conversation.
185
+
186
+ The document's text is also **fenced**, and this is the part worth knowing. The agent's
187
+ file reads come back as `role: "tool"`, a channel the model knows is machine output;
188
+ `/read` has no tool call to attach to, so its text can only arrive as a user turn — the
189
+ highest-trust channel there is. But "hand it a document" usually means a document somebody
190
+ *sent* you, so its author is generally not you. The text is therefore wrapped in
191
+ `<<<DOCUMENT name>>>` … `<<<END DOCUMENT>>>`, the system prompt says that region is
192
+ material to discuss and never instructions to follow, and three markers are defanged
193
+ inside it: the closing fence (a fence a document can close is not a fence), `NEEDS_AGENT:`
194
+ (or a document picks the task you get asked to approve) and `REMEMBER:` (or it writes
195
+ itself a memory that replays forever).
196
+
197
+ ## Commands
198
+
199
+ The stable set. Scripts and the scheduled trigger depend on these, so they are the ones
200
+ that will not move:
201
+
202
+ ```
203
+ mj brief fetch, fuse, print and speak what needs you
204
+ mj ask <question> one question, answered with your context loaded
205
+ mj do <task> the agent works in the current directory
206
+ mj chat the session, with --resume and --list
207
+ mj config what is configured, and which model does what
208
+ mj setup first-run setup, again
209
+ ```
210
+
211
+ Everything else — `sessions`, `resume`, `review`, `start`, `mic`, `activity`, `remember`,
212
+ `install-hooks`, `install-trigger`, `tray` — still works exactly as before and is
213
+ documented below, but is deliberately absent from `mj --help`. Treat it as internal: it
214
+ may be renamed or moved inside the session without notice.
215
+
216
+ ### Doing things
217
+
218
+ ```
219
+ mj do <task> the built-in agent works in the current directory
220
+ --directory, -C DIR work somewhere else
221
+ --yes approve every write and command without asking
222
+ mj review <path> open Claude Code on a repo with /code-review
223
+ mj start <idea> scaffold a repo, write a brief, open Claude Code
224
+ --dry-run print what would happen, create nothing
225
+ ```
226
+
227
+ `mj do` reads, greps and lists freely; **every write, edit and command is shown and
228
+ confirmed first**, and paths are confined to the directory you launched from. Credential
229
+ files — `.env`, `.ssh/`, `*.pem` and friends — are refused outright, including reads,
230
+ because a read means the contents reach a model provider.
231
+
232
+ ### Knowing things
233
+
234
+ ```
235
+ mj brief fetch, fuse, print and speak what needs you
236
+ --no-speak print only
237
+ --explain show which model handled each source, and why
238
+ mj activity what you have been doing on GitHub
239
+ --refresh fetch new events first
240
+ --days N how far back to look
241
+ mj remember <fact> write a memory; omit the fact to list them
242
+ --update NAME replace an existing one
243
+ --forget NAME delete one
244
+ mj sessions list live / idle / blocked coding sessions
245
+ mj resume <id> jump back into a session, on the right surface
246
+ ```
247
+
248
+ ### Wiring it in
249
+
250
+ Setup offers the two that touch your machine. These are the manual equivalents:
251
+
252
+ ```
253
+ mj config what is configured, and which model does what
254
+ --init write an annotated config file to edit
255
+ mj mic measure your microphone, to tune voice input
256
+ mj install-hooks make Claude Code report session state
257
+ mj install-trigger brief automatically on wake / boot / login
258
+ mj tray the resident tray icon
259
+ ```
260
+
261
+ Each has an `uninstall-` counterpart.
262
+
263
+ ## How it is put together
264
+
265
+ Two different shapes, and the difference is the most interesting thing here.
266
+
267
+ **The agent is a loop.** It picks the next action from a tool set and keeps going until it
268
+ is done or hits a turn cap. This is the only place a model decides what happens next — and
269
+ therefore the only place with a confirmation gate. Reads run freely, writes and commands
270
+ are shown and confirmed, paths are confined, credentials are refused outright.
271
+
272
+ **The briefing is a fixed pipeline.** Workers fetch from each source, a router decides
273
+ whether a payload needs reducing first, and a fuser writes the single summary. The code
274
+ chooses every step; models only produce text. If a source fails it degrades to a plain
275
+ list rather than failing.
276
+
277
+ **A conversation is a model, and the terminal is one consumer of it.**
278
+ [`session.py`](majordomo/session.py) owns turns, compaction and the file on disk;
279
+ [`chat.py`](majordomo/chat.py) owns the loop, the prompt and the keys. They were one
280
+ module, and every bug at the seam was the two disagreeing about the same conversation —
281
+ what was stored differing from what you were shown.
282
+
283
+ ## Development
284
+
285
+ ```
286
+ pytest
287
+ ```
288
+
289
+ 1106 tests. The suite covers the safety properties directly — path confinement, the
290
+ confirmation gate, what compaction keeps — because those are the parts where being wrong
291
+ is expensive rather than merely annoying.
292
+
293
+ ## License
294
+
295
+ MIT
@@ -0,0 +1,30 @@
1
+ """Majordomo — the head of household staff who runs the workers and briefs you."""
2
+
3
+ #: Single source of truth for the version. ``pyproject.toml`` reads this
4
+ #: attribute rather than carrying its own literal, so the two cannot disagree.
5
+ __version__ = "0.1.0"
6
+
7
+ #: The distribution name, which is **not** the name of this package and **not**
8
+ #: the name of the command either. `majordomo` is taken on PyPI; `mj` is untaken
9
+ #: but rejected by it ("The name 'mj' isn't allowed" — too short or too close to
10
+ #: something else, a rule the JSON API cannot be asked about). The command stays
11
+ #: `mj`, because `[project.scripts]` is independent of all this.
12
+ DISTRIBUTION = "majordomo-cli"
13
+
14
+
15
+ def install_hint(extra: str) -> str:
16
+ """How to add an optional extra, for whichever way this was installed.
17
+
18
+ Lives here, once, because four modules need to say it — ``asr``,
19
+ ``tts``, ``documents``, ``tray`` — and each had its own copy naming
20
+ ``pip install majordomo[...]``. Both halves of that are now wrong: the
21
+ distribution is ``mj``, and the recommended install is ``uv tool``, which
22
+ puts the package in an isolated environment where a plain ``pip install``
23
+ lands somewhere else entirely and appears to do nothing.
24
+
25
+ Both forms are named because we cannot tell from in here which one applies,
26
+ and guessing wrong wastes the reader's time on the one line that was supposed
27
+ to save it.
28
+ """
29
+ spec = f'"{DISTRIBUTION}[{extra}]"'
30
+ return f"uv tool install --force {spec} (or, in a venv: pip install {spec})"