pocketshell 0.5.3__tar.gz → 0.5.5__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.
Files changed (118) hide show
  1. pocketshell-0.5.5/LICENSE +21 -0
  2. pocketshell-0.5.5/PKG-INFO +404 -0
  3. pocketshell-0.5.5/README.md +373 -0
  4. {pocketshell-0.5.3 → pocketshell-0.5.5}/pyproject.toml +30 -55
  5. {pocketshell-0.5.3 → pocketshell-0.5.5}/scheduler/pocketshell-usage-capture.service +1 -1
  6. {pocketshell-0.5.3 → pocketshell-0.5.5}/scheduler/pocketshell-usage-capture.timer +1 -1
  7. pocketshell-0.5.5/scripts/check-cgroup-cap-proof.sh +191 -0
  8. pocketshell-0.5.5/scripts/check-product-tmux-absent.sh +62 -0
  9. pocketshell-0.5.5/scripts/check-python-ci-lock.sh +73 -0
  10. {pocketshell-0.5.3 → pocketshell-0.5.5}/scripts/sync-readme-exit-codes.py +2 -2
  11. pocketshell-0.5.5/scripts/test-usage-missing-window-mutation.sh +215 -0
  12. {pocketshell-0.5.3 → pocketshell-0.5.5}/src/pocketshell/__init__.py +2 -3
  13. {pocketshell-0.5.3 → pocketshell-0.5.5}/src/pocketshell/agent_card_push.py +1 -1
  14. {pocketshell-0.5.3 → pocketshell-0.5.5}/src/pocketshell/agent_log.py +1 -1
  15. {pocketshell-0.5.3 → pocketshell-0.5.5}/src/pocketshell/agents.py +26 -333
  16. {pocketshell-0.5.3 → pocketshell-0.5.5}/src/pocketshell/agents_kind.py +3 -3
  17. {pocketshell-0.5.3 → pocketshell-0.5.5}/src/pocketshell/aplexer.py +1 -1
  18. {pocketshell-0.5.3 → pocketshell-0.5.5}/src/pocketshell/cards.py +20 -49
  19. {pocketshell-0.5.3 → pocketshell-0.5.5}/src/pocketshell/cgroup_agents.py +9 -12
  20. {pocketshell-0.5.3 → pocketshell-0.5.5}/src/pocketshell/cli.py +8 -16
  21. {pocketshell-0.5.3 → pocketshell-0.5.5}/src/pocketshell/daemon.py +7 -69
  22. {pocketshell-0.5.3 → pocketshell-0.5.5}/src/pocketshell/engines.py +2 -2
  23. {pocketshell-0.5.3 → pocketshell-0.5.5}/src/pocketshell/env.py +1 -1
  24. {pocketshell-0.5.3 → pocketshell-0.5.5}/src/pocketshell/hooks.py +0 -79
  25. {pocketshell-0.5.3 → pocketshell-0.5.5}/src/pocketshell/memcap.py +13 -36
  26. pocketshell-0.5.5/src/pocketshell/session_enum.py +374 -0
  27. pocketshell-0.5.5/src/pocketshell/sessions.py +692 -0
  28. {pocketshell-0.5.3 → pocketshell-0.5.5}/src/pocketshell/tree.py +23 -59
  29. pocketshell-0.5.5/src/pocketshell/workspaces.py +224 -0
  30. {pocketshell-0.5.3 → pocketshell-0.5.5}/tests/conftest.py +0 -5
  31. {pocketshell-0.5.3 → pocketshell-0.5.5}/tests/test_agent_card_push.py +1 -4
  32. {pocketshell-0.5.3 → pocketshell-0.5.5}/tests/test_agents.py +1 -606
  33. {pocketshell-0.5.3 → pocketshell-0.5.5}/tests/test_agents_kind.py +17 -17
  34. {pocketshell-0.5.3 → pocketshell-0.5.5}/tests/test_aplexer_resolution.py +7 -4
  35. {pocketshell-0.5.3 → pocketshell-0.5.5}/tests/test_cards.py +17 -17
  36. {pocketshell-0.5.3 → pocketshell-0.5.5}/tests/test_cards_push_notify.py +0 -1
  37. {pocketshell-0.5.3 → pocketshell-0.5.5}/tests/test_cgroup_agents.py +18 -18
  38. {pocketshell-0.5.3 → pocketshell-0.5.5}/tests/test_daemon.py +3 -47
  39. {pocketshell-0.5.3 → pocketshell-0.5.5}/tests/test_daemon_fallback.py +3 -7
  40. {pocketshell-0.5.3 → pocketshell-0.5.5}/tests/test_engines.py +3 -3
  41. {pocketshell-0.5.3 → pocketshell-0.5.5}/tests/test_hooks.py +7 -84
  42. pocketshell-0.5.3/tests/test_sessions_mem_cap.py → pocketshell-0.5.5/tests/test_session_cgroup_kernel_proof.py +186 -609
  43. pocketshell-0.5.5/tests/test_session_enum.py +93 -0
  44. pocketshell-0.5.5/tests/test_sessions.py +54 -0
  45. pocketshell-0.5.5/tests/test_sessions_attach.py +55 -0
  46. pocketshell-0.5.5/tests/test_sessions_create_routing.py +90 -0
  47. pocketshell-0.5.5/tests/test_sessions_kill.py +57 -0
  48. pocketshell-0.5.5/tests/test_sessions_mem_cap.py +38 -0
  49. {pocketshell-0.5.3 → pocketshell-0.5.5}/tests/test_tree.py +13 -38
  50. {pocketshell-0.5.3 → pocketshell-0.5.5}/tests/test_usage.py +7 -55
  51. pocketshell-0.5.5/tests/test_workspaces.py +98 -0
  52. {pocketshell-0.5.3 → pocketshell-0.5.5}/uv.lock +10 -200
  53. pocketshell-0.5.3/PKG-INFO +0 -628
  54. pocketshell-0.5.3/README.md +0 -595
  55. pocketshell-0.5.3/src/pocketshell/config.py +0 -163
  56. pocketshell-0.5.3/src/pocketshell/jobs.py +0 -829
  57. pocketshell-0.5.3/src/pocketshell/qr_share.py +0 -432
  58. pocketshell-0.5.3/src/pocketshell/resume.py +0 -698
  59. pocketshell-0.5.3/src/pocketshell/send.py +0 -1495
  60. pocketshell-0.5.3/src/pocketshell/session_enum.py +0 -905
  61. pocketshell-0.5.3/src/pocketshell/sessions.py +0 -1991
  62. pocketshell-0.5.3/tests/test_agents_fixture_aplexer.py +0 -714
  63. pocketshell-0.5.3/tests/test_agents_kind_target.py +0 -280
  64. pocketshell-0.5.3/tests/test_agents_source_target.py +0 -426
  65. pocketshell-0.5.3/tests/test_jobs.py +0 -896
  66. pocketshell-0.5.3/tests/test_qr_share.py +0 -327
  67. pocketshell-0.5.3/tests/test_resume.py +0 -558
  68. pocketshell-0.5.3/tests/test_send.py +0 -2715
  69. pocketshell-0.5.3/tests/test_session_enum.py +0 -144
  70. pocketshell-0.5.3/tests/test_session_enum_schema2.py +0 -1226
  71. pocketshell-0.5.3/tests/test_sessions.py +0 -448
  72. pocketshell-0.5.3/tests/test_sessions_attach.py +0 -652
  73. pocketshell-0.5.3/tests/test_sessions_create_routing.py +0 -1284
  74. pocketshell-0.5.3/tests/test_sessions_kill.py +0 -748
  75. pocketshell-0.5.3/tests/test_ssh_fixture_penalties.py +0 -1009
  76. pocketshell-0.5.3/tests/test_watch_ci.py +0 -948
  77. pocketshell-0.5.3/tests/test_watch_ci_cancelled.py +0 -745
  78. {pocketshell-0.5.3 → pocketshell-0.5.5}/.gitignore +0 -0
  79. {pocketshell-0.5.3 → pocketshell-0.5.5}/scheduler/README.md +0 -0
  80. {pocketshell-0.5.3 → pocketshell-0.5.5}/src/pocketshell/__main__.py +0 -0
  81. {pocketshell-0.5.3 → pocketshell-0.5.5}/src/pocketshell/github.py +0 -0
  82. {pocketshell-0.5.3 → pocketshell-0.5.5}/src/pocketshell/logs.py +0 -0
  83. {pocketshell-0.5.3 → pocketshell-0.5.5}/src/pocketshell/profiles.py +0 -0
  84. {pocketshell-0.5.3 → pocketshell-0.5.5}/src/pocketshell/prune_attachments.py +0 -0
  85. {pocketshell-0.5.3 → pocketshell-0.5.5}/src/pocketshell/push.py +0 -0
  86. {pocketshell-0.5.3 → pocketshell-0.5.5}/src/pocketshell/repos.py +0 -0
  87. {pocketshell-0.5.3 → pocketshell-0.5.5}/src/pocketshell/serve.py +0 -0
  88. {pocketshell-0.5.3 → pocketshell-0.5.5}/src/pocketshell/usage.py +0 -0
  89. {pocketshell-0.5.3 → pocketshell-0.5.5}/src/pocketshell/usage_capture.py +0 -0
  90. {pocketshell-0.5.3 → pocketshell-0.5.5}/src/pocketshell/usage_reset.py +0 -0
  91. {pocketshell-0.5.3 → pocketshell-0.5.5}/tests/__init__.py +0 -0
  92. {pocketshell-0.5.3 → pocketshell-0.5.5}/tests/data/quse-0.0.11-usage.json +0 -0
  93. {pocketshell-0.5.3 → pocketshell-0.5.5}/tests/data/quse-0.0.13-usage.json +0 -0
  94. {pocketshell-0.5.3 → pocketshell-0.5.5}/tests/data/quse-0.0.15-usage.json +0 -0
  95. {pocketshell-0.5.3 → pocketshell-0.5.5}/tests/data/quse-0.0.15-usage.ndjson +0 -0
  96. {pocketshell-0.5.3 → pocketshell-0.5.5}/tests/data/quse-0.0.15-usage.provenance.txt +0 -0
  97. {pocketshell-0.5.3 → pocketshell-0.5.5}/tests/data/quse-0.0.9-usage.json +0 -0
  98. {pocketshell-0.5.3 → pocketshell-0.5.5}/tests/data/sshd_anchor/Dockerfile.from-unanchored-base +0 -0
  99. {pocketshell-0.5.3 → pocketshell-0.5.5}/tests/fixtures/aplexer/snapshot-agent.json +0 -0
  100. {pocketshell-0.5.3 → pocketshell-0.5.5}/tests/fixtures/aplexer/snapshot-crashed-start.json +0 -0
  101. {pocketshell-0.5.3 → pocketshell-0.5.5}/tests/fixtures/aplexer/snapshot-liveness-mix.json +0 -0
  102. {pocketshell-0.5.3 → pocketshell-0.5.5}/tests/fixtures/aplexer/snapshot-mid-create.json +0 -0
  103. {pocketshell-0.5.3 → pocketshell-0.5.5}/tests/fixtures/aplexer/snapshot-post-kill-window.json +0 -0
  104. {pocketshell-0.5.3 → pocketshell-0.5.5}/tests/fixtures/aplexer/snapshot-reported-state.json +0 -0
  105. {pocketshell-0.5.3 → pocketshell-0.5.5}/tests/fixtures/aplexer/snapshot.json +0 -0
  106. {pocketshell-0.5.3 → pocketshell-0.5.5}/tests/test_agent_log.py +0 -0
  107. {pocketshell-0.5.3 → pocketshell-0.5.5}/tests/test_aplexer_contract.py +0 -0
  108. {pocketshell-0.5.3 → pocketshell-0.5.5}/tests/test_cli.py +0 -0
  109. {pocketshell-0.5.3 → pocketshell-0.5.5}/tests/test_env.py +0 -0
  110. {pocketshell-0.5.3 → pocketshell-0.5.5}/tests/test_github.py +0 -0
  111. {pocketshell-0.5.3 → pocketshell-0.5.5}/tests/test_logs.py +0 -0
  112. {pocketshell-0.5.3 → pocketshell-0.5.5}/tests/test_profiles.py +0 -0
  113. {pocketshell-0.5.3 → pocketshell-0.5.5}/tests/test_prune_attachments.py +0 -0
  114. {pocketshell-0.5.3 → pocketshell-0.5.5}/tests/test_push.py +0 -0
  115. {pocketshell-0.5.3 → pocketshell-0.5.5}/tests/test_repos.py +0 -0
  116. {pocketshell-0.5.3 → pocketshell-0.5.5}/tests/test_serve.py +0 -0
  117. {pocketshell-0.5.3 → pocketshell-0.5.5}/tests/test_usage_capture.py +0 -0
  118. {pocketshell-0.5.3 → pocketshell-0.5.5}/tests/test_usage_reset.py +0 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Alexey Grigorev
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,404 @@
1
+ Metadata-Version: 2.5
2
+ Name: pocketshell
3
+ Version: 0.5.5
4
+ Summary: Unified server-side Python utility for the PocketShell Android and desktop clients.
5
+ Project-URL: Homepage, https://github.com/PocketShell-io/pocketshell-cli
6
+ Project-URL: Issues, https://github.com/PocketShell-io/pocketshell-cli/issues
7
+ Author: Alexey Grigorev
8
+ License: MIT
9
+ License-File: LICENSE
10
+ Keywords: agents,pocketshell,ssh,usage
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Environment :: Console
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Topic :: Software Development
20
+ Classifier: Topic :: System :: Monitoring
21
+ Requires-Python: >=3.11
22
+ Requires-Dist: aplexer==0.1.5; sys_platform == 'linux'
23
+ Requires-Dist: click>=8.2.0
24
+ Requires-Dist: google-auth>=2.0.0
25
+ Requires-Dist: pyyaml>=6.0
26
+ Requires-Dist: quse==0.0.15
27
+ Provides-Extra: dev
28
+ Requires-Dist: pytest>=8.4.0; extra == 'dev'
29
+ Requires-Dist: ruff>=0.15.0; extra == 'dev'
30
+ Description-Content-Type: text/markdown
31
+
32
+ # pocketshell
33
+
34
+ Unified server-side Python utility for the [PocketShell](https://github.com/PocketShell-io/pocketshell)
35
+ Android and desktop clients. This is the host-side companion CLI, maintained in its own
36
+ repo with an independent release cycle; before v0.5.5 it lived at
37
+ `tools/pocketshell/` in the [monorepo](https://github.com/PocketShell-io/pocketshell)
38
+ (versioning was coupled to the app tag there — see [Release flow](#release-flow)). The app probes for this single helper on each remote
39
+ host and uses its subcommands for usage, aplexer session lifecycle, agent
40
+ conversations, repository discovery, environment files, hooks,
41
+ logs, and daemon lifecycle checks.
42
+
43
+ ## Durable workspaces
44
+
45
+ The Quiet workspace-first client uses the host-side workspace membership
46
+ contract before it has any live session to enumerate:
47
+
48
+ ```text
49
+ pocketshell workspaces list --host <host> --json
50
+ pocketshell workspaces add <path> --host <host> --json
51
+ pocketshell workspaces remove <path> --host <host> --json
52
+ ```
53
+
54
+ Membership is stored in the existing private tree registry. Each entry has a
55
+ canonical absolute `path` for identity and a separate `display_path` for the
56
+ path spelling shown in the UI. Adding or removing the same path repeatedly is
57
+ safe, and `list` retains empty workspaces.
58
+
59
+ ## Install
60
+
61
+ The recommended path is `uv tool install`, which lands the binary on PATH
62
+ under `~/.local/bin/`:
63
+
64
+ ```bash
65
+ uv tool install pocketshell
66
+ ```
67
+
68
+ For local development from a clone:
69
+
70
+ ```bash
71
+ cd pocketshell-cli
72
+ uv venv
73
+ uv pip install -e .
74
+ pocketshell --help
75
+ ```
76
+
77
+ `pipx install pocketshell` works the same way for users who prefer
78
+ pipx. Both install paths produce a `pocketshell` binary that the
79
+ PocketShell app's bootstrap probe detects.
80
+
81
+ ## Usage
82
+
83
+ Top-level commands in the current helper:
84
+
85
+ ```text
86
+ pocketshell usage [provider] [--json] # provider quota / usage
87
+ pocketshell sessions list --json # schema-3 aplexer session rows
88
+ pocketshell sessions create NAME --json # create or reuse a session
89
+ pocketshell sessions attach NAME # attach to a live session
90
+ pocketshell sessions kill NAME --json # stop and reap a session
91
+ pocketshell agent-log ... # agent conversation logs
92
+ pocketshell repos list ... # local / GitHub repositories
93
+ pocketshell github status [--json] # gh install / auth state
94
+ pocketshell env ... # .env / .envrc management
95
+ pocketshell hooks ... # Claude/Codex/OpenCode hooks
96
+ pocketshell logs ... # server-side trace sink
97
+ pocketshell daemon ... # IPC daemon lifecycle
98
+ pocketshell serve --dir PATH [--port N] # foreground static HTTP server
99
+ ```
100
+
101
+ Run `pocketshell --help` or `pocketshell <command> --help` for the live
102
+ flag set. Some parity subcommands still proxy through the existing host
103
+ tools internally so their output remains byte-identical to what the app
104
+ already parses.
105
+
106
+ ### `pocketshell sessions`
107
+
108
+ The session group is deliberately aplexer-only. `list`, `create`, `attach`, and
109
+ `kill` all use the bundled `a` executable resolved next to the installed
110
+ PocketShell interpreter; a missing or unusable aplexer is reported as an error
111
+ instead of an empty session list.
112
+
113
+ ```bash
114
+ pocketshell sessions list --json
115
+ pocketshell sessions create my-session --cwd ~/git/project --mem none --json
116
+ pocketshell sessions attach my-session
117
+ pocketshell sessions kill my-session --json
118
+ ```
119
+
120
+ The list and lifecycle responses use schema 3. Rows carry the aplexer id,
121
+ workspace, tag, phase, attachment state, and agent metadata; there is no
122
+ backend discriminator or legacy session socket. Create is idempotent for the
123
+ same workspace and tag. Kill stops the workload and reaps the aplexer record
124
+ before returning its JSON result.
125
+
126
+ ### `pocketshell usage`
127
+
128
+ ```text
129
+ pocketshell usage # human-readable lines, one per provider
130
+ pocketshell usage --json # machine-readable JSON (consumed by the app)
131
+ pocketshell usage codex # filter to a single provider
132
+ ```
133
+
134
+ The output shape is byte-identical to `quse [provider] [--json]`. When
135
+ the IPC daemon is running, `usage --json` dispatches `usage.fetch` over
136
+ the daemon socket and uses the daemon's short TTL cache; otherwise an
137
+ absent/unavailable daemon or explicitly supported method skew falls through
138
+ to the one-shot subprocess path. Timeout, malformed-response, and
139
+ daemon-internal failures are surfaced instead of being retried locally.
140
+
141
+ All daemon-backed wrappers (`usage`, `repos`, `tree`, `sessions`, and
142
+ `agents kind`) use one typed fallback boundary. It emits the safe
143
+ `pocketshell.daemon_call` event with `reason`, `method`, `phase`, RPC code, and
144
+ available CLI/daemon versions. It never logs RPC parameters or command output.
145
+
146
+ If `quse` is not installed, `pocketshell usage` exits with code 127 and
147
+ prints an install hint to stderr.
148
+
149
+ ### `pocketshell repos list`
150
+
151
+ Enumerate git repositories — either cloned on this host (`--local`) or
152
+ owned by the authenticated GitHub user (`--remote`). The two modes
153
+ share one unified JSON schema so a future merged view can interleave
154
+ them transparently.
155
+
156
+ ```bash
157
+ pocketshell repos list --local # scan ~/git for clones (human)
158
+ pocketshell repos list --local --json # same, JSON output
159
+ pocketshell repos list --remote --json # via owner-only `gh api user/repos`
160
+ pocketshell repos list --remote --limit 20
161
+ ```
162
+
163
+ Schema (every entry):
164
+
165
+ ```json
166
+ {
167
+ "owner": "alexeygrigorev", // null when remote URL is non-GitHub
168
+ "name": "pocketshell", // local dir basename, or GH repo name
169
+ "full_name": "PocketShell-io/pocketshell", // null when owner unknown
170
+ "local": { // populated by --local scans
171
+ "path": "/home/alexey/git/pocketshell",
172
+ "head": "main"
173
+ },
174
+ "remote": { // populated by --remote scans
175
+ "default_branch": "main",
176
+ "html_url": "https://github.com/PocketShell-io/pocketshell",
177
+ "ssh_url": "git@github.com:PocketShell-io/pocketshell.git",
178
+ "updated_at": "2026-05-27T12:00:00Z"
179
+ }
180
+ }
181
+ ```
182
+
183
+ `--local` scans `~/git` by default (override with one or more `--root`
184
+ flags or the colon-separated `POCKETSHELL_REPOS_ROOTS` env var) and
185
+ populates `local` for every entry. `owner` and `full_name` are
186
+ best-effort from the parsed `remote.origin.url`; non-GitHub remotes
187
+ leave them `null`.
188
+
189
+ `--remote` delegates to `gh api 'user/repos?affiliation=owner&sort=updated' --paginate --slurp`.
190
+ Requires `gh` on PATH (`apt install gh` on Debian/Ubuntu,
191
+ `brew install gh` on macOS) authenticated via
192
+ `gh auth login -s repo:read`. Sorted by `updated_at` descending so the
193
+ picker shows the most-recently-touched repos first. Missing `gh` exits
194
+ 127 with an install hint; a non-zero `gh` exit (auth missing,
195
+ rate-limit, etc.) propagates the exit code and stderr verbatim.
196
+
197
+ With neither flag, defaults to `--local` and prints a one-line
198
+ discoverability hint mentioning `--remote`.
199
+
200
+ Daemon mode caches `repos.list_local` for 10 s and `repos.list_remote`
201
+ for 5 min. `--no-daemon` forces the in-process path; `--no-cache`
202
+ forces the daemon to re-run upstream on the next call.
203
+
204
+ ### `pocketshell github status`
205
+
206
+ Reports whether the GitHub CLI (`gh`) is installed and authenticated, as
207
+ structured JSON the app consumes to gate GitHub features and prompt the
208
+ user to configure `gh` when it is missing (epic #644, slice #645).
209
+
210
+ ```bash
211
+ pocketshell github status # human-readable summary
212
+ pocketshell github status --json # machine-readable JSON (consumed by the app)
213
+ ```
214
+
215
+ Schema:
216
+
217
+ ```json
218
+ {
219
+ "installed": true, // shutil.which("gh") found the binary
220
+ "authenticated": true, // `gh auth status` exited 0
221
+ "account": "alexeygrigorev", // logged-in username, or null
222
+ "hint": null // actionable hint when something is missing
223
+ }
224
+ ```
225
+
226
+ The command always exits 0 — "gh missing" and "not authenticated" are
227
+ normal, reportable states (not probe failures), so the app can poll the
228
+ status without treating it as an error. When `gh` is absent the `hint` tells
229
+ the user to install it and run `gh auth login`; when present but
230
+ unauthenticated the `hint` tells them to run `gh auth login`. The only
231
+ network access is whatever `gh auth status` itself performs (a token-validity
232
+ check); the command does NOT call the GitHub API.
233
+
234
+ ### `pocketshell serve`
235
+
236
+ Serve a folder over HTTP for a client-owned SSH port forward:
237
+
238
+ ```bash
239
+ pocketshell serve --dir /path/to/site
240
+ pocketshell serve --dir /path/to/site --port 8080 --bind 127.0.0.1
241
+ ```
242
+
243
+ The server binds `127.0.0.1` by default. Omitting `--port` (or passing
244
+ `--port 0`) lets the OS select a free port; after binding, stdout contains
245
+ exactly one stable JSON line with the selected port:
246
+
247
+ ```json
248
+ {"port":43123}
249
+ ```
250
+
251
+ The process stays in the foreground so the caller owns its lifetime: keep the
252
+ SSH exec channel alive while the site is needed and terminate that process
253
+ when the view closes or the connection is lost. There is no detached server
254
+ registry or `--stop` command in this contract. HTTP access logs and errors go
255
+ to stderr, keeping stdout parseable.
256
+
257
+ Requests serve static files with stdlib MIME detection. A directory resolves
258
+ to its `index.html` when present; paths are resolved before the containment
259
+ check, so parent traversal and symlinks that leave the selected directory are
260
+ rejected rather than served.
261
+
262
+ ### `pocketshell hooks`
263
+
264
+ Installs agent **stop / idle-detection** hooks across Claude Code,
265
+ Codex, and OpenCode and normalizes their events into a single
266
+ append-only JSONL bus the app can read back. Server-side only;
267
+ integration only — no "tell the agent to continue" action yet (deferred;
268
+ see issue #267 and locked decision **D26** in `docs/decisions.md`).
269
+
270
+ ```bash
271
+ pocketshell hooks install [--engine claude|codex|opencode|all] # default: all
272
+ pocketshell hooks status [--engine ...] [--json] [--last N]
273
+ pocketshell hooks events [--since ISO8601] [--limit N] [--json]
274
+ pocketshell hooks uninstall [--engine ...]
275
+ ```
276
+
277
+ `install` is **non-destructive — it merges, it never clobbers**:
278
+
279
+ - **Claude Code** — adds a `{type: "command", command: "python3 <handler>"}`
280
+ entry under the `Stop`, `SubagentStop`, and `Notification` hook events
281
+ in `~/.claude/settings.json`, only when absent. All other top-level
282
+ keys and any pre-existing user hooks are preserved.
283
+ - **Codex** — sets the top-level `notify` program in `~/.codex/config.toml`
284
+ to our handler (Codex hooks do not fire under `codex exec`, so `notify`
285
+ is the headless-safe signal). If `notify` is already set to something
286
+ else, it warns and **skips** rather than overwriting. The rest of the
287
+ TOML is preserved.
288
+ - **OpenCode** — drops a `pocketshell-idle-signal.js` plugin into
289
+ `~/.config/opencode/plugin/` without disturbing other plugins.
290
+
291
+ `install` is idempotent (running twice adds nothing new). Generated handler
292
+ scripts and `.installed` ownership metadata are durable data under
293
+ `$XDG_DATA_HOME/pocketshell/hooks/` (default
294
+ `~/.local/share/pocketshell/hooks/`). The volatile event bus stays at
295
+ `$XDG_CACHE_HOME/pocketshell/hooks/events.jsonl` (default
296
+ `~/.cache/pocketshell/hooks/events.jsonl`). A routine cache cleanup therefore
297
+ starts a fresh bus without breaking the absolute commands retained by Claude or
298
+ Codex; the next event recreates the cache directory and bus.
299
+
300
+ Path overrides are intentionally separate:
301
+
302
+ - `$POCKETSHELL_HOOKS_HANDLER_DIR` overrides the durable generated-handler dir.
303
+ - `$POCKETSHELL_HOOKS_EVENTS_FILE` overrides the event bus file.
304
+ - The historical `$POCKETSHELL_HOOKS_DIR` remains an alias for the **handler
305
+ directory only** when the new handler variable is unset. It no longer moves
306
+ the bus. Use both new variables and rerun `hooks install` when both paths need
307
+ customization.
308
+
309
+ Each generated handler embeds the resolved bus path and appends a normalized
310
+ record `{ts, engine, state, source, session_id, cwd, ...}` there. `install`
311
+ also migrates PocketShell-owned Claude/Codex commands from the old cache path to
312
+ the durable path even when cache cleanup already removed the old scripts;
313
+ foreign hooks and foreign Codex `notify` programs remain untouched.
314
+
315
+ **Per-engine uninstall** (`pocketshell hooks uninstall`) removes only what
316
+ we added and is idempotent:
317
+
318
+ - **Claude Code** — drops our command group from each hook event; an
319
+ event key (and the top-level `hooks` object) is deleted only if we
320
+ created it and it ends up empty. A user's pre-existing hooks always
321
+ survive, so a pre-populated `settings.json` comes back
322
+ byte-equivalent for the unrelated parts.
323
+ - **Codex** — removes the top-level `notify` line only when it still
324
+ points at our handler. A `notify` the user pointed elsewhere is left
325
+ alone.
326
+ - **OpenCode** — deletes our plugin file; other plugins and the dir
327
+ itself are left in place.
328
+
329
+ The event bus (`events.jsonl`) is preserved on uninstall so already-emitted
330
+ records stay readable; only PocketShell-owned current/legacy config entries,
331
+ generated executables, and durable ownership metadata are cleaned up.
332
+
333
+ ## Development
334
+
335
+ ```bash
336
+ cd pocketshell-cli
337
+ uv venv
338
+ uv pip install -e ".[dev]"
339
+ uv run pytest
340
+ ```
341
+
342
+ Or via the dependency-group:
343
+
344
+ ```bash
345
+ uv sync --group dev
346
+ uv run pytest
347
+ ```
348
+
349
+ The tests stub subprocess boundaries and the bundled aplexer resolver so
350
+ they run in seconds without invoking a real host session.
351
+
352
+ ## Release flow
353
+
354
+ `pocketshell` is released from THIS repo on its own cycle, independent of the
355
+ Android app (extraction: PocketShell-io/pocketshell#2643). The `version` in
356
+ `pyproject.toml` is the single source of truth:
357
+
358
+ 1. Bump `version` in `pyproject.toml` (one small commit — the release
359
+ declaration; org convention, cf. PocketShell-io/quse).
360
+ 2. Tag that commit `vX.Y.Z` — the tag MUST equal the pyproject version — and
361
+ push the tag. `.github/workflows/publish.yml` builds the sdist + wheel,
362
+ refuses to publish if `dist/` does not carry exactly the tagged version,
363
+ and publishes to PyPI.
364
+ 3. Hosts pick it up with `uv tool upgrade pocketshell`.
365
+
366
+ CI (`.github/workflows/ci.yml`) runs the pytest suite plus the packaging and
367
+ frozen-lock guards on every PR and every push to `main`. The first
368
+ self-governed release is **0.5.5**, continuing from the last app-coupled
369
+ release 0.5.4 so `uv tool upgrade` stays monotonic.
370
+
371
+ ### PyPI publish setup (one-time)
372
+
373
+ The `publish-pypi` job publishes with a PyPI API token stored as the
374
+ `PYPI_API_TOKEN` secret on this repo (same mechanism as
375
+ [PocketShell-io/quse](https://github.com/PocketShell-io/quse)):
376
+
377
+ 1. On pypi.org, create an API token scoped to the `pocketshell` project (or
378
+ reuse the existing project-scoped token from the monorepo era).
379
+ 2. On GitHub: this repo → Settings → Secrets and variables → Actions →
380
+ New repository secret → name `PYPI_API_TOKEN`. The `pypi` environment
381
+ already exists; the publish job runs inside it.
382
+
383
+ If a publish fails after the tag is pushed, fix the cause and re-run
384
+ `publish.yml` from the Actions tab (it has a `workflow_dispatch` trigger) —
385
+ the tag does not need to move.
386
+
387
+ Manual escape hatch (maintainer account, only when CI publishing is
388
+ unavailable):
389
+
390
+ ```bash
391
+ uv build
392
+ uv run --with twine twine upload dist/*
393
+ ```
394
+
395
+ ## Why a unified CLI?
396
+
397
+ The PocketShell app previously depended on multiple host-side tools.
398
+ That meant separate installs to keep up to date, separate probes to
399
+ surface failures from, and multiple PATH-discovery edge cases. A single
400
+ `pocketshell` binary collapses that app-facing contract into one install,
401
+ one probe, and one bootstrap row. The Android bootstrap probe now derives
402
+ PATH from the user's shell rc and prepends `$HOME/.local/bin`,
403
+ `$HOME/bin`, and `$HOME/.cargo/bin` before probing, so cloned-repo or
404
+ venv installs can be discovered without a manual app-side PATH field.