pocketshell 0.5.4__tar.gz → 0.5.6__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 (99) hide show
  1. pocketshell-0.5.6/LICENSE +21 -0
  2. {pocketshell-0.5.4 → pocketshell-0.5.6}/PKG-INFO +51 -179
  3. {pocketshell-0.5.4 → pocketshell-0.5.6}/README.md +46 -173
  4. {pocketshell-0.5.4 → pocketshell-0.5.6}/pyproject.toml +11 -23
  5. {pocketshell-0.5.4 → pocketshell-0.5.6}/scheduler/pocketshell-usage-capture.service +1 -1
  6. {pocketshell-0.5.4 → pocketshell-0.5.6}/scheduler/pocketshell-usage-capture.timer +1 -1
  7. pocketshell-0.5.6/scripts/check-cgroup-cap-proof.sh +191 -0
  8. pocketshell-0.5.6/scripts/check-product-tmux-absent.sh +62 -0
  9. pocketshell-0.5.6/scripts/check-python-ci-lock.sh +73 -0
  10. {pocketshell-0.5.4 → pocketshell-0.5.6}/scripts/sync-readme-exit-codes.py +2 -2
  11. pocketshell-0.5.6/scripts/test-usage-missing-window-mutation.sh +215 -0
  12. {pocketshell-0.5.4 → pocketshell-0.5.6}/src/pocketshell/cli.py +4 -6
  13. {pocketshell-0.5.4 → pocketshell-0.5.6}/src/pocketshell/sessions.py +28 -1
  14. {pocketshell-0.5.4 → pocketshell-0.5.6}/tests/test_sessions_create_routing.py +31 -0
  15. {pocketshell-0.5.4 → pocketshell-0.5.6}/tests/test_usage.py +7 -55
  16. {pocketshell-0.5.4 → pocketshell-0.5.6}/uv.lock +2 -110
  17. pocketshell-0.5.4/src/pocketshell/qr_share.py +0 -432
  18. pocketshell-0.5.4/tests/test_agents_fixture_aplexer.py +0 -158
  19. pocketshell-0.5.4/tests/test_qr_share.py +0 -327
  20. pocketshell-0.5.4/tests/test_ssh_fixture_penalties.py +0 -966
  21. pocketshell-0.5.4/tests/test_watch_ci.py +0 -948
  22. pocketshell-0.5.4/tests/test_watch_ci_cancelled.py +0 -745
  23. {pocketshell-0.5.4 → pocketshell-0.5.6}/.gitignore +0 -0
  24. {pocketshell-0.5.4 → pocketshell-0.5.6}/scheduler/README.md +0 -0
  25. {pocketshell-0.5.4 → pocketshell-0.5.6}/src/pocketshell/__init__.py +0 -0
  26. {pocketshell-0.5.4 → pocketshell-0.5.6}/src/pocketshell/__main__.py +0 -0
  27. {pocketshell-0.5.4 → pocketshell-0.5.6}/src/pocketshell/agent_card_push.py +0 -0
  28. {pocketshell-0.5.4 → pocketshell-0.5.6}/src/pocketshell/agent_log.py +0 -0
  29. {pocketshell-0.5.4 → pocketshell-0.5.6}/src/pocketshell/agents.py +0 -0
  30. {pocketshell-0.5.4 → pocketshell-0.5.6}/src/pocketshell/agents_kind.py +0 -0
  31. {pocketshell-0.5.4 → pocketshell-0.5.6}/src/pocketshell/aplexer.py +0 -0
  32. {pocketshell-0.5.4 → pocketshell-0.5.6}/src/pocketshell/cards.py +0 -0
  33. {pocketshell-0.5.4 → pocketshell-0.5.6}/src/pocketshell/cgroup_agents.py +0 -0
  34. {pocketshell-0.5.4 → pocketshell-0.5.6}/src/pocketshell/daemon.py +0 -0
  35. {pocketshell-0.5.4 → pocketshell-0.5.6}/src/pocketshell/engines.py +0 -0
  36. {pocketshell-0.5.4 → pocketshell-0.5.6}/src/pocketshell/env.py +0 -0
  37. {pocketshell-0.5.4 → pocketshell-0.5.6}/src/pocketshell/github.py +0 -0
  38. {pocketshell-0.5.4 → pocketshell-0.5.6}/src/pocketshell/hooks.py +0 -0
  39. {pocketshell-0.5.4 → pocketshell-0.5.6}/src/pocketshell/logs.py +0 -0
  40. {pocketshell-0.5.4 → pocketshell-0.5.6}/src/pocketshell/memcap.py +0 -0
  41. {pocketshell-0.5.4 → pocketshell-0.5.6}/src/pocketshell/profiles.py +0 -0
  42. {pocketshell-0.5.4 → pocketshell-0.5.6}/src/pocketshell/prune_attachments.py +0 -0
  43. {pocketshell-0.5.4 → pocketshell-0.5.6}/src/pocketshell/push.py +0 -0
  44. {pocketshell-0.5.4 → pocketshell-0.5.6}/src/pocketshell/repos.py +0 -0
  45. {pocketshell-0.5.4 → pocketshell-0.5.6}/src/pocketshell/serve.py +0 -0
  46. {pocketshell-0.5.4 → pocketshell-0.5.6}/src/pocketshell/session_enum.py +0 -0
  47. {pocketshell-0.5.4 → pocketshell-0.5.6}/src/pocketshell/tree.py +0 -0
  48. {pocketshell-0.5.4 → pocketshell-0.5.6}/src/pocketshell/usage.py +0 -0
  49. {pocketshell-0.5.4 → pocketshell-0.5.6}/src/pocketshell/usage_capture.py +0 -0
  50. {pocketshell-0.5.4 → pocketshell-0.5.6}/src/pocketshell/usage_reset.py +0 -0
  51. {pocketshell-0.5.4 → pocketshell-0.5.6}/src/pocketshell/workspaces.py +0 -0
  52. {pocketshell-0.5.4 → pocketshell-0.5.6}/tests/__init__.py +0 -0
  53. {pocketshell-0.5.4 → pocketshell-0.5.6}/tests/conftest.py +0 -0
  54. {pocketshell-0.5.4 → pocketshell-0.5.6}/tests/data/quse-0.0.11-usage.json +0 -0
  55. {pocketshell-0.5.4 → pocketshell-0.5.6}/tests/data/quse-0.0.13-usage.json +0 -0
  56. {pocketshell-0.5.4 → pocketshell-0.5.6}/tests/data/quse-0.0.15-usage.json +0 -0
  57. {pocketshell-0.5.4 → pocketshell-0.5.6}/tests/data/quse-0.0.15-usage.ndjson +0 -0
  58. {pocketshell-0.5.4 → pocketshell-0.5.6}/tests/data/quse-0.0.15-usage.provenance.txt +0 -0
  59. {pocketshell-0.5.4 → pocketshell-0.5.6}/tests/data/quse-0.0.9-usage.json +0 -0
  60. {pocketshell-0.5.4 → pocketshell-0.5.6}/tests/data/sshd_anchor/Dockerfile.from-unanchored-base +0 -0
  61. {pocketshell-0.5.4 → pocketshell-0.5.6}/tests/fixtures/aplexer/snapshot-agent.json +0 -0
  62. {pocketshell-0.5.4 → pocketshell-0.5.6}/tests/fixtures/aplexer/snapshot-crashed-start.json +0 -0
  63. {pocketshell-0.5.4 → pocketshell-0.5.6}/tests/fixtures/aplexer/snapshot-liveness-mix.json +0 -0
  64. {pocketshell-0.5.4 → pocketshell-0.5.6}/tests/fixtures/aplexer/snapshot-mid-create.json +0 -0
  65. {pocketshell-0.5.4 → pocketshell-0.5.6}/tests/fixtures/aplexer/snapshot-post-kill-window.json +0 -0
  66. {pocketshell-0.5.4 → pocketshell-0.5.6}/tests/fixtures/aplexer/snapshot-reported-state.json +0 -0
  67. {pocketshell-0.5.4 → pocketshell-0.5.6}/tests/fixtures/aplexer/snapshot.json +0 -0
  68. {pocketshell-0.5.4 → pocketshell-0.5.6}/tests/test_agent_card_push.py +0 -0
  69. {pocketshell-0.5.4 → pocketshell-0.5.6}/tests/test_agent_log.py +0 -0
  70. {pocketshell-0.5.4 → pocketshell-0.5.6}/tests/test_agents.py +0 -0
  71. {pocketshell-0.5.4 → pocketshell-0.5.6}/tests/test_agents_kind.py +0 -0
  72. {pocketshell-0.5.4 → pocketshell-0.5.6}/tests/test_aplexer_contract.py +0 -0
  73. {pocketshell-0.5.4 → pocketshell-0.5.6}/tests/test_aplexer_resolution.py +0 -0
  74. {pocketshell-0.5.4 → pocketshell-0.5.6}/tests/test_cards.py +0 -0
  75. {pocketshell-0.5.4 → pocketshell-0.5.6}/tests/test_cards_push_notify.py +0 -0
  76. {pocketshell-0.5.4 → pocketshell-0.5.6}/tests/test_cgroup_agents.py +0 -0
  77. {pocketshell-0.5.4 → pocketshell-0.5.6}/tests/test_cli.py +0 -0
  78. {pocketshell-0.5.4 → pocketshell-0.5.6}/tests/test_daemon.py +0 -0
  79. {pocketshell-0.5.4 → pocketshell-0.5.6}/tests/test_daemon_fallback.py +0 -0
  80. {pocketshell-0.5.4 → pocketshell-0.5.6}/tests/test_engines.py +0 -0
  81. {pocketshell-0.5.4 → pocketshell-0.5.6}/tests/test_env.py +0 -0
  82. {pocketshell-0.5.4 → pocketshell-0.5.6}/tests/test_github.py +0 -0
  83. {pocketshell-0.5.4 → pocketshell-0.5.6}/tests/test_hooks.py +0 -0
  84. {pocketshell-0.5.4 → pocketshell-0.5.6}/tests/test_logs.py +0 -0
  85. {pocketshell-0.5.4 → pocketshell-0.5.6}/tests/test_profiles.py +0 -0
  86. {pocketshell-0.5.4 → pocketshell-0.5.6}/tests/test_prune_attachments.py +0 -0
  87. {pocketshell-0.5.4 → pocketshell-0.5.6}/tests/test_push.py +0 -0
  88. {pocketshell-0.5.4 → pocketshell-0.5.6}/tests/test_repos.py +0 -0
  89. {pocketshell-0.5.4 → pocketshell-0.5.6}/tests/test_serve.py +0 -0
  90. {pocketshell-0.5.4 → pocketshell-0.5.6}/tests/test_session_cgroup_kernel_proof.py +0 -0
  91. {pocketshell-0.5.4 → pocketshell-0.5.6}/tests/test_session_enum.py +0 -0
  92. {pocketshell-0.5.4 → pocketshell-0.5.6}/tests/test_sessions.py +0 -0
  93. {pocketshell-0.5.4 → pocketshell-0.5.6}/tests/test_sessions_attach.py +0 -0
  94. {pocketshell-0.5.4 → pocketshell-0.5.6}/tests/test_sessions_kill.py +0 -0
  95. {pocketshell-0.5.4 → pocketshell-0.5.6}/tests/test_sessions_mem_cap.py +0 -0
  96. {pocketshell-0.5.4 → pocketshell-0.5.6}/tests/test_tree.py +0 -0
  97. {pocketshell-0.5.4 → pocketshell-0.5.6}/tests/test_usage_capture.py +0 -0
  98. {pocketshell-0.5.4 → pocketshell-0.5.6}/tests/test_usage_reset.py +0 -0
  99. {pocketshell-0.5.4 → pocketshell-0.5.6}/tests/test_workspaces.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.
