sessionkit 0.2.0__tar.gz → 0.2.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,191 @@
1
+ Metadata-Version: 2.4
2
+ Name: sessionkit
3
+ Version: 0.2.1
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.2.1
12
+ Requires-Dist: sessiontools>=0.2.0
13
+ Requires-Dist: sessionphone[media]>=0.2.1
14
+ Requires-Dist: sessionview>=0.2.0
15
+ Requires-Dist: sessionvoice[capture]>=0.2.1
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 and [Claude Code][claude-code]. If you do not have uv yet,
30
+ install it with `curl -LsSf https://astral.sh/uv/install.sh | sh`, then open
31
+ a new terminal (or run `source ~/.local/bin/env`).
32
+
33
+ ```bash
34
+ uv tool install --python 3.14 sessionkit
35
+ claude plugin marketplace add sessionkit/sessionkit
36
+ claude plugin install sessionkit@sessionkit
37
+ ```
38
+
39
+ The first line puts the commands on your PATH. The other two install the
40
+ kit's plugin and the five family plugins it depends on. Start Claude Code
41
+ (or restart it) to load them.
42
+
43
+ - **Keep `--python 3.14`.** Without it, uv takes the Python it finds: 3.15,
44
+ where a voice library has no build yet and the install fails, or the
45
+ Command Line Tools' 3.11. The kit is tested on 3.14.
46
+ - **A dialog may ask to install the Command Line Tools.** On a Mac without
47
+ them, the first `uv tool install` opens it. Click Install and let it
48
+ finish: the plugin lines need the git it brings.
49
+ - **Phone calls are installed, not yet walked.** Whether the phone bot
50
+ signs its mail to your Claude follows your mail carrier: [bot mail
51
+ signing][phone-signing].
52
+
53
+ - **The mail hooks start off.** They tell an agent about new mail between
54
+ its tool calls. To turn them on, set `"hooks": true` in
55
+ `~/.sessionkit/mail/settings/mail.json`. On a fresh install the file is
56
+ not there yet, so make it:
57
+
58
+ ```bash
59
+ mkdir -p ~/.sessionkit/mail/settings
60
+ echo '{"version": 1, "hooks": true}' > ~/.sessionkit/mail/settings/mail.json
61
+ ```
62
+
63
+ If the file is there, change only that key, because it can hold other
64
+ settings:
65
+
66
+ ```bash
67
+ perl -pi -e 's/"hooks": *false/"hooks": true/' ~/.sessionkit/mail/settings/mail.json
68
+ ```
69
+
70
+ Step by step, with every step that needs you marked: [the manual
71
+ install][install].
72
+
73
+ ## Quick start
74
+
75
+ ```bash
76
+ sessionkit --version
77
+ ```
78
+
79
+ It lists the kit and each family package, with its version:
80
+
81
+ ```text
82
+ sessionkit, version 0.2.1
83
+ sessionmail 0.2.1
84
+ sessiontools 0.2.0
85
+ sessionphone 0.2.1
86
+ sessionview 0.2.0
87
+ sessionvoice 0.2.1
88
+ ```
89
+
90
+ Start Claude with `session start claude`, never a bare `claude`: mail to
91
+ your agent's box, your phone's included, reaches only a session started as
92
+ that agent. `sessionkit setup` makes the box and the agent first: [first
93
+ checks][checks].
94
+
95
+ Then, from your agent inside a Claude Code session:
96
+
97
+ ```bash
98
+ session list # your live sessions
99
+ message send <box> -s "PR ready" -f note.md # mail another agent
100
+ mouth say --device default "the build is green" # speak one line
101
+ ```
102
+
103
+ - **Mail works inside a session.** `message` and `pager` read who is asking
104
+ from the session. A plain terminal has no session, so they refuse with
105
+ `no seat in the environment ... set an identity` ([first checks][checks]).
106
+ A send never makes a box: the address must have one already ([mail
107
+ between agents][mail]).
108
+ - **The mouth needs a device.** `--device default` is your Mac's default
109
+ output; `mouth devices` lists the others.
110
+ - **To hear you**, the ears need a speech-to-text server: [hearing and
111
+ speaking][hearing].
112
+
113
+ ## Uninstall
114
+
115
+ ```bash
116
+ claude plugin marketplace remove sessionkit
117
+ uv tool uninstall sessionkit
118
+ ```
119
+
120
+ Removing the marketplace removes its six plugins too. The services and your
121
+ data have their own steps: [uninstall][uninstall].
122
+
123
+ ## Usage
124
+
125
+ Every command answers `--help`, and each has a page: [components][components].
126
+
127
+ ## The family
128
+
129
+ Session Kit installs these together:
130
+
131
+ - [sessionmail][pkg-mail]: mail for coding agents. `message`, `pager` and
132
+ `memory` over one maildir.
133
+ - [sessiontools][pkg-tools]: sessions, status and idle. `session`.
134
+ - [sessionvoice][pkg-voice]: ambient voice. `ears` hears; `mouth` speaks.
135
+ - [sessionphone][pkg-phone]: phone calls to a session over Delta Chat.
136
+ Installed, not yet walked.
137
+ - [sessionview][pkg-view]: a tmux sidebar of your projects and your agents.
138
+
139
+ ## Components
140
+
141
+ The commands, services and folders the install adds, and where each
142
+ package logs: [components][components].
143
+
144
+ ## Configuration
145
+
146
+ Each package configures itself: [configuration][configuration]. How the
147
+ settings files stack: [the settings guide][settings].
148
+
149
+ ## Logs
150
+
151
+ Each package keeps its own log: [logs][logs]. Which one to read for a
152
+ symptom: [troubleshooting][troubleshooting].
153
+
154
+ ## Development
155
+
156
+ ```bash
157
+ git clone https://github.com/sessionkit/sessionkit
158
+ cd sessionkit
159
+ make install # editable: uv tool install -e .
160
+ make check # ruff, shellcheck, then the tests
161
+ ```
162
+
163
+ The rules for changing the kit: [AGENTS.md][agents].
164
+
165
+ ## Status
166
+
167
+ Alpha, 0.2.1. Tested on macOS 27 on Apple silicon; Linux and Windows are
168
+ untested. What you may still hit: [known gaps][gaps].
169
+
170
+ Licence: [AGPL-3.0][licence].
171
+
172
+ [claude-code]: https://claude.ai/code
173
+ [install]: INSTALL.md
174
+ [checks]: INSTALL.md#4-first-checks
175
+ [hearing]: INSTALL.md#5-hearing-and-speaking-it-hears-me-and-speaks
176
+ [mail]: INSTALL.md#mail-between-agents
177
+ [uninstall]: INSTALL.md#8-uninstall
178
+ [gaps]: INSTALL.md#7-known-gaps-you-will-hit
179
+ [components]: docs/components.md
180
+ [logs]: docs/components.md#logs
181
+ [configuration]: docs/configuration.md
182
+ [settings]: docs/settings.md
183
+ [troubleshooting]: docs/troubleshooting.md
184
+ [agents]: AGENTS.md
185
+ [licence]: LICENSE
186
+ [pkg-mail]: https://github.com/sessionkit/sessionmail
187
+ [pkg-tools]: https://github.com/sessionkit/sessiontools
188
+ [pkg-voice]: https://github.com/sessionkit/sessionvoice
189
+ [pkg-phone]: https://github.com/sessionkit/sessionphone
190
+ [phone-signing]: https://github.com/sessionkit/sessionphone/blob/HEAD/docs/bot-mail-signing.md
191
+ [pkg-view]: https://github.com/sessionkit/sessionview
@@ -0,0 +1,171 @@
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 and [Claude Code][claude-code]. If you do not have uv yet,
10
+ install it with `curl -LsSf https://astral.sh/uv/install.sh | sh`, then open
11
+ a new terminal (or run `source ~/.local/bin/env`).
12
+
13
+ ```bash
14
+ uv tool install --python 3.14 sessionkit
15
+ claude plugin marketplace add sessionkit/sessionkit
16
+ claude plugin install sessionkit@sessionkit
17
+ ```
18
+
19
+ The first line puts the commands on your PATH. The other two install the
20
+ kit's plugin and the five family plugins it depends on. Start Claude Code
21
+ (or restart it) to load them.
22
+
23
+ - **Keep `--python 3.14`.** Without it, uv takes the Python it finds: 3.15,
24
+ where a voice library has no build yet and the install fails, or the
25
+ Command Line Tools' 3.11. The kit is tested on 3.14.
26
+ - **A dialog may ask to install the Command Line Tools.** On a Mac without
27
+ them, the first `uv tool install` opens it. Click Install and let it
28
+ finish: the plugin lines need the git it brings.
29
+ - **Phone calls are installed, not yet walked.** Whether the phone bot
30
+ signs its mail to your Claude follows your mail carrier: [bot mail
31
+ signing][phone-signing].
32
+
33
+ - **The mail hooks start off.** They tell an agent about new mail between
34
+ its tool calls. To turn them on, set `"hooks": true` in
35
+ `~/.sessionkit/mail/settings/mail.json`. On a fresh install the file is
36
+ not there yet, so make it:
37
+
38
+ ```bash
39
+ mkdir -p ~/.sessionkit/mail/settings
40
+ echo '{"version": 1, "hooks": true}' > ~/.sessionkit/mail/settings/mail.json
41
+ ```
42
+
43
+ If the file is there, change only that key, because it can hold other
44
+ settings:
45
+
46
+ ```bash
47
+ perl -pi -e 's/"hooks": *false/"hooks": true/' ~/.sessionkit/mail/settings/mail.json
48
+ ```
49
+
50
+ Step by step, with every step that needs you marked: [the manual
51
+ install][install].
52
+
53
+ ## Quick start
54
+
55
+ ```bash
56
+ sessionkit --version
57
+ ```
58
+
59
+ It lists the kit and each family package, with its version:
60
+
61
+ ```text
62
+ sessionkit, version 0.2.1
63
+ sessionmail 0.2.1
64
+ sessiontools 0.2.0
65
+ sessionphone 0.2.1
66
+ sessionview 0.2.0
67
+ sessionvoice 0.2.1
68
+ ```
69
+
70
+ Start Claude with `session start claude`, never a bare `claude`: mail to
71
+ your agent's box, your phone's included, reaches only a session started as
72
+ that agent. `sessionkit setup` makes the box and the agent first: [first
73
+ checks][checks].
74
+
75
+ Then, from your agent inside a Claude Code session:
76
+
77
+ ```bash
78
+ session list # your live sessions
79
+ message send <box> -s "PR ready" -f note.md # mail another agent
80
+ mouth say --device default "the build is green" # speak one line
81
+ ```
82
+
83
+ - **Mail works inside a session.** `message` and `pager` read who is asking
84
+ from the session. A plain terminal has no session, so they refuse with
85
+ `no seat in the environment ... set an identity` ([first checks][checks]).
86
+ A send never makes a box: the address must have one already ([mail
87
+ between agents][mail]).
88
+ - **The mouth needs a device.** `--device default` is your Mac's default
89
+ output; `mouth devices` lists the others.
90
+ - **To hear you**, the ears need a speech-to-text server: [hearing and
91
+ speaking][hearing].
92
+
93
+ ## Uninstall
94
+
95
+ ```bash
96
+ claude plugin marketplace remove sessionkit
97
+ uv tool uninstall sessionkit
98
+ ```
99
+
100
+ Removing the marketplace removes its six plugins too. The services and your
101
+ data have their own steps: [uninstall][uninstall].
102
+
103
+ ## Usage
104
+
105
+ Every command answers `--help`, and each has a page: [components][components].
106
+
107
+ ## The family
108
+
109
+ Session Kit installs these together:
110
+
111
+ - [sessionmail][pkg-mail]: mail for coding agents. `message`, `pager` and
112
+ `memory` over one maildir.
113
+ - [sessiontools][pkg-tools]: sessions, status and idle. `session`.
114
+ - [sessionvoice][pkg-voice]: ambient voice. `ears` hears; `mouth` speaks.
115
+ - [sessionphone][pkg-phone]: phone calls to a session over Delta Chat.
116
+ Installed, not yet walked.
117
+ - [sessionview][pkg-view]: a tmux sidebar of your projects and your agents.
118
+
119
+ ## Components
120
+
121
+ The commands, services and folders the install adds, and where each
122
+ package logs: [components][components].
123
+
124
+ ## Configuration
125
+
126
+ Each package configures itself: [configuration][configuration]. How the
127
+ settings files stack: [the settings guide][settings].
128
+
129
+ ## Logs
130
+
131
+ Each package keeps its own log: [logs][logs]. Which one to read for a
132
+ symptom: [troubleshooting][troubleshooting].
133
+
134
+ ## Development
135
+
136
+ ```bash
137
+ git clone https://github.com/sessionkit/sessionkit
138
+ cd sessionkit
139
+ make install # editable: uv tool install -e .
140
+ make check # ruff, shellcheck, then the tests
141
+ ```
142
+
143
+ The rules for changing the kit: [AGENTS.md][agents].
144
+
145
+ ## Status
146
+
147
+ Alpha, 0.2.1. Tested on macOS 27 on Apple silicon; Linux and Windows are
148
+ untested. What you may still hit: [known gaps][gaps].
149
+
150
+ Licence: [AGPL-3.0][licence].
151
+
152
+ [claude-code]: https://claude.ai/code
153
+ [install]: INSTALL.md
154
+ [checks]: INSTALL.md#4-first-checks
155
+ [hearing]: INSTALL.md#5-hearing-and-speaking-it-hears-me-and-speaks
156
+ [mail]: INSTALL.md#mail-between-agents
157
+ [uninstall]: INSTALL.md#8-uninstall
158
+ [gaps]: INSTALL.md#7-known-gaps-you-will-hit
159
+ [components]: docs/components.md
160
+ [logs]: docs/components.md#logs
161
+ [configuration]: docs/configuration.md
162
+ [settings]: docs/settings.md
163
+ [troubleshooting]: docs/troubleshooting.md
164
+ [agents]: AGENTS.md
165
+ [licence]: LICENSE
166
+ [pkg-mail]: https://github.com/sessionkit/sessionmail
167
+ [pkg-tools]: https://github.com/sessionkit/sessiontools
168
+ [pkg-voice]: https://github.com/sessionkit/sessionvoice
169
+ [pkg-phone]: https://github.com/sessionkit/sessionphone
170
+ [phone-signing]: https://github.com/sessionkit/sessionphone/blob/HEAD/docs/bot-mail-signing.md
171
+ [pkg-view]: https://github.com/sessionkit/sessionview
@@ -0,0 +1,54 @@
1
+ [project]
2
+ name = "sessionkit"
3
+ version = "0.2.1"
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"
6
+ license = "AGPL-3.0-or-later"
7
+ license-files = ["LICENSE"]
8
+ classifiers = ["Development Status :: 3 - Alpha"]
9
+ requires-python = ">=3.11,<3.15"
10
+ dependencies = [
11
+ "click>=8.1",
12
+ "sessionmail>=0.2.1",
13
+ "sessiontools>=0.2.0",
14
+ "sessionphone[media]>=0.2.1",
15
+ "sessionview>=0.2.0",
16
+ "sessionvoice[capture]>=0.2.1",
17
+ ]
18
+
19
+ [[project.authors]]
20
+ name = "Mike Bailey"
21
+ email = "mike@sessionkit.ai"
22
+
23
+ [project.urls]
24
+ Homepage = "https://github.com/sessionkit/sessionkit"
25
+ Repository = "https://github.com/sessionkit/sessionkit"
26
+
27
+ [project.scripts]
28
+ sessionkit = "sessionkit.cli:main"
29
+ message = "sessionmail.message:cli"
30
+ pager = "sessionmail.pager:cli"
31
+ memory = "sessionmail.memory:cli"
32
+ session = "sessiontools.cli:main"
33
+ sessionphone = "sessionphone.cli:main"
34
+ sessionview = "sessionview:main"
35
+ sessionview-config = "sessionview:main"
36
+ sessionview-session = "sessionview:main"
37
+ sessionview-messages = "sessionview:main"
38
+ ears = "ears.cli:main"
39
+ ears-serve = "ears.serve:main"
40
+ mouth = "mouth.__main__:main"
41
+ talk = "talk.cli:main"
42
+
43
+ [build-system]
44
+ requires = ["uv_build>=0.11.9,<0.12.0"]
45
+ build-backend = "uv_build"
46
+
47
+ [dependency-groups]
48
+ dev = [
49
+ "pytest>=8",
50
+ "ruff>=0.6",
51
+ ]
52
+
53
+ [tool.pytest.ini_options]
54
+ testpaths = ["tests"]
@@ -1,7 +1,8 @@
1
1
  [project]
