sessionkit 0.2.0__tar.gz → 0.3.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.
@@ -0,0 +1,233 @@
1
+ Metadata-Version: 2.4
2
+ Name: sessionkit
3
+ Version: 0.3.0
4
+ Summary: Session Kit gives your Claude Code sessions voice, mail, a phone line and a tmux sidebar, in one install.
5
+ Author: Mike Bailey
6
+ Author-email: Mike Bailey <mike@sessionkit.ai>
7
+ License-Expression: AGPL-3.0-or-later
8
+ License-File: LICENSE
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Requires-Dist: click>=8.1
11
+ Requires-Dist: sessionmail>=0.3.0
12
+ Requires-Dist: sessiontools>=0.3.0
13
+ Requires-Dist: sessionphone[media]>=0.3.0
14
+ Requires-Dist: sessionview>=0.3.0
15
+ Requires-Dist: sessionvoice[capture]>=0.3.0
16
+ Requires-Python: >=3.11, <3.15
17
+ Project-URL: Homepage, https://github.com/sessionkit/sessionkit
18
+ Project-URL: Repository, https://github.com/sessionkit/sessionkit
19
+ Description-Content-Type: text/markdown
20
+
21
+ # Session Kit
22
+
23
+ Session Kit lets your Claude Code sessions mail each other, hear you and
24
+ answer aloud. One install brings the five family packages and their Claude
25
+ Code plugins.
26
+
27
+ ## Installation
28
+
29
+ You need macOS, [Claude Code][claude-code], and [Homebrew][brew] or at
30
+ least Apple's Command Line Tools. Homebrew installs the Command Line Tools
31
+ for you ([prerequisites][prereqs]), and the kit needs two of their tools:
32
+ the plugin lines clone with git, and the ears build a Swift helper with
33
+ `swiftc`. Without Homebrew, run `xcode-select --install` to get the Command
34
+ Line Tools alone. If you do not have uv yet, install it with
35
+ `curl -LsSf https://astral.sh/uv/install.sh | sh`, then open a new terminal
36
+ (or run `source ~/.local/bin/env`).
37
+
38
+ ```bash
39
+ uv tool install --python 3.14 sessionkit
40
+ claude plugin marketplace add sessionkit/sessionkit
41
+ claude plugin install sessionkit@sessionkit
42
+ ```
43
+
44
+ The first line puts the commands on your PATH. The other two install the
45
+ kit's plugin and the five family plugins it depends on.
46
+
47
+ - **Keep `--python 3.14`.** Without it, uv takes the Python it finds: 3.15,
48
+ where a voice library has no build yet and the install fails, or the
49
+ Command Line Tools' 3.11. The kit is tested on 3.14.
50
+ - **Upgrade with `uv tool upgrade sessionkit`.** It keeps the Python the
51
+ install chose. To reinstall, give the pin again:
52
+ `uv tool install --python 3.14 --reinstall sessionkit`. Without it, uv
53
+ drops the pin from its record, or, with `--force`, builds the kit again on
54
+ the Python it finds.
55
+ - **A dialog may ask to install the Command Line Tools.** On a Mac without
56
+ them, the first `uv tool install` opens it. Click Install and let it
57
+ finish: the plugin lines need the git it brings.
58
+ - **Phone calls are installed, and walked for a session started with
59
+ `session start <name>`.** A fresh Mac took a real call and passed. A plain
60
+ `claude` session does not hear calls you make to it yet. Whether the phone
61
+ bot signs its mail to your Claude follows your mail carrier:
62
+ [bot mail signing][phone-signing].
63
+
64
+ **Then quit Claude Code and start it again**, every window of it, before you
65
+ run anything else. A plugin installed from the shell loads only when Claude
66
+ Code starts, so a session that was already open has no plugin commands on
67
+ its PATH, and no plugin hooks.
68
+
69
+ The three lines leave the ears without the model they listen with. Fetch it
70
+ once:
71
+
72
+ ```bash
73
+ ears install
74
+ ```
75
+
76
+ It downloads the 2.3 MB voice-activity model to
77
+ `~/.cache/ears/silero_vad.onnx` (network, no sudo) and prints `done`. Without
78
+ it the ears cannot hear you, and `ears services install`, the step that
79
+ starts them ([hearing and speaking][hearing]), refuses with
80
+ `silero MISSING`. The Swift helper is built later, when those services start.
81
+
82
+ - **The mail hooks start off.** They tell an agent about new mail between
83
+ its tool calls. To turn them on, set `"hooks": true` in
84
+ `~/.sessionkit/mail/settings/mail.json`. On a fresh install the file is
85
+ not there yet, so make it:
86
+
87
+ ```bash
88
+ mkdir -p ~/.sessionkit/mail/settings
89
+ echo '{"version": 1, "hooks": true}' > ~/.sessionkit/mail/settings/mail.json
90
+ ```
91
+
92
+ If the file is there, change only that key, because it can hold other
93
+ settings:
94
+
95
+ ```bash
96
+ perl -pi -e 's/"hooks": *false/"hooks": true/' ~/.sessionkit/mail/settings/mail.json
97
+ ```
98
+
99
+ Step by step, with every step that needs you marked: [the manual
100
+ install][install].
101
+
102
+ ## Quick start
103
+
104
+ ```bash
105
+ sessionkit --version
106
+ ```
107
+
108
+ It lists the kit and each family package, with its version:
109
+
110
+ ```text
111
+ sessionkit, version 0.3.0
112
+ sessionmail 0.3.0
113
+ sessiontools 0.3.0
114
+ sessionphone 0.3.0
115
+ sessionview 0.3.0
116
+ sessionvoice 0.3.0
117
+ ```
118
+
119
+ Start Claude with `session start claude`, never a bare `claude`: mail to
120
+ your agent's box, your phone's included, reaches only a session started as
121
+ that agent. `sessionkit setup` makes the box and the agent first: [first
122
+ checks][checks].
123
+
124
+ Then, from your agent inside a Claude Code session:
125
+
126
+ ```bash
127
+ session list # your live sessions
128
+ message send <box> -s "PR ready" -f note.md # mail another agent
129
+ mouth say --device default "the build is green" # speak one line
130
+ ```
131
+
132
+ - **Mail works inside a session.** `message` and `pager` read who is asking
133
+ from the session. A plain terminal has no session, so they refuse with
134
+ `no seat in the environment ... set an identity` ([first checks][checks]).
135
+ A send never makes a box: the address must have one already ([mail
136
+ between agents][mail]).
137
+ - **The mouth needs a device.** `--device default` is your Mac's default
138
+ output; `mouth devices` lists the others.
139
+ - **To hear you**, the ears need a speech-to-text server: [hearing and
140
+ speaking][hearing].
141
+ - **Your agent talks with the kit's mouth and ears.** sessionvoice's ears
142
+ skill sets the default:
143
+
144
+ > **With the kit, an agent talks through the kit's own mouth and ears: it
145
+ > speaks with `mouth say` and listens by arming the ears with `ears-arm`. That
146
+ > is the default whenever the user asks it to talk, speak, listen or use voice,
147
+ > even on a host that also has VoiceMode's `converse` tool. It uses `converse`
148
+ > only when the user asks for converse by name.**
149
+
150
+ ## Uninstall
151
+
152
+ ```bash
153
+ claude plugin marketplace remove sessionkit
154
+ uv tool uninstall sessionkit
155
+ ```
156
+
157
+ Removing the marketplace removes its six plugins too, but not the copies they
158
+ ran from. Those copies, the services and your data have their own steps:
159
+ [uninstall][uninstall].
160
+
161
+ ## Usage
162
+
163
+ Every command answers `--help`, and each has a page: [components][components].
164
+
165
+ ## The family
166
+
167
+ Session Kit installs these together:
168
+
169
+ - [sessionmail][pkg-mail]: mail for coding agents. `message`, `pager` and
170
+ `memory` over one maildir.
171
+ - [sessiontools][pkg-tools]: sessions, status and idle. `session`.
172
+ - [sessionvoice][pkg-voice]: ambient voice. `ears` hears; `mouth` speaks.
173
+ - [sessionphone][pkg-phone]: phone calls to a session over Delta Chat.
174
+ Installed, and walked on a fresh Mac for a session started with
175
+ `session start <name>`. A plain `claude` session does not hear calls you
176
+ make to it yet.
177
+ - [sessionview][pkg-view]: a tmux sidebar of your projects and your agents.
178
+
179
+ ## Components
180
+
181
+ The commands, services and folders the install adds, and where each
182
+ package logs: [components][components].
183
+
184
+ ## Configuration
185
+
186
+ Each package configures itself: [configuration][configuration]. How the
187
+ settings files stack: [the settings guide][settings].
188
+
189
+ ## Logs
190
+
191
+ Each package keeps its own log: [logs][logs]. Which one to read for a
192
+ symptom: [troubleshooting][troubleshooting].
193
+
194
+ ## Development
195
+
196
+ ```bash
197
+ git clone https://github.com/sessionkit/sessionkit
198
+ cd sessionkit
199
+ make install # editable: uv tool install -e .
200
+ make check # ruff, shellcheck, then the tests
201
+ ```
202
+
203
+ The rules for changing the kit: [AGENTS.md][agents].
204
+
205
+ ## Status
206
+
207
+ Alpha, 0.3.0. Tested on macOS 27 on Apple silicon; Linux and Windows are
208
+ untested. What you may still hit: [known gaps][gaps].
209
+
210
+ Licence: [AGPL-3.0][licence].
211
+
212
+ [claude-code]: https://claude.ai/code
213
+ [brew]: https://brew.sh
214
+ [install]: INSTALL.md
215
+ [prereqs]: INSTALL.md#1-prerequisites-human
216
+ [checks]: INSTALL.md#4-first-checks
217
+ [hearing]: INSTALL.md#5-hearing-and-speaking-it-hears-me-and-speaks
218
+ [mail]: INSTALL.md#mail-between-agents
219
+ [uninstall]: INSTALL.md#8-uninstall
220
+ [gaps]: INSTALL.md#7-known-gaps-you-will-hit
221
+ [components]: docs/components.md
222
+ [logs]: docs/components.md#logs
223
+ [configuration]: docs/configuration.md
224
+ [settings]: docs/settings.md
225
+ [troubleshooting]: docs/troubleshooting.md
226
+ [agents]: AGENTS.md
227
+ [licence]: LICENSE
228
+ [pkg-mail]: https://github.com/sessionkit/sessionmail
229
+ [pkg-tools]: https://github.com/sessionkit/sessiontools
230
+ [pkg-voice]: https://github.com/sessionkit/sessionvoice
231
+ [pkg-phone]: https://github.com/sessionkit/sessionphone
232
+ [phone-signing]: https://github.com/sessionkit/sessionphone/blob/HEAD/docs/bot-mail-signing.md
233
+ [pkg-view]: https://github.com/sessionkit/sessionview
@@ -0,0 +1,213 @@
1
+ # Session Kit
2
+
3
+ Session Kit lets your Claude Code sessions mail each other, hear you and
4
+ answer aloud. One install brings the five family packages and their Claude
5
+ Code plugins.
6
+
7
+ ## Installation
8
+
9
+ You need macOS, [Claude Code][claude-code], and [Homebrew][brew] or at
10
+ least Apple's Command Line Tools. Homebrew installs the Command Line Tools
11
+ for you ([prerequisites][prereqs]), and the kit needs two of their tools:
12
+ the plugin lines clone with git, and the ears build a Swift helper with
13
+ `swiftc`. Without Homebrew, run `xcode-select --install` to get the Command
14
+ Line Tools alone. If you do not have uv yet, install it with
15
+ `curl -LsSf https://astral.sh/uv/install.sh | sh`, then open a new terminal
16
+ (or run `source ~/.local/bin/env`).
17
+
18
+ ```bash
19
+ uv tool install --python 3.14 sessionkit
20
+ claude plugin marketplace add sessionkit/sessionkit
21
+ claude plugin install sessionkit@sessionkit
22
+ ```
23
+
24
+ The first line puts the commands on your PATH. The other two install the
25
+ kit's plugin and the five family plugins it depends on.
26
+
27
+ - **Keep `--python 3.14`.** Without it, uv takes the Python it finds: 3.15,
28
+ where a voice library has no build yet and the install fails, or the
29
+ Command Line Tools' 3.11. The kit is tested on 3.14.
30
+ - **Upgrade with `uv tool upgrade sessionkit`.** It keeps the Python the
31
+ install chose. To reinstall, give the pin again:
32
+ `uv tool install --python 3.14 --reinstall sessionkit`. Without it, uv
33
+ drops the pin from its record, or, with `--force`, builds the kit again on
34
+ the Python it finds.
35
+ - **A dialog may ask to install the Command Line Tools.** On a Mac without
36
+ them, the first `uv tool install` opens it. Click Install and let it
37
+ finish: the plugin lines need the git it brings.
38
+ - **Phone calls are installed, and walked for a session started with
39
+ `session start <name>`.** A fresh Mac took a real call and passed. A plain
40
+ `claude` session does not hear calls you make to it yet. Whether the phone
41
+ bot signs its mail to your Claude follows your mail carrier:
42
+ [bot mail signing][phone-signing].
43
+
44
+ **Then quit Claude Code and start it again**, every window of it, before you
45
+ run anything else. A plugin installed from the shell loads only when Claude
46
+ Code starts, so a session that was already open has no plugin commands on
47
+ its PATH, and no plugin hooks.
48
+
49
+ The three lines leave the ears without the model they listen with. Fetch it
50
+ once:
51
+
52
+ ```bash
53
+ ears install
54
+ ```
55
+
56
+ It downloads the 2.3 MB voice-activity model to
57
+ `~/.cache/ears/silero_vad.onnx` (network, no sudo) and prints `done`. Without
58
+ it the ears cannot hear you, and `ears services install`, the step that
59
+ starts them ([hearing and speaking][hearing]), refuses with
60
+ `silero MISSING`. The Swift helper is built later, when those services start.
61
+
62
+ - **The mail hooks start off.** They tell an agent about new mail between
63
+ its tool calls. To turn them on, set `"hooks": true` in
64
+ `~/.sessionkit/mail/settings/mail.json`. On a fresh install the file is
65
+ not there yet, so make it:
66
+
67
+ ```bash
68
+ mkdir -p ~/.sessionkit/mail/settings
69
+ echo '{"version": 1, "hooks": true}' > ~/.sessionkit/mail/settings/mail.json
70
+ ```
71
+
72
+ If the file is there, change only that key, because it can hold other
73
+ settings:
74
+
75
+ ```bash
76
+ perl -pi -e 's/"hooks": *false/"hooks": true/' ~/.sessionkit/mail/settings/mail.json
77
+ ```
78
+
79
+ Step by step, with every step that needs you marked: [the manual
80
+ install][install].
81
+
82
+ ## Quick start
83
+
84
+ ```bash
85
+ sessionkit --version
86
+ ```
87
+
88
+ It lists the kit and each family package, with its version:
89
+
90
+ ```text
91
+ sessionkit, version 0.3.0
92
+ sessionmail 0.3.0
93
+ sessiontools 0.3.0
94
+ sessionphone 0.3.0
95
+ sessionview 0.3.0
96
+ sessionvoice 0.3.0
97
+ ```
98
+
99
+ Start Claude with `session start claude`, never a bare `claude`: mail to
100
+ your agent's box, your phone's included, reaches only a session started as
101
+ that agent. `sessionkit setup` makes the box and the agent first: [first
102
+ checks][checks].
103
+
104
+ Then, from your agent inside a Claude Code session:
105
+
106
+ ```bash
107
+ session list # your live sessions
108
+ message send <box> -s "PR ready" -f note.md # mail another agent
109
+ mouth say --device default "the build is green" # speak one line
110
+ ```
111
+
112
+ - **Mail works inside a session.** `message` and `pager` read who is asking
113
+ from the session. A plain terminal has no session, so they refuse with
114
+ `no seat in the environment ... set an identity` ([first checks][checks]).
115
+ A send never makes a box: the address must have one already ([mail
116
+ between agents][mail]).
117
+ - **The mouth needs a device.** `--device default` is your Mac's default
118
+ output; `mouth devices` lists the others.
119
+ - **To hear you**, the ears need a speech-to-text server: [hearing and
120
+ speaking][hearing].
121
+ - **Your agent talks with the kit's mouth and ears.** sessionvoice's ears
122
+ skill sets the default:
123
+
124
+ > **With the kit, an agent talks through the kit's own mouth and ears: it
125
+ > speaks with `mouth say` and listens by arming the ears with `ears-arm`. That
126
+ > is the default whenever the user asks it to talk, speak, listen or use voice,
127
+ > even on a host that also has VoiceMode's `converse` tool. It uses `converse`
128
+ > only when the user asks for converse by name.**
129
+
130
+ ## Uninstall
131
+
132
+ ```bash
133
+ claude plugin marketplace remove sessionkit
134
+ uv tool uninstall sessionkit
135
+ ```
136
+
137
+ Removing the marketplace removes its six plugins too, but not the copies they
138
+ ran from. Those copies, the services and your data have their own steps:
139
+ [uninstall][uninstall].
140
+
141
+ ## Usage
142
+
143
+ Every command answers `--help`, and each has a page: [components][components].
144
+
145
+ ## The family
146
+
147
+ Session Kit installs these together:
148
+
149
+ - [sessionmail][pkg-mail]: mail for coding agents. `message`, `pager` and
150
+ `memory` over one maildir.
151
+ - [sessiontools][pkg-tools]: sessions, status and idle. `session`.
152
+ - [sessionvoice][pkg-voice]: ambient voice. `ears` hears; `mouth` speaks.
153
+ - [sessionphone][pkg-phone]: phone calls to a session over Delta Chat.
154
+ Installed, and walked on a fresh Mac for a session started with
155
+ `session start <name>`. A plain `claude` session does not hear calls you
156
+ make to it yet.
157
+ - [sessionview][pkg-view]: a tmux sidebar of your projects and your agents.
158
+
159
+ ## Components
160
+
161
+ The commands, services and folders the install adds, and where each
162
+ package logs: [components][components].
163
+
164
+ ## Configuration
165
+
166
+ Each package configures itself: [configuration][configuration]. How the
167
+ settings files stack: [the settings guide][settings].
168
+
169
+ ## Logs
170
+
171
+ Each package keeps its own log: [logs][logs]. Which one to read for a
172
+ symptom: [troubleshooting][troubleshooting].
173
+
174
+ ## Development
175
+
176
+ ```bash
177
+ git clone https://github.com/sessionkit/sessionkit
178
+ cd sessionkit
179
+ make install # editable: uv tool install -e .
180
+ make check # ruff, shellcheck, then the tests
181
+ ```
182
+
183
+ The rules for changing the kit: [AGENTS.md][agents].
184
+
185
+ ## Status
186
+
187
+ Alpha, 0.3.0. Tested on macOS 27 on Apple silicon; Linux and Windows are
188
+ untested. What you may still hit: [known gaps][gaps].
189
+
190
+ Licence: [AGPL-3.0][licence].
191
+
192
+ [claude-code]: https://claude.ai/code
193
+ [brew]: https://brew.sh
194
+ [install]: INSTALL.md
195
+ [prereqs]: INSTALL.md#1-prerequisites-human
196
+ [checks]: INSTALL.md#4-first-checks
197
+ [hearing]: INSTALL.md#5-hearing-and-speaking-it-hears-me-and-speaks
198
+ [mail]: INSTALL.md#mail-between-agents
199
+ [uninstall]: INSTALL.md#8-uninstall
200
+ [gaps]: INSTALL.md#7-known-gaps-you-will-hit
201
+ [components]: docs/components.md
202
+ [logs]: docs/components.md#logs
203
+ [configuration]: docs/configuration.md
204
+ [settings]: docs/settings.md
205
+ [troubleshooting]: docs/troubleshooting.md
206
+ [agents]: AGENTS.md
207
+ [licence]: LICENSE
208
+ [pkg-mail]: https://github.com/sessionkit/sessionmail
209
+ [pkg-tools]: https://github.com/sessionkit/sessiontools
210
+ [pkg-voice]: https://github.com/sessionkit/sessionvoice
211
+ [pkg-phone]: https://github.com/sessionkit/sessionphone
212
+ [phone-signing]: https://github.com/sessionkit/sessionphone/blob/HEAD/docs/bot-mail-signing.md
213
+ [pkg-view]: https://github.com/sessionkit/sessionview
@@ -1,7 +1,8 @@
1
1
  [project]