@@ -1,11 +1,12 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: pocketshell
3
- Version: 0.5.4
4
- Summary: Unified server-side Python utility for the PocketShell Android client.
5
- Project-URL: Homepage, https://github.com/alexeygrigorev/pocketshell
6
- Project-URL: Issues, https://github.com/alexeygrigorev/pocketshell/issues
3
+ Version: 0.5.6
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
7
  Author: Alexey Grigorev
8
8
  License: MIT
9
+ License-File: LICENSE
9
10
  Keywords: agents,pocketshell,ssh,usage
10
11
  Classifier: Development Status :: 3 - Alpha
11
12
  Classifier: Environment :: Console
@@ -26,16 +27,17 @@ Requires-Dist: quse==0.0.15
26
27
  Provides-Extra: dev
27
28
  Requires-Dist: pytest>=8.4.0; extra == 'dev'
28
29
  Requires-Dist: ruff>=0.15.0; extra == 'dev'
29
- Provides-Extra: qr
30
- Requires-Dist: qrcode[pil]>=7.4; extra == 'qr'
31
30
  Description-Content-Type: text/markdown
32
31
 
33
32
  # pocketshell
34
33
 
35
- Unified server-side Python utility for the [PocketShell](https://github.com/alexeygrigorev/pocketshell)
36
- Android client. The app probes for this single helper on each remote
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
37
39
  host and uses its subcommands for usage, aplexer session lifecycle, agent
38
- conversations, QR host setup, repository discovery, environment files, hooks,
40
+ conversations, repository discovery, environment files, hooks,
39
41
  logs, and daemon lifecycle checks.
40
42
 
41
43
  ## Durable workspaces
@@ -66,7 +68,7 @@ uv tool install pocketshell
66
68
  For local development from a clone:
67
69
 
68
70
  ```bash
69
- cd tools/pocketshell
71
+ cd pocketshell-cli
70
72
  uv venv
71
73
  uv pip install -e .
72
74
  pocketshell --help
@@ -76,21 +78,6 @@ pocketshell --help
76
78
  pipx. Both install paths produce a `pocketshell` binary that the
77
79
  PocketShell app's bootstrap probe detects.
78
80
 
79
- ### Optional extras
80
-
81
- `pocketshell qr-share` requires the `qrcode[pil]` package (Pillow) to
82
- render QR images. Because Pillow is heavy and not needed by any other
83
- subcommand, it ships behind an optional `qr` extra:
84
-
85
- ```bash
86
- uv tool install pocketshell --with qrcode[pil]
87
- # or
88
- pip install pocketshell[qr]
89
- ```
90
-
91
- Without the extra, every other subcommand keeps working; only
92
- `pocketshell qr-share` exits 127 with a friendly install hint.
93
-
94
81
  ## Usage
95
82
 
96
83
  Top-level commands in the current helper:
@@ -109,7 +96,6 @@ pocketshell hooks ... # Claude/Codex/OpenCode hooks
109
96
  pocketshell logs ... # server-side trace sink
110
97
  pocketshell daemon ... # IPC daemon lifecycle
111
98
  pocketshell serve --dir PATH [--port N] # foreground static HTTP server
112
- pocketshell qr-share ... # SSH host QR import payloads
113
99
  ```
114
100
 
115
101
  Run `pocketshell --help` or `pocketshell <command> --help` for the live
@@ -180,15 +166,15 @@ Schema (every entry):
180
166
  {
181
167
  "owner": "alexeygrigorev", // null when remote URL is non-GitHub
182
168
  "name": "pocketshell", // local dir basename, or GH repo name
183
- "full_name": "alexeygrigorev/pocketshell", // null when owner unknown
169
+ "full_name": "PocketShell-io/pocketshell", // null when owner unknown
184
170
  "local": { // populated by --local scans
185
171
  "path": "/home/alexey/git/pocketshell",
186
172
  "head": "main"
187
173
  },
188
174
  "remote": { // populated by --remote scans
189
175
  "default_branch": "main",
190
- "html_url": "https://github.com/alexeygrigorev/pocketshell",
191
- "ssh_url": "git@github.com:alexeygrigorev/pocketshell.git",
176
+ "html_url": "https://github.com/PocketShell-io/pocketshell",
177
+ "ssh_url": "git@github.com:PocketShell-io/pocketshell.git",
192
178
  "updated_at": "2026-05-27T12:00:00Z"
193
179
  }
194
180
  }
@@ -273,47 +259,6 @@ to its `index.html` when present; paths are resolved before the containment
273
259
  check, so parent traversal and symlinks that leave the selected directory are
274
260
  rejected rather than served.
275
261
 
276
- ### `pocketshell qr-share`
277
-
278
- Builds a `pocketshell.ssh-import.v1` payload from an `~/.ssh/config`
279
- alias (resolved via `ssh -G`) or from explicit flags, wraps it in one or
280
- more `pocketshell.qr.v1` chunked envelopes (matching the Kotlin
281
- `QrChunkCodec` byte-for-byte), and emits QR codes for the phone-side
282
- scanner to consume (issue #129).
283
-
284
- ```bash
285
- pocketshell qr-share prod # ssh-config alias
286
- pocketshell qr-share --host h --user u --key ~/.ssh/id_ed25519 --name h
287
- pocketshell qr-share prod --png --out-dir /tmp/qr # write PNGs
288
- pocketshell qr-share prod --print-only --id deadbeef # debug envelopes
289
- ```
290
-
291
- When stdout is a TTY the QRs are drawn inline as Unicode blocks; between
292
- multi-part transmissions the command pauses on "Press Enter for next
293
- QR" so the user can scan each in turn. When stdout is not a TTY (or
294
- `--png` is passed) a numbered PNG sequence (`qr-share-01.png`,
295
- `qr-share-02.png`, ...) is written to `--out-dir`.
296
-
297
- Requires the optional `qr` extra (see [Optional extras](#optional-extras)).
298
- Without it, the command exits 127 with the install hint and every other
299
- subcommand keeps working.
300
-
301
- #### Running from a repo clone (no install)
302
-
303
- To run `qr-share` straight from a checkout without installing the tool,
304
- use `uv run` from `tools/pocketshell` and include the `qr` extra:
305
-
306
- ```bash
307
- cd tools/pocketshell
308
- uv run --extra qr pocketshell qr-share prod
309
- ```
310
-
311
- The first run creates `.venv` and installs the QR dependency; later runs
312
- are instant. Run it in an interactive terminal so stdout is a TTY and the
313
- QR renders inline — otherwise it falls back to writing PNGs (add
314
- `--png --out-dir ./qr` to force PNGs). Omitting `--extra qr` makes the
315
- command exit 127 with the install hint.
316
-
317
262
  ### `pocketshell hooks`
318
263
 
319
264
  Installs agent **stop / idle-detection** hooks across Claude Code,
@@ -388,7 +333,7 @@ generated executables, and durable ownership metadata are cleaned up.
388
333
  ## Development
389
334
 
390
335
  ```bash
391
- cd tools/pocketshell
336
+ cd pocketshell-cli
392
337
  uv venv
393
338
  uv pip install -e ".[dev]"
394
339
  uv run pytest
@@ -406,120 +351,47 @@ they run in seconds without invoking a real host session.
406
351
 
407
352
  ## Release flow
408
353
 
409
- `pocketshell` ships in lockstep with the Android app. Every time the
410
- maintainer cuts an Android release tag (`vX.Y.Z`), the
411
- [`Build`](../../.github/workflows/build.yml) workflow assembles the APK
412
- and **also** builds the Python sdist + wheel and publishes them to PyPI.
413
-
414
- ### Version coupling (tag-derived, issue #2356)
415
-
416
- Neither side is a hand-maintained literal any more. Both derive from the git
417
- tag being built, via the single shared script
418
- [`scripts/derive-version.sh`](../../scripts/derive-version.sh):
419
-
420
- - `app/build.gradle.kts` computes `versionCode`/`versionName` at Gradle
421
- configuration time by shelling out to `scripts/derive-version.sh`.
422
- - `tools/pocketshell/pyproject.toml`'s committed `version` field is a
423
- placeholder. The release workflow's "Stamp pyproject.toml version from
424
- tag" step overwrites it (in the ephemeral CI checkout, never committed)
425
- from `scripts/derive-version.sh version-name --ref <tag>` immediately
426
- before building the sdist/wheel.
427
-
428
- [`scripts/check-version-coupling.sh`](../../scripts/check-version-coupling.sh)
429
- verifies the derivation script is the SOLE source of truth (its own
430
- self-test, Gradle's resolved version matching a direct script invocation,
431
- and both consumers referencing the script by path rather than an
432
- independent reimplementation) — it runs per-push in `tests.yml`.
433
- [`scripts/check-pypi-version.sh`](../../scripts/check-pypi-version.sh)
434
- verifies, at tag-publish time, that the freshly-stamped
435
- `pyproject.toml` version equals what `scripts/derive-version.sh` derives
436
- for the tag being published:
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:
437
357
 
438
- ```bash
439
- scripts/check-pypi-version.sh --check-tag vX.Y.Z
440
- ```
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`.
441
365
 
442
- ### Cutting a release
443
-
444
- There is no version-bump commit or PR. The tag itself is the version
445
- declaration:
446
-
447
- 1. Pick the next semantic version after the latest GitHub Release/tag.
448
- 2. Run the emulator release validation gate
449
- (`scripts/release-emulator-validation.sh`) against the current `main`
450
- HEAD, as described in [`process.md`](../../process.md) -> "Release
451
- Builds".
452
- 3. Push the tag with `scripts/push-release-tag.sh vX.Y.Z ...`. It creates
453
- the tag locally FIRST and verifies `scripts/derive-version.sh` derives
454
- the expected `versionName` (and a strictly-monotonic `versionCode`
455
- versus the previous tag) from it before pushing — so a derivation bug
456
- is caught before the tag ever reaches `origin`. The tag-triggered
457
- `Build` workflow then:
458
- - builds and uploads the APK (its `versionCode`/`versionName` come from
459
- the tag via `app/build.gradle.kts`'s own derivation) + creates the
460
- GitHub Release
461
- - stamps `tools/pocketshell/pyproject.toml`'s version from the tag
462
- - runs `scripts/check-pypi-version.sh --check-tag vX.Y.Z`
463
- - builds the Python sdist + wheel
464
- - publishes them to PyPI via OIDC trusted publishing
465
-
466
- The PyPI publish job depends on the APK build job, so a broken APK
467
- build also aborts the PyPI publish. If only the PyPI publish fails the
468
- maintainer can re-trigger the workflow at the same tag from the
469
- Actions tab; the APK build is idempotent against an existing release
470
- (`softprops/action-gh-release` updates the existing release rather
471
- than failing).
472
-
473
- ## PyPI trusted publishing setup (one-time)
474
-
475
- The `publish-pypi` job uses GitHub's OIDC token instead of a long-lived
476
- API token. This avoids storing a `PYPI_API_TOKEN` secret in the repo
477
- and means there is nothing to rotate. The trade-off is that the
478
- project owner must complete one configuration step on pypi.org before
479
- the first automated tag publish:
480
-
481
- 1. Sign in to https://pypi.org/ with the project owner account.
482
- 2. Open the `pocketshell` project page ->
483
- **Manage** -> **Publishing**.
484
- 3. Under **Trusted publishers**, click **Add a new pending publisher**
485
- (if the project is empty) or **Add a new publisher**, then fill in:
486
- - **PyPI Project Name**: `pocketshell`
487
- - **Owner**: `alexeygrigorev`
488
- - **Repository name**: `pocketshell`
489
- - **Workflow name**: `build.yml`
490
- - **Environment name**: `pypi`
491
- 4. Save the publisher.
492
- 5. In this repository on GitHub, open
493
- **Settings** -> **Environments** -> **New environment** -> name it
494
- `pypi`. No secrets or reviewers are required; the environment exists
495
- purely to scope the OIDC token. (If the environment already exists,
496
- confirm it has no protection rules that would block the workflow
497
- from running.)
498
- 6. Push the next release tag. The `Publish to PyPI via trusted
499
- publishing` step should succeed without any token configuration.
500
-
501
- ### Why trusted publishing (and not `PYPI_API_TOKEN`)?
502
-
503
- - No long-lived secret to rotate, leak, or accidentally print in logs.
504
- - The OIDC subject is scoped to `repo=alexeygrigorev/pocketshell`,
505
- `workflow=build.yml`, `environment=pypi`, so a compromised fork or
506
- a different workflow file in this repo cannot reuse it.
507
- - D22 (no backwards-compat): we do not also maintain a token-fallback
508
- path. If trusted publishing breaks, fix it; do not add a token
509
- branch alongside.
510
-
511
- If trusted publishing is ever unavailable for a tag (e.g. PyPI outage
512
- on the OIDC verifier), the recommended manual escape hatch is:
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):
513
389
 
514
390
  ```bash
515
- cd tools/pocketshell
516
- python -m build
517
- python -m twine upload dist/*
391
+ uv build
392
+ uv run --with twine twine upload dist/*
518
393
  ```
519
394
 
520
- with the maintainer's account. Do not re-add a `PYPI_API_TOKEN` secret
521
- as a permanent fallback.
522
-
523
395
  ## Why a unified CLI?
524
396
 
525
397
  The PocketShell app previously depended on multiple host-side tools.
@@ -1,9 +1,12 @@
1
1
  # pocketshell
2
2
 
3
- Unified server-side Python utility for the [PocketShell](https://github.com/alexeygrigorev/pocketshell)
4
- Android client. The app probes for this single helper on each remote
3
+ Unified server-side Python utility for the [PocketShell](https://github.com/PocketShell-io/pocketshell)
4
+ Android and desktop clients. This is the host-side companion CLI, maintained in its own
5
+ repo with an independent release cycle; before v0.5.5 it lived at
6
+ `tools/pocketshell/` in the [monorepo](https://github.com/PocketShell-io/pocketshell)
7
+ (versioning was coupled to the app tag there — see [Release flow](#release-flow)). The app probes for this single helper on each remote
5
8
  host and uses its subcommands for usage, aplexer session lifecycle, agent
6
- conversations, QR host setup, repository discovery, environment files, hooks,
9
+ conversations, repository discovery, environment files, hooks,
7
10
  logs, and daemon lifecycle checks.
8
11
 
9
12
  ## Durable workspaces
@@ -34,7 +37,7 @@ uv tool install pocketshell
34
37
  For local development from a clone:
35
38
 
36
39
  ```bash
37
- cd tools/pocketshell
40
+ cd pocketshell-cli
38
41
  uv venv
39
42
  uv pip install -e .
40
43
  pocketshell --help
@@ -44,21 +47,6 @@ pocketshell --help
44
47
  pipx. Both install paths produce a `pocketshell` binary that the
45
48
  PocketShell app's bootstrap probe detects.
46
49
 
47
- ### Optional extras
48
-
49
- `pocketshell qr-share` requires the `qrcode[pil]` package (Pillow) to
50
- render QR images. Because Pillow is heavy and not needed by any other
51
- subcommand, it ships behind an optional `qr` extra:
52
-
53
- ```bash
54
- uv tool install pocketshell --with qrcode[pil]
55
- # or
56
- pip install pocketshell[qr]
57
- ```
58
-
59
- Without the extra, every other subcommand keeps working; only
60
- `pocketshell qr-share` exits 127 with a friendly install hint.
61
-
62
50
  ## Usage
63
51
 
64
52
  Top-level commands in the current helper:
@@ -77,7 +65,6 @@ pocketshell hooks ... # Claude/Codex/OpenCode hooks
77
65
  pocketshell logs ... # server-side trace sink
78
66
  pocketshell daemon ... # IPC daemon lifecycle
79
67
  pocketshell serve --dir PATH [--port N] # foreground static HTTP server
80
- pocketshell qr-share ... # SSH host QR import payloads
81
68
  ```
82
69
 
83
70
  Run `pocketshell --help` or `pocketshell <command> --help` for the live
@@ -148,15 +135,15 @@ Schema (every entry):
148
135
  {
149
136
  "owner": "alexeygrigorev", // null when remote URL is non-GitHub
150
137
  "name": "pocketshell", // local dir basename, or GH repo name
151
- "full_name": "alexeygrigorev/pocketshell", // null when owner unknown
138
+ "full_name": "PocketShell-io/pocketshell", // null when owner unknown
152
139
  "local": { // populated by --local scans
153
140
  "path": "/home/alexey/git/pocketshell",
154
141
  "head": "main"
155
142
  },
156
143
  "remote": { // populated by --remote scans
157
144
  "default_branch": "main",
158
- "html_url": "https://github.com/alexeygrigorev/pocketshell",
159
- "ssh_url": "git@github.com:alexeygrigorev/pocketshell.git",
145
+ "html_url": "https://github.com/PocketShell-io/pocketshell",
146
+ "ssh_url": "git@github.com:PocketShell-io/pocketshell.git",
160
147
  "updated_at": "2026-05-27T12:00:00Z"
161
148
  }
162
149
  }
@@ -241,47 +228,6 @@ to its `index.html` when present; paths are resolved before the containment
241
228
  check, so parent traversal and symlinks that leave the selected directory are
242
229
  rejected rather than served.
243
230
 
244
- ### `pocketshell qr-share`
245
-
246
- Builds a `pocketshell.ssh-import.v1` payload from an `~/.ssh/config`
247
- alias (resolved via `ssh -G`) or from explicit flags, wraps it in one or
248
- more `pocketshell.qr.v1` chunked envelopes (matching the Kotlin
249
- `QrChunkCodec` byte-for-byte), and emits QR codes for the phone-side
250
- scanner to consume (issue #129).
251
-
252
- ```bash
253
- pocketshell qr-share prod # ssh-config alias
254
- pocketshell qr-share --host h --user u --key ~/.ssh/id_ed25519 --name h
255
- pocketshell qr-share prod --png --out-dir /tmp/qr # write PNGs
256
- pocketshell qr-share prod --print-only --id deadbeef # debug envelopes
257
- ```
258
-
259
- When stdout is a TTY the QRs are drawn inline as Unicode blocks; between
260
- multi-part transmissions the command pauses on "Press Enter for next
261
- QR" so the user can scan each in turn. When stdout is not a TTY (or
262
- `--png` is passed) a numbered PNG sequence (`qr-share-01.png`,
263
- `qr-share-02.png`, ...) is written to `--out-dir`.
264
-
265
- Requires the optional `qr` extra (see [Optional extras](#optional-extras)).
266
- Without it, the command exits 127 with the install hint and every other
267
- subcommand keeps working.
268
-
269
- #### Running from a repo clone (no install)
270
-
271
- To run `qr-share` straight from a checkout without installing the tool,
272
- use `uv run` from `tools/pocketshell` and include the `qr` extra:
273
-
274
- ```bash
275
- cd tools/pocketshell
276
- uv run --extra qr pocketshell qr-share prod
277
- ```
278
-
279
- The first run creates `.venv` and installs the QR dependency; later runs
280
- are instant. Run it in an interactive terminal so stdout is a TTY and the
281
- QR renders inline — otherwise it falls back to writing PNGs (add
282
- `--png --out-dir ./qr` to force PNGs). Omitting `--extra qr` makes the
283
- command exit 127 with the install hint.
284
-
285
231
  ### `pocketshell hooks`
286
232
 
287
233
  Installs agent **stop / idle-detection** hooks across Claude Code,
@@ -356,7 +302,7 @@ generated executables, and durable ownership metadata are cleaned up.
356
302
  ## Development
357
303
 
358
304
  ```bash
359
- cd tools/pocketshell
305
+ cd pocketshell-cli
360
306
  uv venv
361
307
  uv pip install -e ".[dev]"
362
308
  uv run pytest
@@ -374,120 +320,47 @@ they run in seconds without invoking a real host session.
374
320
 
375
321
  ## Release flow
376
322
 
377
- `pocketshell` ships in lockstep with the Android app. Every time the
378
- maintainer cuts an Android release tag (`vX.Y.Z`), the
379
- [`Build`](../../.github/workflows/build.yml) workflow assembles the APK
380
- and **also** builds the Python sdist + wheel and publishes them to PyPI.
381
-
382
- ### Version coupling (tag-derived, issue #2356)
383
-
384
- Neither side is a hand-maintained literal any more. Both derive from the git
385
- tag being built, via the single shared script
386
- [`scripts/derive-version.sh`](../../scripts/derive-version.sh):
387
-
388
- - `app/build.gradle.kts` computes `versionCode`/`versionName` at Gradle
389
- configuration time by shelling out to `scripts/derive-version.sh`.
390
- - `tools/pocketshell/pyproject.toml`'s committed `version` field is a
391
- placeholder. The release workflow's "Stamp pyproject.toml version from
392
- tag" step overwrites it (in the ephemeral CI checkout, never committed)
393
- from `scripts/derive-version.sh version-name --ref <tag>` immediately
394
- before building the sdist/wheel.
395
-
396
- [`scripts/check-version-coupling.sh`](../../scripts/check-version-coupling.sh)
397
- verifies the derivation script is the SOLE source of truth (its own
398
- self-test, Gradle's resolved version matching a direct script invocation,
399
- and both consumers referencing the script by path rather than an
400
- independent reimplementation) — it runs per-push in `tests.yml`.
401
- [`scripts/check-pypi-version.sh`](../../scripts/check-pypi-version.sh)
402
- verifies, at tag-publish time, that the freshly-stamped
403
- `pyproject.toml` version equals what `scripts/derive-version.sh` derives
404
- for the tag being published:
323
+ `pocketshell` is released from THIS repo on its own cycle, independent of the
324
+ Android app (extraction: PocketShell-io/pocketshell#2643). The `version` in
325
+ `pyproject.toml` is the single source of truth:
405
326
 
406
- ```bash
407
- scripts/check-pypi-version.sh --check-tag vX.Y.Z
408
- ```
327
+ 1. Bump `version` in `pyproject.toml` (one small commit — the release
328
+ declaration; org convention, cf. PocketShell-io/quse).
329
+ 2. Tag that commit `vX.Y.Z` — the tag MUST equal the pyproject version — and
330
+ push the tag. `.github/workflows/publish.yml` builds the sdist + wheel,
331
+ refuses to publish if `dist/` does not carry exactly the tagged version,
332
+ and publishes to PyPI.
333
+ 3. Hosts pick it up with `uv tool upgrade pocketshell`.
409
334
 
410
- ### Cutting a release
411
-
412
- There is no version-bump commit or PR. The tag itself is the version
413
- declaration:
414
-
415
- 1. Pick the next semantic version after the latest GitHub Release/tag.
416
- 2. Run the emulator release validation gate
417
- (`scripts/release-emulator-validation.sh`) against the current `main`
418
- HEAD, as described in [`process.md`](../../process.md) -> "Release
419
- Builds".
420
- 3. Push the tag with `scripts/push-release-tag.sh vX.Y.Z ...`. It creates
421
- the tag locally FIRST and verifies `scripts/derive-version.sh` derives
422
- the expected `versionName` (and a strictly-monotonic `versionCode`
423
- versus the previous tag) from it before pushing — so a derivation bug
424
- is caught before the tag ever reaches `origin`. The tag-triggered
425
- `Build` workflow then:
426
- - builds and uploads the APK (its `versionCode`/`versionName` come from
427
- the tag via `app/build.gradle.kts`'s own derivation) + creates the
428
- GitHub Release
429
- - stamps `tools/pocketshell/pyproject.toml`'s version from the tag
430
- - runs `scripts/check-pypi-version.sh --check-tag vX.Y.Z`
431
- - builds the Python sdist + wheel
432
- - publishes them to PyPI via OIDC trusted publishing
433
-
434
- The PyPI publish job depends on the APK build job, so a broken APK
435
- build also aborts the PyPI publish. If only the PyPI publish fails the
436
- maintainer can re-trigger the workflow at the same tag from the
437
- Actions tab; the APK build is idempotent against an existing release
438
- (`softprops/action-gh-release` updates the existing release rather
439
- than failing).
440
-
441
- ## PyPI trusted publishing setup (one-time)
442
-
443
- The `publish-pypi` job uses GitHub's OIDC token instead of a long-lived
444
- API token. This avoids storing a `PYPI_API_TOKEN` secret in the repo
445
- and means there is nothing to rotate. The trade-off is that the
446
- project owner must complete one configuration step on pypi.org before
447
- the first automated tag publish:
448
-
449
- 1. Sign in to https://pypi.org/ with the project owner account.
450
- 2. Open the `pocketshell` project page ->
451
- **Manage** -> **Publishing**.
452
- 3. Under **Trusted publishers**, click **Add a new pending publisher**
453
- (if the project is empty) or **Add a new publisher**, then fill in:
454
- - **PyPI Project Name**: `pocketshell`
455
- - **Owner**: `alexeygrigorev`
456
- - **Repository name**: `pocketshell`
457
- - **Workflow name**: `build.yml`
458
- - **Environment name**: `pypi`
459
- 4. Save the publisher.
460
- 5. In this repository on GitHub, open
461
- **Settings** -> **Environments** -> **New environment** -> name it
462
- `pypi`. No secrets or reviewers are required; the environment exists
463
- purely to scope the OIDC token. (If the environment already exists,
464
- confirm it has no protection rules that would block the workflow
465
- from running.)
466
- 6. Push the next release tag. The `Publish to PyPI via trusted
467
- publishing` step should succeed without any token configuration.
468
-
469
- ### Why trusted publishing (and not `PYPI_API_TOKEN`)?
470
-
471
- - No long-lived secret to rotate, leak, or accidentally print in logs.
472
- - The OIDC subject is scoped to `repo=alexeygrigorev/pocketshell`,
473
- `workflow=build.yml`, `environment=pypi`, so a compromised fork or
474
- a different workflow file in this repo cannot reuse it.
475
- - D22 (no backwards-compat): we do not also maintain a token-fallback
476
- path. If trusted publishing breaks, fix it; do not add a token
477
- branch alongside.
478
-
479
- If trusted publishing is ever unavailable for a tag (e.g. PyPI outage
480
- on the OIDC verifier), the recommended manual escape hatch is:
335
+ CI (`.github/workflows/ci.yml`) runs the pytest suite plus the packaging and
336
+ frozen-lock guards on every PR and every push to `main`. The first
337
+ self-governed release is **0.5.5**, continuing from the last app-coupled
338
+ release 0.5.4 so `uv tool upgrade` stays monotonic.
339
+
340
+ ### PyPI publish setup (one-time)
341
+
342
+ The `publish-pypi` job publishes with a PyPI API token stored as the
343
+ `PYPI_API_TOKEN` secret on this repo (same mechanism as
344
+ [PocketShell-io/quse](https://github.com/PocketShell-io/quse)):
345
+
346
+ 1. On pypi.org, create an API token scoped to the `pocketshell` project (or
347
+ reuse the existing project-scoped token from the monorepo era).
348
+ 2. On GitHub: this repo → Settings → Secrets and variables → Actions →
349
+ New repository secret → name `PYPI_API_TOKEN`. The `pypi` environment
350
+ already exists; the publish job runs inside it.
351
+
352
+ If a publish fails after the tag is pushed, fix the cause and re-run
353
+ `publish.yml` from the Actions tab (it has a `workflow_dispatch` trigger) —
354
+ the tag does not need to move.
355
+
356
+ Manual escape hatch (maintainer account, only when CI publishing is
357
+ unavailable):
481
358
 
482
359
  ```bash
483
- cd tools/pocketshell
484
- python -m build
485
- python -m twine upload dist/*
360
+ uv build
361
+ uv run --with twine twine upload dist/*
486
362
  ```
487
363
 
488
- with the maintainer's account. Do not re-add a `PYPI_API_TOKEN` secret
489
- as a permanent fallback.
490
-
491
364
  ## Why a unified CLI?
492
365
 
493
366
  The PocketShell app previously depended on multiple host-side tools.