sessiontools 0.0.1__py3-none-any.whl

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,41 @@
1
+ """sessiontools - the harness layer: sessions, their status, and how long
2
+ they have been idle - one driver per harness, a library first.
3
+
4
+ Import it; do not look for a command. There is no console script until the
5
+ cutover (Mike, voice 12:18 Thu 2026-09-17), and the one that arrives then
6
+ is `session`, whose verbs the fleet's callers are already written to.
7
+
8
+ from sessiontools import sessions, find, idle_seconds
9
+
10
+ Every function speaks OUR words - busy, not busy, unknown - never a
11
+ harness's. The first driver is `claude_code`; the day a harness changes a
12
+ status word, that driver changes and nothing above it does.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ from importlib.metadata import PackageNotFoundError, version
18
+
19
+ from sessiontools.claude_code import (
20
+ SIDECAR_DIR_ENV,
21
+ Session,
22
+ find,
23
+ idle_seconds,
24
+ sessions,
25
+ sidecar_dir,
26
+ )
27
+
28
+ try:
29
+ __version__ = version("sessiontools")
30
+ except PackageNotFoundError: # a checkout that is not installed
31
+ __version__ = "0+unknown"
32
+
33
+ __all__ = [
34
+ "SIDECAR_DIR_ENV",
35
+ "Session",
36
+ "__version__",
37
+ "find",
38
+ "idle_seconds",
39
+ "sessions",
40
+ "sidecar_dir",
41
+ ]
@@ -0,0 +1,118 @@
1
+ """Claude Code - the first driver.
2
+
3
+ Claude Code writes one sidecar per live session, `<dir>/<pid>.json`, and
4
+ keeps its `status` and `updatedAt` current as the session works. Three of
5
+ our tools each grew a private reader of that file (sessionmail's
6
+ `_seat._sidecar_idle`, taskmaster's `agents/discovery.py`, `session get
7
+ idle`); this is the one copy they share.
8
+
9
+ The words are the harness's, not ours. Claude Code says `busy`, `shell`,
10
+ `idle`, `waiting`; a driver translates: busy is busy, anything else is
11
+ not, and no sidecar is unknown. Nothing above this module should ever
12
+ see a harness's vocabulary (Cora, by mail 11:50 Thu 2026-09-17: "the API
13
+ must not speak Claude Code's vocabulary ... the day a status word
14
+ changes, one driver changes and nothing else does").
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import json
20
+ import os
21
+ import time
22
+ from dataclasses import dataclass
23
+ from pathlib import Path
24
+
25
+ HARNESS = "claude-code"
26
+
27
+ #: Where the sidecars live. The same rule every reader already used:
28
+ #: `SESSION_SIDECAR_DIR`, else `~/.claude/sessions`.
29
+ SIDECAR_DIR_ENV = "SESSION_SIDECAR_DIR"
30
+
31
+
32
+ def sidecar_dir(env: dict[str, str] | None = None) -> Path:
33
+ env = os.environ if env is None else env
34
+ return Path(env.get(SIDECAR_DIR_ENV) or (Path.home() / ".claude" / "sessions"))
35
+
36
+
37
+ @dataclass(frozen=True)
38
+ class Session:
39
+ """One live session as the harness reports it, in our words."""
40
+
41
+ id: str
42
+ harness: str
43
+ busy: bool
44
+ updated_at: float # epoch seconds, when status last changed
45
+ pid: int | None = None
46
+ cwd: str | None = None
47
+ name: str | None = None
48
+ agent: str | None = None
49
+ raw_status: str | None = None # the harness's own word, for the curious
50
+ path: Path | None = None # the sidecar it came from
51
+
52
+ def idle_seconds(self, now: float | None = None) -> float:
53
+ """0 while busy; else seconds since the status last changed."""
54
+ if self.busy:
55
+ return 0.0
56
+ now = time.time() if now is None else now
57
+ return max(0.0, now - self.updated_at)
58
+
59
+
60
+ def _read(path: Path) -> Session | None:
61
+ """One sidecar -> one Session, or None when it is not one: unreadable,
62
+ not JSON, not a dict, or missing the two fields a verdict needs."""
63
+ try:
64
+ data = json.loads(path.read_text())
65
+ except (OSError, ValueError):
66
+ return None
67
+ if not isinstance(data, dict):
68
+ return None
69
+ sid = data.get("sessionId")
70
+ status = data.get("status")
71
+ updated = data.get("updatedAt")
72
+ if not isinstance(sid, str) or not status or updated is None:
73
+ return None
74
+ try:
75
+ updated_at = float(updated) / 1000.0
76
+ except (TypeError, ValueError):
77
+ return None
78
+ return Session(
79
+ id=sid,
80
+ harness=HARNESS,
81
+ busy=(status == "busy"),
82
+ updated_at=updated_at,
83
+ pid=data.get("pid") if isinstance(data.get("pid"), int) else None,
84
+ cwd=data.get("cwd") if isinstance(data.get("cwd"), str) else None,
85
+ name=data.get("name") if isinstance(data.get("name"), str) else None,
86
+ agent=data.get("agent") if isinstance(data.get("agent"), str) else None,
87
+ raw_status=status if isinstance(status, str) else None,
88
+ path=path,
89
+ )
90
+
91
+
92
+ def sessions(env: dict[str, str] | None = None) -> list[Session]:
93
+ """Every session with a readable sidecar, newest status first. A
94
+ directory that is missing or unreadable is an empty list, not an
95
+ error: no harness here is a fact, not a fault."""
96
+ try:
97
+ paths = sorted(sidecar_dir(env).glob("*.json"))
98
+ except OSError:
99
+ return []
100
+ found = [s for s in (_read(p) for p in paths) if s is not None]
101
+ return sorted(found, key=lambda s: s.updated_at, reverse=True)
102
+
103
+
104
+ def find(session_id: str, env: dict[str, str] | None = None) -> Session | None:
105
+ """The session with this id, or None when no sidecar names it."""
106
+ for s in sessions(env):
107
+ if s.id == session_id:
108
+ return s
109
+ return None
110
+
111
+
112
+ def idle_seconds(session_id: str, now: float | None = None,
113
+ env: dict[str, str] | None = None) -> float | None:
114
+ """The one number the pager wants: 0 while busy, else seconds since
115
+ the status last changed; None when nothing can be said. Same rule as
116
+ every reader it replaces."""
117
+ s = find(session_id, env)
118
+ return None if s is None else s.idle_seconds(now)
sessiontools/cli.py ADDED
@@ -0,0 +1,14 @@
1
+ """The `session` command - NOT YET, by ruling.
2
+
3
+ Mike, voice 12:18 Thu 2026-09-17: *"if sessionmail only consumes it via the
4
+ library, then we don't need to put the session command from sessiontools on
5
+ our path today. We can put that on when we've migrated all the other
6
+ commands across."*
7
+
8
+ So this module is a placeholder on purpose: no click, no `main`, nothing
9
+ `pyproject.toml` points at. When the cutover comes, the command is
10
+ `session` - `session list`, `session get idle` - the verbs the fleet's
11
+ callers (sessionview, voicemode, the shell aliases) are already written to,
12
+ and exactly one package owns it on the PATH. See
13
+ `openspec/changes/the-session-command`.
14
+ """
sessiontools/py.typed ADDED
File without changes
@@ -0,0 +1,7 @@
1
+ Metadata-Version: 2.3
2
+ Name: sessiontools
3
+ Version: 0.0.1
4
+ Summary: The harness layer: sessions, status and idle, one driver per harness - a library first
5
+ Author: Cora 7
6
+ Author-email: Cora 7 <ai-cora@failmode.com>
7
+ Requires-Python: >=3.13
@@ -0,0 +1,7 @@
1
+ sessiontools/__init__.py,sha256=x2tOf_mbxQ99QotdQjTXwebDdR25ZrrLtNHpvYTWWKY,1139
2
+ sessiontools/claude_code.py,sha256=L3tV8tDLJGRkJOj-TzIU5eszp1OCgTPGkcueRLy-JC0,4348
3
+ sessiontools/cli.py,sha256=edmbbztERL6DADr08yrqdhO45OEjU9Hf1eunLqhY6go,669
4
+ sessiontools/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
+ sessiontools-0.0.1.dist-info/WHEEL,sha256=1uB4K0BXH0KDzZOLGYqB4NnSgGon9oosxpA-QGkDuEA,80
6
+ sessiontools-0.0.1.dist-info/METADATA,sha256=r1v0DaZDKPDjLJGimGtZQbrFKKd-fV8q9mBy4J694yM,235
7
+ sessiontools-0.0.1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: uv 0.11.9
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any