2
2
  name = "sessionkit"
3
- version = "0.2.0"
3
+ version = "0.3.0"
4
4
  description = "Session Kit gives your Claude Code sessions voice, mail, a phone line and a tmux sidebar, in one install."
5
+ readme = "README.md"
5
6
  license = "AGPL-3.0-or-later"
6
7
  license-files = ["LICENSE"]
7
8
  classifiers = ["Development Status :: 3 - Alpha"]
@@ -9,16 +10,22 @@ authors = [
9
10
  { name = "Mike Bailey", email = "mike@sessionkit.ai" }
10
11
  ]
11
12
  requires-python = ">=3.11,<3.15"
12
- # The family, one install. Floored at 0.1.0: every family package is alpha at 0.1.0.
13
- # PyPI's 0.0.1 name placeholders must never satisfy these.
13
+ # The family, one install. Floored at 0.3.0: the kit is released and tested
14
+ # with the family's 0.3.0, so it needs every family package at 0.3.0 or
15
+ # later, and PyPI's 0.0.1 name placeholders and the 0.1.0 alphas must never
16
+ # satisfy these.
17
+ # sessionphone with its media extra: the bot, calls and `sessionphone web` need it.
14
18
  # sessionvoice with its capture extra, so the ears can hear the room.
19
+ # This is the RULED list, kept in tests/_ruled_deps.py with the ruling;
20
+ # tests/test_dependency_gate.py fails when these lines, the built wheel or
21
+ # uv.lock drift from it.
15
22
  dependencies = [
16
23
  "click>=8.1",
17
- "sessionmail>=0.1.0",
18
- "sessiontools>=0.1.0",
19
- "sessionphone>=0.1.0",
20
- "sessionview>=0.1.0",
21
- "sessionvoice[capture]>=0.1.0",
24
+ "sessionmail>=0.3.0",
25
+ "sessiontools>=0.3.0",
26
+ "sessionphone[media]>=0.3.0",
27
+ "sessionview>=0.3.0",
28
+ "sessionvoice[capture]>=0.3.0",
22
29
  ]
