pocketshell 0.4.47__tar.gz → 0.5.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.
- {pocketshell-0.4.47 → pocketshell-0.5.0}/PKG-INFO +1 -1
- {pocketshell-0.4.47 → pocketshell-0.5.0}/pyproject.toml +1 -1
- {pocketshell-0.4.47 → pocketshell-0.5.0}/src/pocketshell/agent_log.py +14 -8
- pocketshell-0.5.0/src/pocketshell/config.py +163 -0
- pocketshell-0.5.0/src/pocketshell/session_enum.py +686 -0
- pocketshell-0.5.0/src/pocketshell/sessions.py +1296 -0
- {pocketshell-0.4.47 → pocketshell-0.5.0}/tests/conftest.py +5 -0
- pocketshell-0.5.0/tests/fixtures/aplexer/snapshot-reported-state.json +226 -0
- pocketshell-0.5.0/tests/fixtures/aplexer/snapshot.json +402 -0
- {pocketshell-0.4.47 → pocketshell-0.5.0}/tests/test_send.py +3 -0
- {pocketshell-0.4.47 → pocketshell-0.5.0}/tests/test_session_enum.py +6 -4
- pocketshell-0.5.0/tests/test_session_enum_schema2.py +664 -0
- {pocketshell-0.4.47 → pocketshell-0.5.0}/tests/test_sessions.py +8 -1
- pocketshell-0.5.0/tests/test_sessions_attach.py +480 -0
- pocketshell-0.5.0/tests/test_sessions_create_routing.py +793 -0
- {pocketshell-0.4.47 → pocketshell-0.5.0}/tests/test_usage.py +2 -0
- pocketshell-0.4.47/src/pocketshell/session_enum.py +0 -267
- pocketshell-0.4.47/src/pocketshell/sessions.py +0 -497
- {pocketshell-0.4.47 → pocketshell-0.5.0}/.gitignore +0 -0
- {pocketshell-0.4.47 → pocketshell-0.5.0}/README.md +0 -0
- {pocketshell-0.4.47 → pocketshell-0.5.0}/scheduler/README.md +0 -0
- {pocketshell-0.4.47 → pocketshell-0.5.0}/scheduler/pocketshell-usage-capture.service +0 -0
- {pocketshell-0.4.47 → pocketshell-0.5.0}/scheduler/pocketshell-usage-capture.timer +0 -0
- {pocketshell-0.4.47 → pocketshell-0.5.0}/scripts/sync-readme-exit-codes.py +0 -0
- {pocketshell-0.4.47 → pocketshell-0.5.0}/src/pocketshell/__init__.py +0 -0
- {pocketshell-0.4.47 → pocketshell-0.5.0}/src/pocketshell/__main__.py +0 -0
- {pocketshell-0.4.47 → pocketshell-0.5.0}/src/pocketshell/agent_card_push.py +0 -0
- {pocketshell-0.4.47 → pocketshell-0.5.0}/src/pocketshell/agents.py +0 -0
- {pocketshell-0.4.47 → pocketshell-0.5.0}/src/pocketshell/agents_kind.py +0 -0
- {pocketshell-0.4.47 → pocketshell-0.5.0}/src/pocketshell/aplexer.py +0 -0
- {pocketshell-0.4.47 → pocketshell-0.5.0}/src/pocketshell/cards.py +0 -0
- {pocketshell-0.4.47 → pocketshell-0.5.0}/src/pocketshell/cgroup_agents.py +0 -0
- {pocketshell-0.4.47 → pocketshell-0.5.0}/src/pocketshell/cli.py +0 -0
- {pocketshell-0.4.47 → pocketshell-0.5.0}/src/pocketshell/daemon.py +0 -0
- {pocketshell-0.4.47 → pocketshell-0.5.0}/src/pocketshell/engines.py +0 -0
- {pocketshell-0.4.47 → pocketshell-0.5.0}/src/pocketshell/env.py +0 -0
- {pocketshell-0.4.47 → pocketshell-0.5.0}/src/pocketshell/github.py +0 -0
- {pocketshell-0.4.47 → pocketshell-0.5.0}/src/pocketshell/hooks.py +0 -0
- {pocketshell-0.4.47 → pocketshell-0.5.0}/src/pocketshell/jobs.py +0 -0
- {pocketshell-0.4.47 → pocketshell-0.5.0}/src/pocketshell/logs.py +0 -0
- {pocketshell-0.4.47 → pocketshell-0.5.0}/src/pocketshell/profiles.py +0 -0
- {pocketshell-0.4.47 → pocketshell-0.5.0}/src/pocketshell/prune_attachments.py +0 -0
- {pocketshell-0.4.47 → pocketshell-0.5.0}/src/pocketshell/push.py +0 -0
- {pocketshell-0.4.47 → pocketshell-0.5.0}/src/pocketshell/qr_share.py +0 -0
- {pocketshell-0.4.47 → pocketshell-0.5.0}/src/pocketshell/repos.py +0 -0
- {pocketshell-0.4.47 → pocketshell-0.5.0}/src/pocketshell/resume.py +0 -0
- {pocketshell-0.4.47 → pocketshell-0.5.0}/src/pocketshell/send.py +0 -0
- {pocketshell-0.4.47 → pocketshell-0.5.0}/src/pocketshell/serve.py +0 -0
- {pocketshell-0.4.47 → pocketshell-0.5.0}/src/pocketshell/tree.py +0 -0
- {pocketshell-0.4.47 → pocketshell-0.5.0}/src/pocketshell/usage.py +0 -0
- {pocketshell-0.4.47 → pocketshell-0.5.0}/src/pocketshell/usage_capture.py +0 -0
- {pocketshell-0.4.47 → pocketshell-0.5.0}/src/pocketshell/usage_reset.py +0 -0
- {pocketshell-0.4.47 → pocketshell-0.5.0}/tests/__init__.py +0 -0
- {pocketshell-0.4.47 → pocketshell-0.5.0}/tests/data/quse-0.0.11-usage.json +0 -0
- {pocketshell-0.4.47 → pocketshell-0.5.0}/tests/data/quse-0.0.13-usage.json +0 -0
- {pocketshell-0.4.47 → pocketshell-0.5.0}/tests/data/quse-0.0.15-usage.json +0 -0
- {pocketshell-0.4.47 → pocketshell-0.5.0}/tests/data/quse-0.0.15-usage.ndjson +0 -0
- {pocketshell-0.4.47 → pocketshell-0.5.0}/tests/data/quse-0.0.15-usage.provenance.txt +0 -0
- {pocketshell-0.4.47 → pocketshell-0.5.0}/tests/data/quse-0.0.9-usage.json +0 -0
- {pocketshell-0.4.47 → pocketshell-0.5.0}/tests/data/sshd_anchor/Dockerfile.from-unanchored-base +0 -0
- {pocketshell-0.4.47 → pocketshell-0.5.0}/tests/test_agent_card_push.py +0 -0
- {pocketshell-0.4.47 → pocketshell-0.5.0}/tests/test_agent_log.py +0 -0
- {pocketshell-0.4.47 → pocketshell-0.5.0}/tests/test_agents.py +0 -0
- {pocketshell-0.4.47 → pocketshell-0.5.0}/tests/test_agents_kind.py +0 -0
- {pocketshell-0.4.47 → pocketshell-0.5.0}/tests/test_agents_kind_target.py +0 -0
- {pocketshell-0.4.47 → pocketshell-0.5.0}/tests/test_agents_source_target.py +0 -0
- {pocketshell-0.4.47 → pocketshell-0.5.0}/tests/test_cards.py +0 -0
- {pocketshell-0.4.47 → pocketshell-0.5.0}/tests/test_cards_push_notify.py +0 -0
- {pocketshell-0.4.47 → pocketshell-0.5.0}/tests/test_cgroup_agents.py +0 -0
- {pocketshell-0.4.47 → pocketshell-0.5.0}/tests/test_cli.py +0 -0
- {pocketshell-0.4.47 → pocketshell-0.5.0}/tests/test_daemon.py +0 -0
- {pocketshell-0.4.47 → pocketshell-0.5.0}/tests/test_daemon_fallback.py +0 -0
- {pocketshell-0.4.47 → pocketshell-0.5.0}/tests/test_engines.py +0 -0
- {pocketshell-0.4.47 → pocketshell-0.5.0}/tests/test_env.py +0 -0
- {pocketshell-0.4.47 → pocketshell-0.5.0}/tests/test_github.py +0 -0
- {pocketshell-0.4.47 → pocketshell-0.5.0}/tests/test_hooks.py +0 -0
- {pocketshell-0.4.47 → pocketshell-0.5.0}/tests/test_jobs.py +0 -0
- {pocketshell-0.4.47 → pocketshell-0.5.0}/tests/test_logs.py +0 -0
- {pocketshell-0.4.47 → pocketshell-0.5.0}/tests/test_profiles.py +0 -0
- {pocketshell-0.4.47 → pocketshell-0.5.0}/tests/test_prune_attachments.py +0 -0
- {pocketshell-0.4.47 → pocketshell-0.5.0}/tests/test_push.py +0 -0
- {pocketshell-0.4.47 → pocketshell-0.5.0}/tests/test_qr_share.py +0 -0
- {pocketshell-0.4.47 → pocketshell-0.5.0}/tests/test_repos.py +0 -0
- {pocketshell-0.4.47 → pocketshell-0.5.0}/tests/test_resume.py +0 -0
- {pocketshell-0.4.47 → pocketshell-0.5.0}/tests/test_serve.py +0 -0
- {pocketshell-0.4.47 → pocketshell-0.5.0}/tests/test_ssh_fixture_penalties.py +0 -0
- {pocketshell-0.4.47 → pocketshell-0.5.0}/tests/test_tree.py +0 -0
- {pocketshell-0.4.47 → pocketshell-0.5.0}/tests/test_usage_capture.py +0 -0
- {pocketshell-0.4.47 → pocketshell-0.5.0}/tests/test_usage_reset.py +0 -0
- {pocketshell-0.4.47 → pocketshell-0.5.0}/tests/test_watch_ci.py +0 -0
- {pocketshell-0.4.47 → pocketshell-0.5.0}/tests/test_watch_ci_cancelled.py +0 -0
- {pocketshell-0.4.47 → pocketshell-0.5.0}/uv.lock +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: pocketshell
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
4
4
|
Summary: Unified server-side Python utility for the PocketShell Android client.
|
|
5
5
|
Project-URL: Homepage, https://github.com/alexeygrigorev/pocketshell
|
|
6
6
|
Project-URL: Issues, https://github.com/alexeygrigorev/pocketshell/issues
|
|
@@ -14,7 +14,7 @@ name = "pocketshell"
|
|
|
14
14
|
# scripts/check-pypi-version.sh enforces the stamped value matches the tag;
|
|
15
15
|
# scripts/check-version-coupling.sh enforces the derivation itself is
|
|
16
16
|
# single-sourced. See tools/pocketshell/README.md ("Version coupling").
|
|
17
|
-
version = "0.
|
|
17
|
+
version = "0.5.0"
|
|
18
18
|
description = "Unified server-side Python utility for the PocketShell Android client."
|
|
19
19
|
readme = "README.md"
|
|
20
20
|
requires-python = ">=3.11"
|
|
@@ -539,6 +539,19 @@ def _grok_messages_from_row(row: dict[str, Any]) -> List[HandoffMessage]:
|
|
|
539
539
|
return []
|
|
540
540
|
|
|
541
541
|
|
|
542
|
+
def _handoff_messages_from_row(engine: str, row: dict[str, Any]) -> List[HandoffMessage]:
|
|
543
|
+
"""Dispatch one parsed JSONL row to its per-engine message extractor."""
|
|
544
|
+
if engine == "claude":
|
|
545
|
+
return _claude_messages_from_row(row)
|
|
546
|
+
if engine == "codex":
|
|
547
|
+
return _codex_messages_from_row(row)
|
|
548
|
+
if engine == "opencode":
|
|
549
|
+
return _opencode_messages_from_row(row)
|
|
550
|
+
if engine == "grok":
|
|
551
|
+
return _grok_messages_from_row(row)
|
|
552
|
+
return []
|
|
553
|
+
|
|
554
|
+
|
|
542
555
|
def _handoff_messages_from_lines(engine: str, lines: Iterable[str]) -> List[HandoffMessage]:
|
|
543
556
|
"""Extract user/assistant prose from raw agent JSONL rows."""
|
|
544
557
|
messages: List[HandoffMessage] = []
|
|
@@ -546,14 +559,7 @@ def _handoff_messages_from_lines(engine: str, lines: Iterable[str]) -> List[Hand
|
|
|
546
559
|
row = _parse_json_line(line)
|
|
547
560
|
if row is None:
|
|
548
561
|
continue
|
|
549
|
-
|
|
550
|
-
messages.extend(_claude_messages_from_row(row))
|
|
551
|
-
elif engine == "codex":
|
|
552
|
-
messages.extend(_codex_messages_from_row(row))
|
|
553
|
-
elif engine == "opencode":
|
|
554
|
-
messages.extend(_opencode_messages_from_row(row))
|
|
555
|
-
elif engine == "grok":
|
|
556
|
-
messages.extend(_grok_messages_from_row(row))
|
|
562
|
+
messages.extend(_handoff_messages_from_row(engine, row))
|
|
557
563
|
return messages
|
|
558
564
|
|
|
559
565
|
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"""Host-level PocketShell CLI configuration (``~/.config/pocketshell/config.toml``).
|
|
2
|
+
|
|
3
|
+
Today this file carries exactly one section — ``[backends]`` — the per-host
|
|
4
|
+
policy that decides which session backend a *new* session is created on:
|
|
5
|
+
|
|
6
|
+
.. code-block:: toml
|
|
7
|
+
|
|
8
|
+
[backends]
|
|
9
|
+
agent = "tmux" # sessions created with --engine
|
|
10
|
+
shell = "tmux" # plain shell sessions
|
|
11
|
+
|
|
12
|
+
Why a pocketshell-owned config file rather than a flag or a backend-side
|
|
13
|
+
setting: routing is deployment policy that spans BOTH backends, and neither
|
|
14
|
+
backend can answer "not me" (see the simplification plan, §"Backend routing
|
|
15
|
+
policy"). The phone never sends a backend; the host decides.
|
|
16
|
+
|
|
17
|
+
Defaults are ``tmux`` for both keys. Flipping ``agent = "aplexer"`` is a
|
|
18
|
+
deliberate host config change the maintainer makes — never a code change and
|
|
19
|
+
never an automatic upgrade.
|
|
20
|
+
|
|
21
|
+
Parsing rules (deliberate, see ``load_config``):
|
|
22
|
+
|
|
23
|
+
* missing file / empty file -> defaults;
|
|
24
|
+
* malformed TOML, a non-table ``[backends]``, or an unknown backend value ->
|
|
25
|
+
:class:`ConfigError`, i.e. **fail loud**. Silently falling back to the
|
|
26
|
+
defaults would route agent sessions to tmux while the host owner believes
|
|
27
|
+
aplexer is switched on — a wrong-path failure that shows up as "the feature
|
|
28
|
+
just doesn't work", which is exactly the class of silent misroute this
|
|
29
|
+
project rejects. A typo in a two-line file is cheap to fix once it is
|
|
30
|
+
reported; a silent misroute is not.
|
|
31
|
+
|
|
32
|
+
``tomllib`` is used unconditionally: this package's ``requires-python`` is
|
|
33
|
+
``>=3.11``, where ``tomllib`` is stdlib, so there is no ``tomli`` fallback to
|
|
34
|
+
maintain (D22: no compatibility shims).
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
from __future__ import annotations
|
|
38
|
+
|
|
39
|
+
import os
|
|
40
|
+
import tomllib
|
|
41
|
+
from pathlib import Path
|
|
42
|
+
from typing import Any, Mapping, Optional
|
|
43
|
+
|
|
44
|
+
#: Session backends this CLI knows how to create on.
|
|
45
|
+
BACKEND_TMUX = "tmux"
|
|
46
|
+
BACKEND_APLEXER = "aplexer"
|
|
47
|
+
BACKENDS = (BACKEND_TMUX, BACKEND_APLEXER)
|
|
48
|
+
|
|
49
|
+
#: Backend used when the config file says nothing.
|
|
50
|
+
DEFAULT_BACKEND = BACKEND_TMUX
|
|
51
|
+
|
|
52
|
+
#: ``[backends]`` keys: ``agent`` covers a session launched with an engine,
|
|
53
|
+
#: ``shell`` a plain shell session.
|
|
54
|
+
BACKEND_KEY_AGENT = "agent"
|
|
55
|
+
BACKEND_KEY_SHELL = "shell"
|
|
56
|
+
BACKEND_KEYS = (BACKEND_KEY_AGENT, BACKEND_KEY_SHELL)
|
|
57
|
+
|
|
58
|
+
DEFAULT_BACKENDS: dict[str, str] = {
|
|
59
|
+
BACKEND_KEY_AGENT: DEFAULT_BACKEND,
|
|
60
|
+
BACKEND_KEY_SHELL: DEFAULT_BACKEND,
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class ConfigError(RuntimeError):
|
|
65
|
+
"""``config.toml`` exists but cannot be trusted (malformed / bad value)."""
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _env_source(env: Optional[Mapping[str, str]] = None) -> Mapping[str, str]:
|
|
69
|
+
return env if env is not None else os.environ
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _home_dir(env: Optional[Mapping[str, str]] = None) -> Path:
|
|
73
|
+
"""Resolve ``$HOME`` (honouring an injected env for tests)."""
|
|
74
|
+
home = _env_source(env).get("HOME")
|
|
75
|
+
if home:
|
|
76
|
+
return Path(home)
|
|
77
|
+
return Path(os.path.expanduser("~"))
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def config_path(env: Optional[Mapping[str, str]] = None) -> Path:
|
|
81
|
+
"""``$XDG_CONFIG_HOME``/``~/.config`` + ``pocketshell/config.toml``.
|
|
82
|
+
|
|
83
|
+
Same resolution order as ``profiles.py``'s ``profiles.yaml`` so a host
|
|
84
|
+
keeps all PocketShell config in one directory.
|
|
85
|
+
"""
|
|
86
|
+
xdg = _env_source(env).get("XDG_CONFIG_HOME")
|
|
87
|
+
base = Path(xdg) if xdg else _home_dir(env) / ".config"
|
|
88
|
+
return base / "pocketshell" / "config.toml"
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def default_config() -> dict[str, Any]:
|
|
92
|
+
"""A fresh copy of the fully-defaulted config document."""
|
|
93
|
+
return {"backends": dict(DEFAULT_BACKENDS)}
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _normalize_backends(raw: Any, *, path: Path) -> dict[str, str]:
|
|
97
|
+
"""Validate the ``[backends]`` table and merge it over the defaults."""
|
|
98
|
+
backends = dict(DEFAULT_BACKENDS)
|
|
99
|
+
if raw is None:
|
|
100
|
+
return backends
|
|
101
|
+
if not isinstance(raw, Mapping):
|
|
102
|
+
raise ConfigError(
|
|
103
|
+
f"{path}: [backends] must be a table, got {type(raw).__name__}"
|
|
104
|
+
)
|
|
105
|
+
for key, value in raw.items():
|
|
106
|
+
if key not in BACKEND_KEYS:
|
|
107
|
+
raise ConfigError(
|
|
108
|
+
f"{path}: unknown key [backends].{key} "
|
|
109
|
+
f"(known keys: {', '.join(BACKEND_KEYS)})"
|
|
110
|
+
)
|
|
111
|
+
if not isinstance(value, str) or value.strip() not in BACKENDS:
|
|
112
|
+
raise ConfigError(
|
|
113
|
+
f"{path}: [backends].{key} must be one of "
|
|
114
|
+
f"{', '.join(BACKENDS)}, got {value!r}"
|
|
115
|
+
)
|
|
116
|
+
backends[key] = value.strip()
|
|
117
|
+
return backends
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def load_config(env: Optional[Mapping[str, str]] = None) -> dict[str, Any]:
|
|
121
|
+
"""Read ``~/.config/pocketshell/config.toml``.
|
|
122
|
+
|
|
123
|
+
Returns the parsed document with a fully-populated ``backends`` table
|
|
124
|
+
(every key present, defaults filled in) so callers never have to probe for
|
|
125
|
+
key presence. Any other top-level table the file happens to carry is
|
|
126
|
+
passed through untouched — this loader owns ``[backends]`` only.
|
|
127
|
+
|
|
128
|
+
A missing file returns the defaults. A file that exists but is malformed
|
|
129
|
+
(bad TOML, non-table ``[backends]``, unknown key, unknown backend value,
|
|
130
|
+
unreadable) raises :class:`ConfigError` — see the module docstring for why
|
|
131
|
+
this fails loud instead of degrading to defaults.
|
|
132
|
+
"""
|
|
133
|
+
path = config_path(env)
|
|
134
|
+
if not path.is_file():
|
|
135
|
+
return default_config()
|
|
136
|
+
try:
|
|
137
|
+
raw_bytes = path.read_bytes()
|
|
138
|
+
except OSError as exc:
|
|
139
|
+
raise ConfigError(f"{path}: cannot be read: {exc}") from exc
|
|
140
|
+
try:
|
|
141
|
+
document = tomllib.loads(raw_bytes.decode("utf-8"))
|
|
142
|
+
except (UnicodeDecodeError, tomllib.TOMLDecodeError) as exc:
|
|
143
|
+
raise ConfigError(f"{path}: is not valid TOML: {exc}") from exc
|
|
144
|
+
if not isinstance(document, dict): # pragma: no cover - tomllib invariant
|
|
145
|
+
raise ConfigError(f"{path}: top level must be a table")
|
|
146
|
+
normalized = dict(document)
|
|
147
|
+
normalized["backends"] = _normalize_backends(document.get("backends"), path=path)
|
|
148
|
+
return normalized
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def backend_for(config: Mapping[str, Any], key: str) -> str:
|
|
152
|
+
"""Read one ``[backends]`` key out of a (possibly partial) config dict.
|
|
153
|
+
|
|
154
|
+
Tolerant on purpose: :func:`load_config` is the strict validator, and this
|
|
155
|
+
accessor is also handed hand-built dicts (tests, callers that skipped the
|
|
156
|
+
loader). Anything missing or not a usable string resolves to
|
|
157
|
+
:data:`DEFAULT_BACKEND`.
|
|
158
|
+
"""
|
|
159
|
+
backends = config.get("backends") if isinstance(config, Mapping) else None
|
|
160
|
+
value = backends.get(key) if isinstance(backends, Mapping) else None
|
|
161
|
+
if isinstance(value, str) and value.strip():
|
|
162
|
+
return value.strip()
|
|
163
|
+
return DEFAULT_BACKEND
|