2
2
  name = "sessionkit"
3
- version = "0.2.0"
3
+ version = "0.2.1"
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,24 @@ 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.2.0: the kit needs every family
14
+ # package at 0.2.0 or later, and PyPI's 0.0.1 name placeholders and the
15
+ # 0.1.0 alphas must never satisfy these. sessionmail, sessionphone and
16
+ # sessionvoice are floored at 0.2.1: mail's 0.2.1 judges the doorbell by the
17
+ # drop, phone's 0.2.1 sends unsigned under the drop carrier, and voice's 0.2.1
18
+ # installer sets the drop carrier.
19
+ # sessionphone with its media extra: the bot, calls and `sessionphone web` need it.
14
20
  # sessionvoice with its capture extra, so the ears can hear the room.
21
+ # This is the RULED list, kept in tests/_ruled_deps.py with the ruling;
22
+ # tests/test_dependency_gate.py fails when these lines, the built wheel or
23
+ # uv.lock drift from it.
15
24
  dependencies = [
16
25
  "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",
26
+ "sessionmail>=0.2.1",
27
+ "sessiontools>=0.2.0",
28
+ "sessionphone[media]>=0.2.1",
29
+ "sessionview>=0.2.0",
30
+ "sessionvoice[capture]>=0.2.1",
22
31
  ]