23
30
 
24
31
  [project.urls]
@@ -31,8 +38,8 @@ Repository = "https://github.com/sessionkit/sessionkit"
31
38
  # `uv tool install sessionkit` would put `sessionkit` alone on the PATH.
32
39
  # Each entry point is the child's own target, copied verbatim; re-measure them
33
40
  # at each child's HEAD whenever a child adds, renames or drops a command.
34
- # Measured from each child's built wheel (entry_points.txt) at mail 471a6f7,
35
- # tools ab62717, phone 02d8cf7, view 3fada19, voice eda3f94.
41
+ # Measured from each child's 0.3.0 wheel (entry_points.txt), by wheel sha256:
42
+ # mail 8e4d9476, tools 8d089699, phone ac402a39, view 3942f119, voice ebc1b65c.
36
43
  # sessionview's four commands share one target: `sessionview:main` reads the
37
44
  # name it was run as and runs that command.
38
45
  # The kit itself still ships no `session` of its own: `session` below is
@@ -90,13 +90,22 @@ def _shape(check):
90
90
  help="Your own full mail address (name@host), written as the operator "
91
91
  "instead of a mail box.",
92
92
  )
93
+ @click.option(
94
+ "--agent",
95
+ metavar="NAME",
96
+ default=operator_setup.DEFAULT_AGENT,
97
+ show_default=True,
98
+ callback=_shape(operator_setup.check_agent_shape),
99
+ help="Your assistant's name: setup writes its agent file and makes its "
100
+ "mail box, so `session start NAME` starts Claude as NAME, hearing mail to NAME.",
101
+ )
93
102
  @click.option(
94
103
  "--dry-run",
95
104
  is_flag=True,
96
105
  help="Run every check and print what would be written; write nothing.",
97
106
  )
