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.
- majordomo_cli-0.1.0/PKG-INFO +328 -0
- majordomo_cli-0.1.0/README.md +295 -0
- majordomo_cli-0.1.0/majordomo/__init__.py +30 -0
- majordomo_cli-0.1.0/majordomo/activity.py +669 -0
- majordomo_cli-0.1.0/majordomo/agent.py +217 -0
- majordomo_cli-0.1.0/majordomo/asr.py +396 -0
- majordomo_cli-0.1.0/majordomo/brief.py +83 -0
- majordomo_cli-0.1.0/majordomo/chat.py +768 -0
- majordomo_cli-0.1.0/majordomo/cli.py +1446 -0
- majordomo_cli-0.1.0/majordomo/config.example.yml +229 -0
- majordomo_cli-0.1.0/majordomo/config.py +561 -0
- majordomo_cli-0.1.0/majordomo/context.py +139 -0
- majordomo_cli-0.1.0/majordomo/coordinator.py +255 -0
- majordomo_cli-0.1.0/majordomo/documents.py +165 -0
- majordomo_cli-0.1.0/majordomo/dotenv.py +93 -0
- majordomo_cli-0.1.0/majordomo/firstrun.py +416 -0
- majordomo_cli-0.1.0/majordomo/hook.py +186 -0
- majordomo_cli-0.1.0/majordomo/install.py +229 -0
- majordomo_cli-0.1.0/majordomo/jsonlog.py +50 -0
- majordomo_cli-0.1.0/majordomo/keys.py +321 -0
- majordomo_cli-0.1.0/majordomo/llm.py +680 -0
- majordomo_cli-0.1.0/majordomo/memory.py +530 -0
- majordomo_cli-0.1.0/majordomo/models.py +186 -0
- majordomo_cli-0.1.0/majordomo/panel.py +282 -0
- majordomo_cli-0.1.0/majordomo/paths.py +79 -0
- majordomo_cli-0.1.0/majordomo/prompts.py +672 -0
- majordomo_cli-0.1.0/majordomo/render.py +182 -0
- majordomo_cli-0.1.0/majordomo/resume.py +102 -0
- majordomo_cli-0.1.0/majordomo/router.py +82 -0
- majordomo_cli-0.1.0/majordomo/scaffold.py +233 -0
- majordomo_cli-0.1.0/majordomo/session.py +471 -0
- majordomo_cli-0.1.0/majordomo/speechgate.py +100 -0
- majordomo_cli-0.1.0/majordomo/state.py +223 -0
- majordomo_cli-0.1.0/majordomo/tools.py +668 -0
- majordomo_cli-0.1.0/majordomo/tray.py +84 -0
- majordomo_cli-0.1.0/majordomo/trigger.py +235 -0
- majordomo_cli-0.1.0/majordomo/tts.py +353 -0
- majordomo_cli-0.1.0/majordomo/workers/__init__.py +41 -0
- majordomo_cli-0.1.0/majordomo/workers/github.py +232 -0
- majordomo_cli-0.1.0/majordomo/workers/gmail.py +328 -0
- majordomo_cli-0.1.0/majordomo/workers/sessions.py +197 -0
- majordomo_cli-0.1.0/majordomo_cli.egg-info/PKG-INFO +328 -0
- majordomo_cli-0.1.0/majordomo_cli.egg-info/SOURCES.txt +74 -0
- majordomo_cli-0.1.0/majordomo_cli.egg-info/dependency_links.txt +1 -0
- majordomo_cli-0.1.0/majordomo_cli.egg-info/entry_points.txt +2 -0
- majordomo_cli-0.1.0/majordomo_cli.egg-info/requires.txt +19 -0
- majordomo_cli-0.1.0/majordomo_cli.egg-info/top_level.txt +1 -0
- majordomo_cli-0.1.0/pyproject.toml +79 -0
- majordomo_cli-0.1.0/setup.cfg +4 -0
- majordomo_cli-0.1.0/tests/test_activity.py +774 -0
- majordomo_cli-0.1.0/tests/test_agent_and_tools.py +752 -0
- majordomo_cli-0.1.0/tests/test_asr_and_keys.py +815 -0
- majordomo_cli-0.1.0/tests/test_brief_and_cli.py +745 -0
- majordomo_cli-0.1.0/tests/test_chat_and_context.py +1982 -0
- majordomo_cli-0.1.0/tests/test_cli_assistant.py +830 -0
- majordomo_cli-0.1.0/tests/test_config.py +193 -0
- majordomo_cli-0.1.0/tests/test_coordinator.py +581 -0
- majordomo_cli-0.1.0/tests/test_documents.py +307 -0
- majordomo_cli-0.1.0/tests/test_dotenv.py +104 -0
- majordomo_cli-0.1.0/tests/test_firstrun.py +505 -0
- majordomo_cli-0.1.0/tests/test_hook.py +273 -0
- majordomo_cli-0.1.0/tests/test_install.py +249 -0
- majordomo_cli-0.1.0/tests/test_jsonlog.py +107 -0
- majordomo_cli-0.1.0/tests/test_llm.py +1032 -0
- majordomo_cli-0.1.0/tests/test_memory.py +374 -0
- majordomo_cli-0.1.0/tests/test_panel.py +261 -0
- majordomo_cli-0.1.0/tests/test_render.py +253 -0
- majordomo_cli-0.1.0/tests/test_resume.py +98 -0
- majordomo_cli-0.1.0/tests/test_router.py +70 -0
- majordomo_cli-0.1.0/tests/test_scaffold.py +75 -0
- majordomo_cli-0.1.0/tests/test_speechgate.py +111 -0
- majordomo_cli-0.1.0/tests/test_state.py +280 -0
- majordomo_cli-0.1.0/tests/test_tts.py +447 -0
- majordomo_cli-0.1.0/tests/test_workers_github.py +345 -0
- majordomo_cli-0.1.0/tests/test_workers_gmail.py +332 -0
- 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})"
|