23
32
 
24
33
  [project.urls]
@@ -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,24 @@ 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
+ Exit 0 all done, 1 refused or failed, 2 usage, 4 done what it could,
131
+ mail steps left for you. See INSTALL.md, "Operator and administrators"
132
+ and "Mail between agents".
115
133
  """
116
134
  if mailbox is not None and address is not None:
117
135
  raise click.UsageError("give --mailbox or --address, not both")
118
- outcome = operator_setup.run_setup(mailbox=mailbox, address=address, dry_run=dry_run)
136
+ outcome = operator_setup.run_setup(
137
+ mailbox=mailbox, address=address, agent=agent, dry_run=dry_run
138
+ )
119
139
  for line in outcome.lines:
120
140
  click.echo(line, err=outcome.code != 0)
121
141
  raise SystemExit(outcome.code)
@@ -1,4 +1,6 @@
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).
2
4
 
3
5
  Mail first. sessionmail's mail setup verb (argv in `MAIL_SETUP_ARGV`) makes
4
6
  the mail root and the first boxes, the operator's box among them, and the
@@ -132,6 +134,21 @@ def check_mailbox_shape(name: str) -> str:
132
134
  return name
133
135
 
134
136
 
137
+ def check_agent_shape(name: str) -> str:
138
+ """NAME names the agent's mail box, so it is a mail box name. It names
139
+ its file too (`<agents folder>/NAME.md`), so it may not start with `.`:
140
+ `session start --list-agents` hides a dot-file, and a picker never offers it."""
141
+ try:
142
+ check_mailbox_shape(name)
143
+ except ShapeError:
144
+ raise ShapeError(
145
+ f"{name!r} is not an agent name: one plain name, no '/', no '@', no spaces"
146
+ ) from None
147
+ if name.startswith("."):
148
+ raise ShapeError(f"{name!r} is not an agent name: it may not start with '.'")
149
+ return name
150
+
151
+
135
152
  def check_address_shape(address: str) -> str:
136
153
  """ADDRESS is `local@domain`: exactly one `@`, both sides non-empty, no