98
- def setup(mailbox: str | None, address: str | None, dry_run: bool) -> None:
99
- """Set up mail on this Mac, then write you as the operator: the human other seats ask.
107
+ def setup(mailbox: str | None, address: str | None, agent: str, dry_run: bool) -> None:
108
+ """Set up mail on this Mac, write you as the operator, and define your assistant.
100
109
 
101
110
  Mail first: it runs sessionmail's mail setup (`message setup`), which
102
111
  makes the mail root and your box, and prints what it did and what is
@@ -109,13 +118,28 @@ def setup(mailbox: str | None, address: str | None, dry_run: bool) -> None:
109
118
  confirm who you are: then it says how to name yourself, with --address
110
119
  or --mailbox. It writes nothing, too, while the operator file an earlier
111
120
  release wrote is still in place and none is at the new path: it names
112
- both. Exit 0 all done, 1 refused or failed, 2 usage, 4 done what it
113
- could, mail steps left for you. See INSTALL.md, "Operator and
114
- administrators" and "Mail between agents".
121
+ both.
122
+
123
+ Then your assistant (--agent, default claude): it writes NAME.md where
124
+ `session start` looks for agents (~/.session/agents, unless
125
+ $AGENTS_DIR or ~/.session/agent/config says otherwise), not keyed, and
126
+ asks `message setup` for the box NAME. A file already there, in either
127
+ layout, is left as it is. It ends with the one launch step:
128
+ `session start NAME`.
129
+
130
+ Before that step it asks `ears backends`, read only. If the ears'
131
+ voice-activity model is missing, it says one line naming `ears install`.
132
+ It never runs `ears install`, and the line never changes the exit.
133
+
134
+ Exit 0 all done, 1 refused or failed, 2 usage, 4 done what it could,
135
+ mail steps left for you. See INSTALL.md, "Operator and administrators"
136
+ and "Mail between agents".
115
137
  """
116
138
  if mailbox is not None and address is not None:
117
139
  raise click.UsageError("give --mailbox or --address, not both")
118
- outcome = operator_setup.run_setup(mailbox=mailbox, address=address, dry_run=dry_run)
140
+ outcome = operator_setup.run_setup(
141
+ mailbox=mailbox, address=address, agent=agent, dry_run=dry_run
142
+ )
119
143
  for line in outcome.lines:
120
144
  click.echo(line, err=outcome.code != 0)
121
145
  raise SystemExit(outcome.code)
@@ -1,4 +1,8 @@
1
- """`sessionkit setup`: make this host's mail ready, then write the operator.
1
+ """`sessionkit setup`: make this host's mail ready, write the operator, then
2
+ define the assistant (`run_agent`: the file `session start NAME` resolves, and
3
+ the box it hears, so a seat started that way hears mail to NAME). Last, it
4
+ asks `ears backends` whether the ears' model is there, and says so when it is
5
+ not (`ears_model_missing`): read only, never `ears install`, never the exit.
2
6
 
3
7
  Mail first. sessionmail's mail setup verb (argv in `MAIL_SETUP_ARGV`) makes
4
8
  the mail root and the first boxes, the operator's box among them, and the
@@ -132,6 +136,21 @@ def check_mailbox_shape(name: str) -> str:
132
136
  return name
133
137
 
134
138
 
139
+ def check_agent_shape(name: str) -> str:
140
+ """NAME names the agent's mail box, so it is a mail box name. It names
141
+ its file too (`<agents folder>/NAME.md`), so it may not start with `.`:
142
+ `session start --list-agents` hides a dot-file, and a picker never offers it."""
143
+ try:
144
+ check_mailbox_shape(name)
145
+ except ShapeError:
146
+ raise ShapeError(
147
+ f"{name!r} is not an agent name: one plain name, no '/', no '@', no spaces"
148
+ ) from None
149
+ if name.startswith("."):
150
+ raise ShapeError(f"{name!r} is not an agent name: it may not start with '.'")
151
+ return name
152
+
153
+
135
154
  def check_address_shape(address: str) -> str:
136
155
  """ADDRESS is `local@domain`: exactly one `@`, both sides non-empty, no
