pocketshell 0.4.24__tar.gz → 0.4.26__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.24 → pocketshell-0.4.26}/PKG-INFO +2 -1
- {pocketshell-0.4.24 → pocketshell-0.4.26}/pyproject.toml +12 -5
- {pocketshell-0.4.24 → pocketshell-0.4.26}/src/pocketshell/cards.py +40 -9
- {pocketshell-0.4.24 → pocketshell-0.4.26}/src/pocketshell/daemon.py +19 -15
- {pocketshell-0.4.24 → pocketshell-0.4.26}/src/pocketshell/hooks.py +79 -0
- pocketshell-0.4.26/src/pocketshell/usage.py +517 -0
- pocketshell-0.4.26/tests/data/quse-0.0.9-usage.json +132 -0
- pocketshell-0.4.26/tests/test_cards_push_notify.py +152 -0
- {pocketshell-0.4.24 → pocketshell-0.4.26}/tests/test_daemon.py +84 -41
- {pocketshell-0.4.24 → pocketshell-0.4.26}/tests/test_hooks.py +115 -2
- pocketshell-0.4.26/tests/test_usage.py +366 -0
- {pocketshell-0.4.24 → pocketshell-0.4.26}/tests/test_usage_capture.py +25 -1
- pocketshell-0.4.24/src/pocketshell/usage.py +0 -792
- pocketshell-0.4.24/tests/test_usage.py +0 -507
- {pocketshell-0.4.24 → pocketshell-0.4.26}/.gitignore +0 -0
- {pocketshell-0.4.24 → pocketshell-0.4.26}/README.md +0 -0
- {pocketshell-0.4.24 → pocketshell-0.4.26}/scheduler/README.md +0 -0
- {pocketshell-0.4.24 → pocketshell-0.4.26}/scheduler/pocketshell-usage-capture.service +0 -0
- {pocketshell-0.4.24 → pocketshell-0.4.26}/scheduler/pocketshell-usage-capture.timer +0 -0
- {pocketshell-0.4.24 → pocketshell-0.4.26}/src/pocketshell/__init__.py +0 -0
- {pocketshell-0.4.24 → pocketshell-0.4.26}/src/pocketshell/__main__.py +0 -0
- {pocketshell-0.4.24 → pocketshell-0.4.26}/src/pocketshell/agent_card_push.py +0 -0
- {pocketshell-0.4.24 → pocketshell-0.4.26}/src/pocketshell/agent_log.py +0 -0
- {pocketshell-0.4.24 → pocketshell-0.4.26}/src/pocketshell/agents.py +0 -0
- {pocketshell-0.4.24 → pocketshell-0.4.26}/src/pocketshell/agents_kind.py +0 -0
- {pocketshell-0.4.24 → pocketshell-0.4.26}/src/pocketshell/cgroup_agents.py +0 -0
- {pocketshell-0.4.24 → pocketshell-0.4.26}/src/pocketshell/cli.py +0 -0
- {pocketshell-0.4.24 → pocketshell-0.4.26}/src/pocketshell/env.py +0 -0
- {pocketshell-0.4.24 → pocketshell-0.4.26}/src/pocketshell/github.py +0 -0
- {pocketshell-0.4.24 → pocketshell-0.4.26}/src/pocketshell/jobs.py +0 -0
- {pocketshell-0.4.24 → pocketshell-0.4.26}/src/pocketshell/logs.py +0 -0
- {pocketshell-0.4.24 → pocketshell-0.4.26}/src/pocketshell/profiles.py +0 -0
- {pocketshell-0.4.24 → pocketshell-0.4.26}/src/pocketshell/prune_attachments.py +0 -0
- {pocketshell-0.4.24 → pocketshell-0.4.26}/src/pocketshell/push.py +0 -0
- {pocketshell-0.4.24 → pocketshell-0.4.26}/src/pocketshell/qr_share.py +0 -0
- {pocketshell-0.4.24 → pocketshell-0.4.26}/src/pocketshell/repos.py +0 -0
- {pocketshell-0.4.24 → pocketshell-0.4.26}/src/pocketshell/resume.py +0 -0
- {pocketshell-0.4.24 → pocketshell-0.4.26}/src/pocketshell/sessions.py +0 -0
- {pocketshell-0.4.24 → pocketshell-0.4.26}/src/pocketshell/tree.py +0 -0
- {pocketshell-0.4.24 → pocketshell-0.4.26}/src/pocketshell/usage_capture.py +0 -0
- {pocketshell-0.4.24 → pocketshell-0.4.26}/src/pocketshell/usage_reset.py +0 -0
- {pocketshell-0.4.24 → pocketshell-0.4.26}/tests/__init__.py +0 -0
- {pocketshell-0.4.24 → pocketshell-0.4.26}/tests/test_agent_card_push.py +0 -0
- {pocketshell-0.4.24 → pocketshell-0.4.26}/tests/test_agent_log.py +0 -0
- {pocketshell-0.4.24 → pocketshell-0.4.26}/tests/test_agents.py +0 -0
- {pocketshell-0.4.24 → pocketshell-0.4.26}/tests/test_agents_kind.py +0 -0
- {pocketshell-0.4.24 → pocketshell-0.4.26}/tests/test_cards.py +0 -0
- {pocketshell-0.4.24 → pocketshell-0.4.26}/tests/test_cgroup_agents.py +0 -0
- {pocketshell-0.4.24 → pocketshell-0.4.26}/tests/test_cli.py +0 -0
- {pocketshell-0.4.24 → pocketshell-0.4.26}/tests/test_env.py +0 -0
- {pocketshell-0.4.24 → pocketshell-0.4.26}/tests/test_github.py +0 -0
- {pocketshell-0.4.24 → pocketshell-0.4.26}/tests/test_jobs.py +0 -0
- {pocketshell-0.4.24 → pocketshell-0.4.26}/tests/test_logs.py +0 -0
- {pocketshell-0.4.24 → pocketshell-0.4.26}/tests/test_profiles.py +0 -0
- {pocketshell-0.4.24 → pocketshell-0.4.26}/tests/test_prune_attachments.py +0 -0
- {pocketshell-0.4.24 → pocketshell-0.4.26}/tests/test_push.py +0 -0
- {pocketshell-0.4.24 → pocketshell-0.4.26}/tests/test_qr_share.py +0 -0
- {pocketshell-0.4.24 → pocketshell-0.4.26}/tests/test_repos.py +0 -0
- {pocketshell-0.4.24 → pocketshell-0.4.26}/tests/test_resume.py +0 -0
- {pocketshell-0.4.24 → pocketshell-0.4.26}/tests/test_sessions.py +0 -0
- {pocketshell-0.4.24 → pocketshell-0.4.26}/tests/test_tree.py +0 -0
- {pocketshell-0.4.24 → pocketshell-0.4.26}/tests/test_usage_reset.py +0 -0
- {pocketshell-0.4.24 → pocketshell-0.4.26}/tests/test_watch_ci.py +0 -0
- {pocketshell-0.4.24 → pocketshell-0.4.26}/uv.lock +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pocketshell
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.26
|
|
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
|
|
@@ -21,6 +21,7 @@ Requires-Python: >=3.11
|
|
|
21
21
|
Requires-Dist: click>=8.2.0
|
|
22
22
|
Requires-Dist: google-auth>=2.0.0
|
|
23
23
|
Requires-Dist: pyyaml>=6.0
|
|
24
|
+
Requires-Dist: quse==0.0.9
|
|
24
25
|
Requires-Dist: tmuxctl>=0.3.3
|
|
25
26
|
Provides-Extra: dev
|
|
26
27
|
Requires-Dist: pytest>=8.4.0; extra == 'dev'
|
|
@@ -8,7 +8,7 @@ name = "pocketshell"
|
|
|
8
8
|
# scripts/check-pypi-version.sh enforces this; .github/workflows/build.yml
|
|
9
9
|
# runs that check before publishing to PyPI. See
|
|
10
10
|
# tools/pocketshell/README.md ("Release flow") for the bump procedure.
|
|
11
|
-
version = "0.4.
|
|
11
|
+
version = "0.4.26"
|
|
12
12
|
description = "Unified server-side Python utility for the PocketShell Android client."
|
|
13
13
|
readme = "README.md"
|
|
14
14
|
requires-python = ">=3.11"
|
|
@@ -27,12 +27,19 @@ classifiers = [
|
|
|
27
27
|
"Topic :: Software Development",
|
|
28
28
|
"Topic :: System :: Monitoring",
|
|
29
29
|
]
|
|
30
|
-
# `pocketshell usage` delegates to the
|
|
31
|
-
# (
|
|
32
|
-
#
|
|
33
|
-
#
|
|
30
|
+
# `pocketshell usage` delegates to the `quse` CLI, which is now a PINNED
|
|
31
|
+
# hard dependency (issue #1318): quse owns the unified usage schema, so we
|
|
32
|
+
# ship a known-good quse WITH pocketshell and invoke that pinned console-
|
|
33
|
+
# script (next to `sys.executable`, never PATH — see `pocketshell/usage.py`
|
|
34
|
+
# `_resolve_quse_binary`). A host-level quse upgrade must not reach us.
|
|
34
35
|
dependencies = [
|
|
35
36
|
"click>=8.2.0",
|
|
37
|
+
# Usage backend + single source of truth for the unified provider schema
|
|
38
|
+
# (issue #1318). Pinned exactly: `pocketshell usage --json` expects quse
|
|
39
|
+
# 0.0.9's provider-keyed `--json` document (per provider: status +
|
|
40
|
+
# short_term/long_term {percent_remaining, reset_at, window} + error) and
|
|
41
|
+
# fails loudly on any schema drift, so the version is frozen, not a range.
|
|
42
|
+
"quse==0.0.9",
|
|
36
43
|
# FCM HTTP v1 push delivery (#690): service-account OAuth2 bearer minting
|
|
37
44
|
# for `pocketshell push` / the `usage --capture` reset-push send. Imported
|
|
38
45
|
# lazily and fail-soft — a host without it (or without a Firebase
|
|
@@ -572,6 +572,29 @@ def _require_session(explicit: Optional[str]) -> str:
|
|
|
572
572
|
return session
|
|
573
573
|
|
|
574
574
|
|
|
575
|
+
def _notify_card_pushed_best_effort(
|
|
576
|
+
session: str, card: dict[str, Any], *, card_paths: CardPaths
|
|
577
|
+
) -> None:
|
|
578
|
+
"""Fire a best-effort FCM push for a freshly upserted card (#859 Slice D/2).
|
|
579
|
+
|
|
580
|
+
The single notification path shared by EVERY card-creating ``push`` verb
|
|
581
|
+
(``checklist``, ``note``, and any future card type). Centralising it here is
|
|
582
|
+
the durable class fix for #1446: a new ``push <cardtype>`` verb that upserts
|
|
583
|
+
a card just calls this one helper, so it can't silently forget to notify the
|
|
584
|
+
phone the way ``push note`` originally did.
|
|
585
|
+
|
|
586
|
+
Fail-soft — the push deps are imported lazily so a host without them still
|
|
587
|
+
pushes the card, and any failure is swallowed so a card write never wedges
|
|
588
|
+
the CLI. De-dup + configuration guards live in :func:`notify_card_pushed`.
|
|
589
|
+
"""
|
|
590
|
+
try:
|
|
591
|
+
from pocketshell.agent_card_push import notify_card_pushed
|
|
592
|
+
|
|
593
|
+
notify_card_pushed(session, card, card_paths=card_paths)
|
|
594
|
+
except Exception:
|
|
595
|
+
pass
|
|
596
|
+
|
|
597
|
+
|
|
575
598
|
def register_push_card_commands(push_group: click.Group) -> None:
|
|
576
599
|
"""Register the typed-card verbs onto the existing ``push`` click group.
|
|
577
600
|
|
|
@@ -635,14 +658,8 @@ def register_push_card_commands(push_group: click.Group) -> None:
|
|
|
635
658
|
card_paths = resolve_paths()
|
|
636
659
|
path = upsert_card(target, card, paths=card_paths)
|
|
637
660
|
# Slice D (#859): fire a best-effort FCM push so the phone surfaces a
|
|
638
|
-
# heads-up notification opening this session's card feed.
|
|
639
|
-
|
|
640
|
-
try:
|
|
641
|
-
from pocketshell.agent_card_push import notify_card_pushed
|
|
642
|
-
|
|
643
|
-
notify_card_pushed(target, card, card_paths=card_paths)
|
|
644
|
-
except Exception:
|
|
645
|
-
pass
|
|
661
|
+
# heads-up notification opening this session's card feed.
|
|
662
|
+
_notify_card_pushed_best_effort(target, card, card_paths=card_paths)
|
|
646
663
|
click.echo(
|
|
647
664
|
f"checklist {card_id!r} ({len(parsed)} items) -> session {target!r} ({path})"
|
|
648
665
|
)
|
|
@@ -652,6 +669,8 @@ def register_push_card_commands(push_group: click.Group) -> None:
|
|
|
652
669
|
@click.option("--session", "session", default=None, help="Override the auto-detected tmux session.")
|
|
653
670
|
def push_get(as_json: bool, session: Optional[str]) -> None:
|
|
654
671
|
"""Return the session's cards (human/YAML by default, --json for the app)."""
|
|
672
|
+
# No notify (#1446 audit): read-only query, upserts no card. The app is
|
|
673
|
+
# the caller here; there is nothing new to surface.
|
|
655
674
|
target = _require_session(session)
|
|
656
675
|
cards = read_cards(target, paths=resolve_paths())
|
|
657
676
|
if as_json:
|
|
@@ -677,6 +696,7 @@ def register_push_card_commands(push_group: click.Group) -> None:
|
|
|
677
696
|
@click.option("--session", "session", default=None, help="Override the auto-detected tmux session.")
|
|
678
697
|
def push_status(card_id: Optional[str], as_json: bool, session: Optional[str]) -> None:
|
|
679
698
|
"""Report interaction state — which checklist items the human has ticked."""
|
|
699
|
+
# No notify (#1446 audit): read-only query, upserts no card.
|
|
680
700
|
target = _require_session(session)
|
|
681
701
|
cards = read_cards(target, paths=resolve_paths())
|
|
682
702
|
if card_id is not None:
|
|
@@ -712,6 +732,9 @@ def register_push_card_commands(push_group: click.Group) -> None:
|
|
|
712
732
|
@click.option("--session", "session", default=None, help="Override the auto-detected tmux session.")
|
|
713
733
|
def push_check(card_id: str, item_id: str, done: bool, session: Optional[str]) -> None:
|
|
714
734
|
"""Set a checklist item's checked state (this is what the app's tick calls)."""
|
|
735
|
+
# No notify (#1446 audit): this is the APP writing back the human's tick,
|
|
736
|
+
# not an agent pushing a new card. Notifying would push the phone about
|
|
737
|
+
# its own action (and could loop). It mutates state, never upserts a card.
|
|
715
738
|
target = _require_session(session)
|
|
716
739
|
try:
|
|
717
740
|
card = apply_interaction(
|
|
@@ -771,7 +794,13 @@ def register_push_card_commands(push_group: click.Group) -> None:
|
|
|
771
794
|
title=title,
|
|
772
795
|
build_kwargs={"text": body_text},
|
|
773
796
|
)
|
|
774
|
-
|
|
797
|
+
card_paths = resolve_paths()
|
|
798
|
+
path = upsert_card(target, card, paths=card_paths)
|
|
799
|
+
# #1446: a pushed note is a card the human must see, just like a
|
|
800
|
+
# checklist — fire the same best-effort FCM push so the phone surfaces a
|
|
801
|
+
# heads-up notification opening this session's card feed. (Slice D/#859
|
|
802
|
+
# shipped this only on `checklist`; `push note` was silently omitted.)
|
|
803
|
+
_notify_card_pushed_best_effort(target, card, card_paths=card_paths)
|
|
775
804
|
click.echo(f"note {card_id!r} -> session {target!r} ({path})")
|
|
776
805
|
|
|
777
806
|
@push_group.command("read")
|
|
@@ -780,6 +809,8 @@ def register_push_card_commands(push_group: click.Group) -> None:
|
|
|
780
809
|
@click.option("--session", "session", default=None, help="Override the auto-detected tmux session.")
|
|
781
810
|
def push_read(card_id: str, read: bool, session: Optional[str]) -> None:
|
|
782
811
|
"""Set a note's read state (this is what the app's "mark read" calls)."""
|
|
812
|
+
# No notify (#1446 audit): this is the APP writing back "human read it",
|
|
813
|
+
# not an agent pushing a new card. Same rationale as `push check`.
|
|
783
814
|
target = _require_session(session)
|
|
784
815
|
try:
|
|
785
816
|
card = apply_interaction(
|
|
@@ -358,11 +358,11 @@ def _usage_fetch_handler(params: Mapping[str, Any]) -> dict[str, Any]:
|
|
|
358
358
|
}
|
|
359
359
|
|
|
360
360
|
Returning stdout in the envelope preserves the CLI/daemon contract.
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
361
|
+
``quse --json`` emits a **provider-keyed JSON object**; the daemon
|
|
362
|
+
FLATTENS it into per-provider NDJSON via
|
|
363
|
+
:func:`pocketshell.usage.normalize_usage_stdout` before caching, so the
|
|
364
|
+
cached envelope stdout is already the app-facing NDJSON wire format (the
|
|
365
|
+
CLI proxies it verbatim, without re-flattening).
|
|
366
366
|
"""
|
|
367
367
|
# Lazy import to avoid a circular module load at startup: the
|
|
368
368
|
# daemon module is imported from ``cli.py`` which also imports
|
|
@@ -378,17 +378,13 @@ def _usage_fetch_handler(params: Mapping[str, Any]) -> dict[str, Any]:
|
|
|
378
378
|
|
|
379
379
|
quse_path = _usage._resolve_quse_binary()
|
|
380
380
|
if quse_path is None:
|
|
381
|
-
#
|
|
382
|
-
#
|
|
383
|
-
#
|
|
381
|
+
# quse is bundled WITH pocketshell (issue #1318): a missing pinned
|
|
382
|
+
# copy is a packaging-integrity error, not a "install quse" nag. The
|
|
383
|
+
# daemon does NOT cache this failure.
|
|
384
384
|
return {
|
|
385
385
|
"stdout": "",
|
|
386
|
-
"stderr":
|
|
387
|
-
|
|
388
|
-
"Install it via `uv tool install quse` or `pipx install quse` "
|
|
389
|
-
"and re-run.\n"
|
|
390
|
-
),
|
|
391
|
-
"returncode": 127,
|
|
386
|
+
"stderr": _usage._QUSE_MISSING_MESSAGE + "\n",
|
|
387
|
+
"returncode": _usage._QUSE_MISSING_EXIT_CODE,
|
|
392
388
|
"provider": provider,
|
|
393
389
|
}
|
|
394
390
|
|
|
@@ -402,8 +398,16 @@ def _usage_fetch_handler(params: Mapping[str, Any]) -> dict[str, Any]:
|
|
|
402
398
|
capture_output=True,
|
|
403
399
|
text=True,
|
|
404
400
|
)
|
|
401
|
+
# Only a successful (exit 0) quse run is flattened into per-provider
|
|
402
|
+
# NDJSON. A failed fetch is proxied raw (it is not a valid provider-keyed
|
|
403
|
+
# document, and the daemon does not cache failures anyway).
|
|
404
|
+
stdout = (
|
|
405
|
+
_usage.normalize_usage_stdout(completed.stdout)
|
|
406
|
+
if completed.returncode == 0
|
|
407
|
+
else completed.stdout
|
|
408
|
+
)
|
|
405
409
|
return {
|
|
406
|
-
"stdout":
|
|
410
|
+
"stdout": stdout,
|
|
407
411
|
"stderr": completed.stderr,
|
|
408
412
|
"returncode": completed.returncode,
|
|
409
413
|
"provider": provider,
|
|
@@ -195,6 +195,7 @@ stop and never injects a follow-up (integration only; see D26).
|
|
|
195
195
|
"""
|
|
196
196
|
import json
|
|
197
197
|
import os
|
|
198
|
+
import subprocess
|
|
198
199
|
import sys
|
|
199
200
|
from datetime import datetime, timezone
|
|
200
201
|
|
|
@@ -202,6 +203,32 @@ HERE = os.path.dirname(os.path.abspath(__file__))
|
|
|
202
203
|
EVENTS_FILE = os.path.join(HERE, "events.jsonl")
|
|
203
204
|
|
|
204
205
|
|
|
206
|
+
def record_tmux_agent_state(state):
|
|
207
|
+
if "TMUX" not in os.environ:
|
|
208
|
+
return
|
|
209
|
+
option_value = {
|
|
210
|
+
"FINISHED": "idle",
|
|
211
|
+
"WAITING_FOR_INPUT": "waiting_for_input",
|
|
212
|
+
}.get(state)
|
|
213
|
+
if option_value is None:
|
|
214
|
+
return
|
|
215
|
+
try:
|
|
216
|
+
subprocess.run(
|
|
217
|
+
["tmux", "set-option", "@ps_agent_state", option_value],
|
|
218
|
+
stdout=subprocess.DEVNULL,
|
|
219
|
+
stderr=subprocess.DEVNULL,
|
|
220
|
+
check=False,
|
|
221
|
+
)
|
|
222
|
+
subprocess.run(
|
|
223
|
+
["tmux", "set-option", "@ps_agent_state_updated_at", datetime.now(timezone.utc).isoformat()],
|
|
224
|
+
stdout=subprocess.DEVNULL,
|
|
225
|
+
stderr=subprocess.DEVNULL,
|
|
226
|
+
check=False,
|
|
227
|
+
)
|
|
228
|
+
except (OSError, subprocess.SubprocessError):
|
|
229
|
+
pass
|
|
230
|
+
|
|
231
|
+
|
|
205
232
|
def main():
|
|
206
233
|
raw = sys.stdin.read()
|
|
207
234
|
try:
|
|
@@ -232,6 +259,7 @@ def main():
|
|
|
232
259
|
os.makedirs(os.path.dirname(EVENTS_FILE), exist_ok=True)
|
|
233
260
|
with open(EVENTS_FILE, "a") as handle:
|
|
234
261
|
handle.write(json.dumps(record) + "\\n")
|
|
262
|
+
record_tmux_agent_state(state)
|
|
235
263
|
# Exit clean with no stdout so Claude proceeds with the stop.
|
|
236
264
|
sys.exit(0)
|
|
237
265
|
|
|
@@ -252,6 +280,7 @@ event bus next to this script.
|
|
|
252
280
|
"""
|
|
253
281
|
import json
|
|
254
282
|
import os
|
|
283
|
+
import subprocess
|
|
255
284
|
import sys
|
|
256
285
|
from datetime import datetime, timezone
|
|
257
286
|
|
|
@@ -259,6 +288,32 @@ HERE = os.path.dirname(os.path.abspath(__file__))
|
|
|
259
288
|
EVENTS_FILE = os.path.join(HERE, "events.jsonl")
|
|
260
289
|
|
|
261
290
|
|
|
291
|
+
def record_tmux_agent_state(state):
|
|
292
|
+
if "TMUX" not in os.environ:
|
|
293
|
+
return
|
|
294
|
+
option_value = {
|
|
295
|
+
"FINISHED": "idle",
|
|
296
|
+
"WAITING_FOR_INPUT": "waiting_for_input",
|
|
297
|
+
}.get(state)
|
|
298
|
+
if option_value is None:
|
|
299
|
+
return
|
|
300
|
+
try:
|
|
301
|
+
subprocess.run(
|
|
302
|
+
["tmux", "set-option", "@ps_agent_state", option_value],
|
|
303
|
+
stdout=subprocess.DEVNULL,
|
|
304
|
+
stderr=subprocess.DEVNULL,
|
|
305
|
+
check=False,
|
|
306
|
+
)
|
|
307
|
+
subprocess.run(
|
|
308
|
+
["tmux", "set-option", "@ps_agent_state_updated_at", datetime.now(timezone.utc).isoformat()],
|
|
309
|
+
stdout=subprocess.DEVNULL,
|
|
310
|
+
stderr=subprocess.DEVNULL,
|
|
311
|
+
check=False,
|
|
312
|
+
)
|
|
313
|
+
except (OSError, subprocess.SubprocessError):
|
|
314
|
+
pass
|
|
315
|
+
|
|
316
|
+
|
|
262
317
|
def main():
|
|
263
318
|
raw = sys.argv[1] if len(sys.argv) > 1 else "{}"
|
|
264
319
|
try:
|
|
@@ -283,6 +338,7 @@ def main():
|
|
|
283
338
|
os.makedirs(os.path.dirname(EVENTS_FILE), exist_ok=True)
|
|
284
339
|
with open(EVENTS_FILE, "a") as handle:
|
|
285
340
|
handle.write(json.dumps(record) + "\\n")
|
|
341
|
+
record_tmux_agent_state(state)
|
|
286
342
|
sys.exit(0)
|
|
287
343
|
|
|
288
344
|
|
|
@@ -300,6 +356,7 @@ _OPENCODE_PLUGIN_SOURCE = '''\
|
|
|
300
356
|
// (WAITING_FOR_INPUT), or errors (ERROR). Integration only — no
|
|
301
357
|
// continue/stop decision (see D26).
|
|
302
358
|
import fs from "node:fs";
|
|
359
|
+
import child_process from "node:child_process";
|
|
303
360
|
import os from "node:os";
|
|
304
361
|
import path from "node:path";
|
|
305
362
|
|
|
@@ -311,6 +368,27 @@ function busPath() {
|
|
|
311
368
|
return path.join(dir, "events.jsonl");
|
|
312
369
|
}
|
|
313
370
|
|
|
371
|
+
function recordTmuxAgentState(state) {
|
|
372
|
+
if (!process.env.TMUX) return;
|
|
373
|
+
const value = {
|
|
374
|
+
FINISHED: "idle",
|
|
375
|
+
WAITING_FOR_INPUT: "waiting_for_input",
|
|
376
|
+
}[state];
|
|
377
|
+
if (!value) return;
|
|
378
|
+
try {
|
|
379
|
+
child_process.spawnSync("tmux", ["set-option", "@ps_agent_state", value], {
|
|
380
|
+
stdio: "ignore",
|
|
381
|
+
});
|
|
382
|
+
child_process.spawnSync(
|
|
383
|
+
"tmux",
|
|
384
|
+
["set-option", "@ps_agent_state_updated_at", new Date().toISOString()],
|
|
385
|
+
{ stdio: "ignore" },
|
|
386
|
+
);
|
|
387
|
+
} catch (e) {
|
|
388
|
+
// best-effort; never throw out of a plugin hook
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
314
392
|
export const PocketShellIdleSignal = async () => {
|
|
315
393
|
const EVENTS_FILE = busPath();
|
|
316
394
|
function emit(state, event) {
|
|
@@ -325,6 +403,7 @@ export const PocketShellIdleSignal = async () => {
|
|
|
325
403
|
try {
|
|
326
404
|
fs.mkdirSync(path.dirname(EVENTS_FILE), { recursive: true });
|
|
327
405
|
fs.appendFileSync(EVENTS_FILE, JSON.stringify(rec) + "\\n");
|
|
406
|
+
recordTmuxAgentState(state);
|
|
328
407
|
} catch (e) {
|
|
329
408
|
// best-effort; never throw out of a plugin hook
|
|
330
409
|
}
|