137
154
  whitespace, and not an example domain (a pasted placeholder)."""
@@ -303,8 +320,14 @@ def drop_in_text(block: Mapping[str, str]) -> str:
303
320
  def write_exclusive(target: Path, text: str) -> str | None:
304
321
  """Create TARGET holding TEXT, only if it does not exist. None on
305
322
  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)
323
+ `*.json`: the temp's name ends in `.tmp-<pid>`, and it is removed. A
324
+ folder that cannot be made (a file in the way, no permission) is a
325
+ reason too, never a traceback: the caller's other halves have run, and
326
+ their lines must still print."""
327
+ try:
328
+ target.parent.mkdir(parents=True, exist_ok=True)
329
+ except OSError as e:
330
+ return f"could not make {target.parent} ({e.strerror or e.__class__.__name__})"
308
331
  temp = target.with_name(f".{target.name}.tmp-{os.getpid()}")
309
332
  try:
310
333
  # A temp by this pid's name is a dead run's leftover (no live process
@@ -344,17 +367,22 @@ MAIL_LEFT = 4
344
367
  MAIL_DOC = 'INSTALL.md, "Mail between agents"'
345
368
 
346
369
 
347
- def mail_setup_boxes(mailbox: str | None, address: str | None) -> tuple[str, ...]:
370
+ def mail_setup_boxes(
371
+ mailbox: str | None, address: str | None, agent: str | None
372
+ ) -> tuple[str, ...]:
348
373
  """The boxes the installer asks the verb for, as `--box NAME` each.