137
156
  whitespace, and not an example domain (a pasted placeholder)."""
@@ -303,8 +322,14 @@ def drop_in_text(block: Mapping[str, str]) -> str:
303
322
  def write_exclusive(target: Path, text: str) -> str | None:
304
323
  """Create TARGET holding TEXT, only if it does not exist. None on
305
324
  success, else why not. Never a partial target and never a stray
306
- `*.json`: the temp's name ends in `.tmp-<pid>`, and it is removed."""
307
- target.parent.mkdir(parents=True, exist_ok=True)
325
+ `*.json`: the temp's name ends in `.tmp-<pid>`, and it is removed. A
326
+ folder that cannot be made (a file in the way, no permission) is a
327
+ reason too, never a traceback: the caller's other halves have run, and
328
+ their lines must still print."""
329
+ try:
330
+ target.parent.mkdir(parents=True, exist_ok=True)
331
+ except OSError as e:
332
+ return f"could not make {target.parent} ({e.strerror or e.__class__.__name__})"
308
333
  temp = target.with_name(f".{target.name}.tmp-{os.getpid()}")
309
334
  try:
310
335
  # A temp by this pid's name is a dead run's leftover (no live process
@@ -344,17 +369,22 @@ MAIL_LEFT = 4
344
369
  MAIL_DOC = 'INSTALL.md, "Mail between agents"'
345
370
 
346
371
 
347
- def mail_setup_boxes(mailbox: str | None, address: str | None) -> tuple[str, ...]:
372
+ def mail_setup_boxes(
373
+ mailbox: str | None, address: str | None, agent: str | None
374
+ ) -> tuple[str, ...]:
348
375
  """The boxes the installer asks the verb for, as `--box NAME` each.
349
376
 
350
- Exactly one, and only when the user asked for one: `--mailbox NAME`
351
- passes NAME, so a named box on a fresh install is made before the
352
- operator check looks for it. Otherwise none: the verb's own floor (the
353
- operator's box, `operator.mailbox`, else the installing account's name)
354
- is all that is made. A seat's own box is made by its pager, never here.
377
+ `--mailbox NAME` passes NAME, so a named box on a fresh install is made
378
+ before the operator check looks for it. Then the agent's box (`--agent`,
379
+ default `claude`): a seat started with `session start NAME` hears the
380
+ box NAME, so the box must exist for mail to it to land. The verb's own
381
+ floor (the operator's box, `operator.mailbox`, else the installing
382
+ account's name) is made besides. A session's own box is made by its
383
+ pager, never here. The verb is idempotent: a box that exists stays.
355
384
  """
356
385
  del address # an address needs no box
357
- return (mailbox,) if mailbox is not None else ()
386
+ boxes = [name for name in (mailbox, agent) if name is not None]
387
+ return tuple(dict.fromkeys(boxes))
358
388
 
359
389
 
360
390
  def mail_setup_argv(boxes: Sequence[str], dry_run: bool) -> list[str]:
@@ -371,15 +401,18 @@ def _verb() -> str:
371
401
  return " ".join(MAIL_SETUP_ARGV)
372
402
 
373
403
 
374
- def _no_verb(why: str) -> Outcome:
375
- """This sessionmail cannot run the verb: print the exact step instead."""
404
+ def _no_verb(why: str, boxes: Sequence[str] = ()) -> Outcome:
405
+ """This sessionmail cannot run the verb: print the exact step instead,
406
+ with the `--box NAME` setup would have passed (the agent's among them),
407
+ so running it later makes the same boxes a run of the verb would."""
408
+ step = " ".join(mail_setup_argv(boxes, dry_run=False))
376
409
  return Outcome(
377
410
  EXIT_STEPS_LEFT,
378
411
  [
379
412
  f"Mail not set up: {why}.",
380
413
  (
381
414
  f"A newer sessionmail has it. Until you have that, set mail up by "
382
- f"hand ({MAIL_DOC}); once you do, run: {_verb()}"
415
+ f"hand ({MAIL_DOC}); once you do, run: {step}"
383
416
  ),
384
417
  ],
385
418
  )
@@ -410,14 +443,15 @@ def run_mail_setup(
410
443
  """
411
444
  program = MAIL_SETUP_ARGV[0]
412
445
  if _find(program, environ) is None:
413
- return _no_verb(f"`{program}` is not on the PATH")
446
+ return _no_verb(f"`{program}` is not on the PATH", boxes)
414
447
  probe = _run([*MAIL_SETUP_ARGV, "--help"], environ)
415
448
  if probe is None:
416
- return _no_verb(f"`{_verb()} --help` did not answer")
449
+ return _no_verb(f"`{_verb()} --help` did not answer", boxes)
417
450
  if probe.returncode != 0:
418
451
  return _no_verb(
419
452
  f"this sessionmail has no `{_verb()}` "
420
- f"(`{_verb()} --help` exited {probe.returncode})"
453
+ f"(`{_verb()} --help` exited {probe.returncode})",
454
+ boxes,
421
455
  )
422
456
 
423
457
  argv = mail_setup_argv(boxes, dry_run)
@@ -463,6 +497,175 @@ def run_mail_setup(
463
497
  )
464
498
 
465
499
 
500
+ # --- the agent: the file `session start NAME` resolves -------------------------
501
+
502
+ # The assistant setup defines when no `--agent` is given.
503
+ DEFAULT_AGENT = "claude"
504
+
505
+ # Where `session start NAME` looks for agents: sessiontools'
506
+ # `start/agents.py:agents_root`, the same rule, mirrored here so the kit does
507
+ # not import sessiontools' internals. `$AGENTS_DIR`, else
508
+ # `~/.session/agent/config` if that is a directory, else `~/.session/agents`.
509
+ # tests/test_setup.py holds this rule to the installed sessiontools'.
510
+ AGENTS_DIR_ENV = "AGENTS_DIR"
511
+ SESSION_HOME = ".session"
512
+ AGENT_CONFIG_DIR = ("agent", "config")
513
+ AGENTS_DIR_NAME = "agents"
514
+
515
+ # The reader: every agent `session start` would accept, one name a line.
516
+ LIST_AGENTS_ARGV: tuple[str, ...] = ("session", "start", "--list-agents")
517
+
518
+ # The one launch step, in sessionphone's final words: the same line its Delta
519
+ # bot prints. Spelled once: change it here only.
520
+ LAUNCH_SENTENCE = "To hear Delta messages, start Claude with: session start {agent}"
521
+
522
+ # The agent file. No `start:` keys: the seat is NOT keyed. No front matter at
523
+ # all: Claude Code (2.1.304, measured) names an agent by its file, so `name:`
524
+ # and `description:` are not needed. For any NAME but `claude` this text is
525
+ # the agent's prompt; for `claude`, Claude Code's own built-in agent of that
526
+ # name answers, and the file only has to exist for `session start` to find it.
527
+ AGENT_TEXT = """\
528
+ You are {agent}, a Claude Code assistant.
529
+
530
+ Written by `sessionkit setup`. `session start {agent}` starts Claude as
531
+ {agent}, so the session hears mail to the box {agent}. Edit this file as you
532
+ like: setup never writes over it.
533
+ """
534
+
535
+
536
+ def agents_root(home: Path, environ: Mapping[str, str]) -> Path:
537
+ """The agents folder `session start` reads, by sessiontools' rule."""
538
+ named = environ.get(AGENTS_DIR_ENV)
539
+ if named:
540
+ return Path(os.path.expanduser(named))
541
+ config = home / SESSION_HOME / Path(*AGENT_CONFIG_DIR)
542
+ if config.is_dir():
543
+ return config
544
+ return home / SESSION_HOME / AGENTS_DIR_NAME
545
+
546
+
547
+ def agent_file(home: Path, environ: Mapping[str, str], agent: str) -> Path:
548
+ """The file setup writes for AGENT: the flat layout, `<root>/NAME.md`."""
549
+ return agents_root(home, environ) / f"{agent}.md"
550
+
551
+
552
+ def agent_text(agent: str) -> str:
553
+ return AGENT_TEXT.format(agent=agent)
554
+
555
+
556
+ def launch_sentence(agent: str) -> str:
557
+ return LAUNCH_SENTENCE.format(agent=agent)
558
+
559
+
560
+ def read_listed_agents(environ: Mapping[str, str]) -> list[str] | None:
561
+ """`session start --list-agents`, one name a line, or None when it does
562
+ not answer (absent, non-zero, a timeout): "not verified", never "absent"."""
563
+ done = _run(LIST_AGENTS_ARGV, environ)
564
+ if done is None or done.returncode != 0:
565
+ return None
566
+ return [line.strip() for line in done.stdout.splitlines() if line.strip()]
567
+
568
+
569
+ def run_agent(
570
+ *,
571
+ agent: str,
572
+ dry_run: bool = False,
573
+ home: Path | None = None,
574
+ environ: Mapping[str, str] | None = None,
575
+ ) -> Outcome:
576
+ """Write AGENT's file where `session start` looks, once; then verify.
577
+
578
+ Never clobbers: either layout `session start` reads (`<root>/NAME/NAME.md`,
579
+ which wins, or `<root>/NAME.md`) already there means write nothing. A
580
+ folder `<root>/NAME/` without its NAME.md, or a flat path that is not a
581
+ file, is refused: `session start` could not start NAME from it, and
582
+ writing beside it would not change that. AGENT is already shape-checked.
583
+ """
584
+ environ = os.environ if environ is None else environ
585
+ home = Path.home() if home is None else home
586
+ root = agents_root(home, environ)
587
+ folder = root / agent
588
+ flat = agent_file(home, environ, agent)
589
+ if folder.is_dir():
590
+ inside = folder / f"{agent}.md"
591
+ if inside.is_file():
592
+ return Outcome(
593
+ EXIT_OK, [f"Agent {agent!r} is already in {inside}; left as it is."]
594
+ )
595
+ return Outcome(
596
+ EXIT_REFUSED,
597
+ [
598
+ (
599
+ f"No agent file written: {folder} exists without {agent}.md "
600
+ f"in it, so session start cannot start {agent!r}."
601
+ ),
602
+ f"Put {agent}.md in that folder, or remove it, then run sessionkit setup again.",
603
+ ],
604
+ )
605
+ if flat.is_file():
606
+ return Outcome(
607
+ EXIT_OK, [f"Agent {agent!r} is already in {flat}; left as it is."]
608
+ )
609
+ if os.path.lexists(flat):
610
+ return Outcome(
611
+ EXIT_REFUSED,
612
+ [
613
+ f"No agent file written: {flat} exists and is not a file session start can read.",
614
+ "Remove it, then run sessionkit setup again.",
615
+ ],
616
+ )
617
+ text = agent_text(agent)
618
+ if dry_run:
619
+ return Outcome(EXIT_OK, [f"Would write {flat}:", text.rstrip("\n")])
620
+
621
+ failed = write_exclusive(flat, text)
622
+ if failed is not None:
623
+ return Outcome(EXIT_REFUSED, [f"No agent file written: {failed}."])
624
+ lines = [f"Wrote {flat}: the agent {agent!r}, not keyed."]
625
+
626
+ listed = read_listed_agents(environ)
627
+ verb = " ".join(LIST_AGENTS_ARGV)
628
+ if listed is None:
629
+ lines.append(f"Written; not verified: `{verb}` did not answer.")
630
+ return Outcome(EXIT_OK, lines)
631
+ if agent not in listed:
632
+ lines.append(
633
+ f"WARNING: `{verb}` does not list {agent!r}: the `session` on your "
634
+ "PATH looks in another agents folder ($AGENTS_DIR, or another sessiontools)."
635
+ )
636
+ return Outcome(EXIT_REFUSED, lines)
637
+ lines.append(f"{verb}: lists {agent!r}.")
638
+ return Outcome(EXIT_OK, lines)
639
+
640
+
641
+ # --- the ears' model: read, never fetched -------------------------------------
642
+
643
+ # sessionvoice's read-only probe. Its last line is `silero ok: PATH` or
644
+ # `silero no model: ears install` (sessionvoice 0.2.1, `ears backends`).
645
+ EARS_BACKENDS_ARGV = ("ears", "backends")
646
+ # The probe answers in well under a second; a slow one is never waited on.
647
+ EARS_PROBE_TIMEOUT = 5.0
648
+ EARS_MODEL_LINE = (
649
+ "The ears have no voice-activity model, so they cannot hear you: "
650
+ "run `ears install` to fetch it."
651
+ )
652
+
653
+
654
+ def ears_model_missing(environ: Mapping[str, str]) -> bool:
655
+ """True only when `ears backends` answers, exit 0, with a `silero` line
656
+ saying `no model`. `ears` not on the PATH (no voice here), a timeout, a
657
+ failure or output we do not know all read as False: say nothing. Setup
658
+ never runs `ears install`; the user does."""
659
+ done = _run(EARS_BACKENDS_ARGV, environ, timeout=EARS_PROBE_TIMEOUT)
660
+ if done is None or done.returncode != 0:
661
+ return False
662
+ for line in (done.stdout or "").splitlines():
663
+ name, _, state = line.strip().partition(" ")
664
+ if name == "silero":
665
+ return state.strip().startswith("no model")
666
+ return False
667
+
668
+
466
669
  # --- the run ------------------------------------------------------------------
467
670
 
468
671
 
@@ -513,21 +716,25 @@ def run_setup(
513
716
  *,
514
717
  mailbox: str | None = None,
515
718
  address: str | None = None,
719
+ agent: str = DEFAULT_AGENT,
516
720
  dry_run: bool = False,
517
721
  home: Path | None = None,
518
722
  environ: Mapping[str, str] | None = None,
519
723
  ) -> Outcome:
520
- """The stranded check, then mail, then the operator; both halves print.
724
+ """The stranded check, then mail, the operator and the agent; all print.
521
725
 
522
726
  An operator file stranded at the 0.1.0 place, with none at the new one,
523
727
  stops the whole run before mail: nothing is made or written, one line.
524
728
  The verb would otherwise make the operator's box from settings that
525
- cannot see that file. Past it, both halves always run. Mail goes first
729
+ cannot see that file. Past it, all three always run. Mail goes first
526
730
  because the operator is written only for a box that exists, and the
527
- mail setup verb is what makes the operator's box. The exit is the worse
528
- of the two: 1 if either refused or failed, else 4 if mail left steps
529
- for you, else 0. MAILBOX / ADDRESS are already shape-checked by the
530
- caller.
731
+ mail setup verb is what makes the operator's box and the agent's. The
732
+ exit is the worst of the three: 1 if any refused or failed, else 4 if
733
+ mail left steps for you, else 0. Then `ears backends` is asked, read
734
+ only: when the ears' model is missing, one line (EARS_MODEL_LINE) names
735
+ `ears install`; it never changes the exit. With the agent defined, the
736
+ run ends on the one launch step (LAUNCH_SENTENCE). MAILBOX / ADDRESS /
737
+ AGENT are already shape-checked by the caller.
531
738
  """
532
739
  environ = os.environ if environ is None else environ
533
740
  resolved = Path.home() if home is None else home
@@ -538,18 +745,32 @@ def run_setup(
538
745
  # sessionmail's default carrier, and postfix is the user's opt-in.
539
746
  # `--write-maps` is never passed: writing postfix maps is opt-in too.
540
747
  mail = run_mail_setup(
541
- boxes=mail_setup_boxes(mailbox, address), dry_run=dry_run, environ=environ
748
+ boxes=mail_setup_boxes(mailbox, address, agent),
749
+ dry_run=dry_run,
750
+ environ=environ,
542
751
  )
543
752
  operator = run_operator(
544
753
  mailbox=mailbox, address=address, dry_run=dry_run, home=home, environ=environ
545
754
  )
546
- if EXIT_REFUSED in (mail.code, operator.code):
755
+ defined = run_agent(agent=agent, dry_run=dry_run, home=resolved, environ=environ)
756
+ codes = (mail.code, operator.code, defined.code)
757
+ if EXIT_REFUSED in codes:
547
758
  code = EXIT_REFUSED
548
- elif EXIT_STEPS_LEFT in (mail.code, operator.code):
759
+ elif EXIT_STEPS_LEFT in codes:
549
760
  code = EXIT_STEPS_LEFT
550
761
  else:
551
762
  code = EXIT_OK
552
- return Outcome(code, mail.lines + operator.lines)
763
+ lines = mail.lines + operator.lines + defined.lines
764
+ if dry_run:
765
+ lines.append(
766
+ f"Would ask `{_verb()}` for the agent's box {agent!r} (--box {agent})."
767
+ )
768
+ # Read only, and never part of the exit: 4 means mail steps are left.
769
+ if ears_model_missing(environ):
770
+ lines.append(EARS_MODEL_LINE)
771
+ if defined.code == EXIT_OK:
772
+ lines.append(launch_sentence(agent))
773
+ return Outcome(code, lines)
553
774
 
554
775
 
555
776
  def run_operator(
sessionkit-0.2.0/PKG-INFO DELETED
@@ -1,18 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: sessionkit
3
- Version: 0.2.0
4
- Summary: Session Kit gives your Claude Code sessions voice, mail, a phone line and a tmux sidebar, in one install.
5
- Author: Mike Bailey
6
- Author-email: Mike Bailey <mike@sessionkit.ai>
7
- License-Expression: AGPL-3.0-or-later
8
- License-File: LICENSE
9
- Classifier: Development Status :: 3 - Alpha
10
- Requires-Dist: click>=8.1
11
- Requires-Dist: sessionmail>=0.1.0
12
- Requires-Dist: sessiontools>=0.1.0
13
- Requires-Dist: sessionphone>=0.1.0
14
- Requires-Dist: sessionview>=0.1.0
15
- Requires-Dist: sessionvoice[capture]>=0.1.0
16
- Requires-Python: >=3.11, <3.15
17
- Project-URL: Homepage, https://github.com/sessionkit/sessionkit
18
- Project-URL: Repository, https://github.com/sessionkit/sessionkit
File without changes