349
374
 
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.
375
+ `--mailbox NAME` passes NAME, so a named box on a fresh install is made
376
+ before the operator check looks for it. Then the agent's box (`--agent`,
377
+ default `claude`): a seat started with `session start NAME` hears the
378
+ box NAME, so the box must exist for mail to it to land. The verb's own
379
+ floor (the operator's box, `operator.mailbox`, else the installing
380
+ account's name) is made besides. A session's own box is made by its
381
+ pager, never here. The verb is idempotent: a box that exists stays.
355
382
  """
356
383
  del address # an address needs no box
357
- return (mailbox,) if mailbox is not None else ()
384
+ boxes = [name for name in (mailbox, agent) if name is not None]
385
+ return tuple(dict.fromkeys(boxes))
358
386
 
359
387
 
360
388
  def mail_setup_argv(boxes: Sequence[str], dry_run: bool) -> list[str]:
@@ -371,15 +399,18 @@ def _verb() -> str:
371
399
  return " ".join(MAIL_SETUP_ARGV)
372
400
 
373
401
 
374
- def _no_verb(why: str) -> Outcome:
375
- """This sessionmail cannot run the verb: print the exact step instead."""
402
+ def _no_verb(why: str, boxes: Sequence[str] = ()) -> Outcome:
403
+ """This sessionmail cannot run the verb: print the exact step instead,
404
+ with the `--box NAME` setup would have passed (the agent's among them),
405
+ so running it later makes the same boxes a run of the verb would."""
406
+ step = " ".join(mail_setup_argv(boxes, dry_run=False))
376
407
  return Outcome(
377
408
  EXIT_STEPS_LEFT,
378
409
  [
379
410
  f"Mail not set up: {why}.",
380
411
  (
381
412
  f"A newer sessionmail has it. Until you have that, set mail up by "
382
- f"hand ({MAIL_DOC}); once you do, run: {_verb()}"
413
+ f"hand ({MAIL_DOC}); once you do, run: {step}"
383
414
  ),
384
415
  ],
385
416
  )
@@ -410,14 +441,15 @@ def run_mail_setup(
410
441
  """
411
442
  program = MAIL_SETUP_ARGV[0]
412
443
  if _find(program, environ) is None:
413
- return _no_verb(f"`{program}` is not on the PATH")
444
+ return _no_verb(f"`{program}` is not on the PATH", boxes)
414
445
  probe = _run([*MAIL_SETUP_ARGV, "--help"], environ)
415
446
  if probe is None:
416
- return _no_verb(f"`{_verb()} --help` did not answer")
447
+ return _no_verb(f"`{_verb()} --help` did not answer", boxes)
417
448
  if probe.returncode != 0:
418
449
  return _no_verb(
419
450
  f"this sessionmail has no `{_verb()}` "
420
- f"(`{_verb()} --help` exited {probe.returncode})"
451
+ f"(`{_verb()} --help` exited {probe.returncode})",
452
+ boxes,
421
453
  )
422
454
 
423
455
  argv = mail_setup_argv(boxes, dry_run)
@@ -463,6 +495,147 @@ def run_mail_setup(
463
495
  )
464
496
 
465
497
 
498
+ # --- the agent: the file `session start NAME` resolves -------------------------
499
+
500
+ # The assistant setup defines when no `--agent` is given.
501
+ DEFAULT_AGENT = "claude"
502
+
503
+ # Where `session start NAME` looks for agents: sessiontools'
504
+ # `start/agents.py:agents_root`, the same rule, mirrored here so the kit does
505
+ # not import sessiontools' internals. `$AGENTS_DIR`, else
506
+ # `~/.session/agent/config` if that is a directory, else `~/.session/agents`.
507
+ # tests/test_setup.py holds this rule to the installed sessiontools'.
508
+ AGENTS_DIR_ENV = "AGENTS_DIR"
509
+ SESSION_HOME = ".session"
510
+ AGENT_CONFIG_DIR = ("agent", "config")
511
+ AGENTS_DIR_NAME = "agents"
512
+
513
+ # The reader: every agent `session start` would accept, one name a line.
514
+ LIST_AGENTS_ARGV: tuple[str, ...] = ("session", "start", "--list-agents")
515
+
516
+ # The one launch step, in sessionphone's final words: the same line its Delta
517
+ # bot prints. Spelled once: change it here only.
518
+ LAUNCH_SENTENCE = "To hear Delta messages, start Claude with: session start {agent}"
519
+
520
+ # The agent file. No `start:` keys: the seat is NOT keyed. No front matter at
521
+ # all: Claude Code (2.1.304, measured) names an agent by its file, so `name:`
522
+ # and `description:` are not needed. For any NAME but `claude` this text is
523
+ # the agent's prompt; for `claude`, Claude Code's own built-in agent of that
524
+ # name answers, and the file only has to exist for `session start` to find it.
525
+ AGENT_TEXT = """\
526
+ You are {agent}, a Claude Code assistant.
527
+
528
+ Written by `sessionkit setup`. `session start {agent}` starts Claude as
529
+ {agent}, so the session hears mail to the box {agent}. Edit this file as you
530
+ like: setup never writes over it.
531
+ """
532
+
533
+
534
+ def agents_root(home: Path, environ: Mapping[str, str]) -> Path:
535
+ """The agents folder `session start` reads, by sessiontools' rule."""
536
+ named = environ.get(AGENTS_DIR_ENV)
537
+ if named:
538
+ return Path(os.path.expanduser(named))
539
+ config = home / SESSION_HOME / Path(*AGENT_CONFIG_DIR)
540
+ if config.is_dir():
541
+ return config
542
+ return home / SESSION_HOME / AGENTS_DIR_NAME
543
+
544
+
545
+ def agent_file(home: Path, environ: Mapping[str, str], agent: str) -> Path:
546
+ """The file setup writes for AGENT: the flat layout, `<root>/NAME.md`."""
547
+ return agents_root(home, environ) / f"{agent}.md"
548
+
549
+
550
+ def agent_text(agent: str) -> str:
551
+ return AGENT_TEXT.format(agent=agent)
552
+
553
+
554
+ def launch_sentence(agent: str) -> str:
555
+ return LAUNCH_SENTENCE.format(agent=agent)
556
+
557
+
558
+ def read_listed_agents(environ: Mapping[str, str]) -> list[str] | None:
559
+ """`session start --list-agents`, one name a line, or None when it does
560
+ not answer (absent, non-zero, a timeout): "not verified", never "absent"."""
561
+ done = _run(LIST_AGENTS_ARGV, environ)
562
+ if done is None or done.returncode != 0:
563
+ return None
564
+ return [line.strip() for line in done.stdout.splitlines() if line.strip()]
565
+
566
+
567
+ def run_agent(
568
+ *,
569
+ agent: str,
570
+ dry_run: bool = False,
571
+ home: Path | None = None,
572
+ environ: Mapping[str, str] | None = None,
573
+ ) -> Outcome:
574
+ """Write AGENT's file where `session start` looks, once; then verify.
575
+
576
+ Never clobbers: either layout `session start` reads (`<root>/NAME/NAME.md`,
577
+ which wins, or `<root>/NAME.md`) already there means write nothing. A
578
+ folder `<root>/NAME/` without its NAME.md, or a flat path that is not a
579
+ file, is refused: `session start` could not start NAME from it, and
580
+ writing beside it would not change that. AGENT is already shape-checked.
581
+ """
582
+ environ = os.environ if environ is None else environ
583
+ home = Path.home() if home is None else home
584
+ root = agents_root(home, environ)
585
+ folder = root / agent
586
+ flat = agent_file(home, environ, agent)
587
+ if folder.is_dir():
588
+ inside = folder / f"{agent}.md"
589
+ if inside.is_file():
590
+ return Outcome(
591
+ EXIT_OK, [f"Agent {agent!r} is already in {inside}; left as it is."]
592
+ )
593
+ return Outcome(
594
+ EXIT_REFUSED,
595
+ [
596
+ (
597
+ f"No agent file written: {folder} exists without {agent}.md "
598
+ f"in it, so session start cannot start {agent!r}."
599
+ ),
600
+ f"Put {agent}.md in that folder, or remove it, then run sessionkit setup again.",
601
+ ],
602
+ )
603
+ if flat.is_file():
604
+ return Outcome(
605
+ EXIT_OK, [f"Agent {agent!r} is already in {flat}; left as it is."]
606
+ )
607
+ if os.path.lexists(flat):
608
+ return Outcome(
609
+ EXIT_REFUSED,
610
+ [
611
+ f"No agent file written: {flat} exists and is not a file session start can read.",
612
+ "Remove it, then run sessionkit setup again.",
613
+ ],
614
+ )
615
+ text = agent_text(agent)
616
+ if dry_run:
617
+ return Outcome(EXIT_OK, [f"Would write {flat}:", text.rstrip("\n")])
618
+
619
+ failed = write_exclusive(flat, text)
620
+ if failed is not None:
621
+ return Outcome(EXIT_REFUSED, [f"No agent file written: {failed}."])
622
+ lines = [f"Wrote {flat}: the agent {agent!r}, not keyed."]
623
+
624
+ listed = read_listed_agents(environ)
625
+ verb = " ".join(LIST_AGENTS_ARGV)
626
+ if listed is None:
627
+ lines.append(f"Written; not verified: `{verb}` did not answer.")
628
+ return Outcome(EXIT_OK, lines)
629
+ if agent not in listed:
630
+ lines.append(
631
+ f"WARNING: `{verb}` does not list {agent!r}: the `session` on your "
632
+ "PATH looks in another agents folder ($AGENTS_DIR, or another sessiontools)."
633
+ )
634
+ return Outcome(EXIT_REFUSED, lines)
635
+ lines.append(f"{verb}: lists {agent!r}.")
636
+ return Outcome(EXIT_OK, lines)
637
+
638
+
466
639
  # --- the run ------------------------------------------------------------------
467
640
 
468
641
 
@@ -513,21 +686,23 @@ def run_setup(
513
686
  *,
514
687
  mailbox: str | None = None,
515
688
  address: str | None = None,
689
+ agent: str = DEFAULT_AGENT,
516
690
  dry_run: bool = False,
517
691
  home: Path | None = None,
518
692
  environ: Mapping[str, str] | None = None,
519
693
  ) -> Outcome:
520
- """The stranded check, then mail, then the operator; both halves print.
694
+ """The stranded check, then mail, the operator and the agent; all print.
521
695
 
522
696
  An operator file stranded at the 0.1.0 place, with none at the new one,
523
697
  stops the whole run before mail: nothing is made or written, one line.
524
698
  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
699
+ cannot see that file. Past it, all three always run. Mail goes first
526
700
  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.
701
+ mail setup verb is what makes the operator's box and the agent's. The
702
+ exit is the worst of the three: 1 if any refused or failed, else 4 if
703
+ mail left steps for you, else 0. With the agent defined, the run ends on
704
+ the one launch step (LAUNCH_SENTENCE). MAILBOX / ADDRESS / AGENT are
705
+ already shape-checked by the caller.
531
706
  """
532
707
  environ = os.environ if environ is None else environ
533
708
  resolved = Path.home() if home is None else home
@@ -538,18 +713,29 @@ def run_setup(
538
713
  # sessionmail's default carrier, and postfix is the user's opt-in.
539
714
  # `--write-maps` is never passed: writing postfix maps is opt-in too.
540
715
  mail = run_mail_setup(
541
- boxes=mail_setup_boxes(mailbox, address), dry_run=dry_run, environ=environ
716
+ boxes=mail_setup_boxes(mailbox, address, agent),
717
+ dry_run=dry_run,
718
+ environ=environ,
542
719
  )
543
720
  operator = run_operator(
544
721
  mailbox=mailbox, address=address, dry_run=dry_run, home=home, environ=environ
545
722
  )
546
- if EXIT_REFUSED in (mail.code, operator.code):
723
+ defined = run_agent(agent=agent, dry_run=dry_run, home=resolved, environ=environ)
724
+ codes = (mail.code, operator.code, defined.code)
725
+ if EXIT_REFUSED in codes:
547
726
  code = EXIT_REFUSED
548
- elif EXIT_STEPS_LEFT in (mail.code, operator.code):
727
+ elif EXIT_STEPS_LEFT in codes:
549
728
  code = EXIT_STEPS_LEFT
550
729
  else:
551
730
  code = EXIT_OK
552
- return Outcome(code, mail.lines + operator.lines)
731
+ lines = mail.lines + operator.lines + defined.lines
732
+ if dry_run:
733
+ lines.append(
734
+ f"Would ask `{_verb()}` for the agent's box {agent!r} (--box {agent})."
735
+ )
736
+ if defined.code == EXIT_OK:
737
+ lines.append(launch_sentence(agent))
738
+ return Outcome(code, lines)
553
739
 
554
740
 
555
741
  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