portacode 1.5.18.dev0__tar.gz → 1.5.19.dev1__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 (448) hide show
  1. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/.gitignore +2 -0
  2. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/Makefile +9 -5
  3. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/PKG-INFO +1 -1
  4. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/portacode/_version.py +2 -2
  5. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/portacode/cli.py +71 -5
  6. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/portacode/codex_loopback_proxy.py +107 -1
  7. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/portacode/connection/client.py +10 -0
  8. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/portacode/connection/codex_app_server.py +21 -3
  9. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/portacode/connection/handlers/__init__.py +2 -0
  10. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/portacode/connection/handlers/codex_handlers.py +124 -5
  11. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/portacode/connection/handlers/file_handlers.py +54 -0
  12. portacode-1.5.19.dev1/portacode/connection/handlers/test_file_move_copy.py +81 -0
  13. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/portacode/connection/terminal.py +2 -0
  14. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/portacode/data.py +16 -1
  15. portacode-1.5.19.dev1/portacode/github_credential.py +122 -0
  16. portacode-1.5.19.dev1/portacode/singleton.py +124 -0
  17. portacode-1.5.19.dev1/portacode/test_singleton.py +34 -0
  18. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/portacode/tests/test_codex_app_server.py +6 -1
  19. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/portacode/tests/test_codex_handlers.py +65 -0
  20. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/portacode/tests/test_codex_loopback_proxy.py +66 -1
  21. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/portacode.egg-info/PKG-INFO +1 -1
  22. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/portacode.egg-info/SOURCES.txt +4 -3
  23. portacode-1.5.19.dev1/portacode.egg-info/entry_points.txt +3 -0
  24. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/setup.py +4 -3
  25. portacode-1.5.18.dev0/backup.sh +0 -42
  26. portacode-1.5.18.dev0/docker-compose.yaml +0 -292
  27. portacode-1.5.18.dev0/portacode.egg-info/entry_points.txt +0 -2
  28. portacode-1.5.18.dev0/restore.sh +0 -56
  29. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/.claude/agents/communication-manager.md +0 -0
  30. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/.claude/settings.local.json +0 -0
  31. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/.gitmodules +0 -0
  32. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/LICENSE +0 -0
  33. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/MANIFEST.in +0 -0
  34. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/README.md +0 -0
  35. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/build_android.sh +0 -0
  36. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/connect.py +0 -0
  37. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/connect.sh +0 -0
  38. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/docs/cloudflared-domain-connect-containers-audit.md +0 -0
  39. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/docs/creative-team-brief-portacode.md +0 -0
  40. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/docs/device-health-alerting-plan.md +0 -0
  41. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/docs/devops-messaging-ab-tests.md +0 -0
  42. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/docs/homepage-dashboard-positioning-fixes.md +0 -0
  43. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/docs/images/device-transfer-button.png +0 -0
  44. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/docs/images/device-transfer-modal.png +0 -0
  45. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/docs/images/pair-device-button.png +0 -0
  46. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/docs/images/pairing-request.png +0 -0
  47. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/docs/images/student-workspace.png +0 -0
  48. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/docs/template-guide.html +0 -0
  49. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/README.md +0 -0
  50. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/simple_device/Dockerfile +0 -0
  51. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/simple_device/README.md +0 -0
  52. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/simple_device/data/device-01/.local/share/portacode/keys/id_portacode +0 -0
  53. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/simple_device/data/device-01/.local/share/portacode/keys/id_portacode.pub +0 -0
  54. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/simple_device/docker-compose.yaml +0 -0
  55. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/Dockerfile +0 -0
  56. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/README.md +0 -0
  57. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-01/.local/share/portacode/keys/id_portacode +0 -0
  58. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-01/.local/share/portacode/keys/id_portacode.pub +0 -0
  59. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-01/.local/share/portacode/run/gateway.pid +0 -0
  60. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-01/workspace/.gitignore +0 -0
  61. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-01/workspace/README.md +0 -0
  62. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-01/workspace/db.sqlite3 +0 -0
  63. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-01/workspace/galactic_bakeshop/__init__.py +0 -0
  64. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-01/workspace/galactic_bakeshop/__pycache__/__init__.cpython-311.pyc +0 -0
  65. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-01/workspace/galactic_bakeshop/__pycache__/settings.cpython-311.pyc +0 -0
  66. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-01/workspace/galactic_bakeshop/__pycache__/urls.cpython-311.pyc +0 -0
  67. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-01/workspace/galactic_bakeshop/__pycache__/wsgi.cpython-311.pyc +0 -0
  68. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-01/workspace/galactic_bakeshop/asgi.py +0 -0
  69. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-01/workspace/galactic_bakeshop/settings.py +0 -0
  70. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-01/workspace/galactic_bakeshop/urls.py +0 -0
  71. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-01/workspace/galactic_bakeshop/wsgi.py +0 -0
  72. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-01/workspace/manage.py +0 -0
  73. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-01/workspace/requirements.txt +0 -0
  74. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-01/workspace/templates/treats/home.html +0 -0
  75. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-01/workspace/treats/__init__.py +0 -0
  76. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-01/workspace/treats/__pycache__/__init__.cpython-311.pyc +0 -0
  77. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-01/workspace/treats/__pycache__/admin.cpython-311.pyc +0 -0
  78. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-01/workspace/treats/__pycache__/apps.cpython-311.pyc +0 -0
  79. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-01/workspace/treats/__pycache__/menu.cpython-311.pyc +0 -0
  80. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-01/workspace/treats/__pycache__/models.cpython-311.pyc +0 -0
  81. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-01/workspace/treats/__pycache__/urls.cpython-311.pyc +0 -0
  82. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-01/workspace/treats/__pycache__/views.cpython-311.pyc +0 -0
  83. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-01/workspace/treats/admin.py +0 -0
  84. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-01/workspace/treats/apps.py +0 -0
  85. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-01/workspace/treats/menu.py +0 -0
  86. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-01/workspace/treats/migrations/__init__.py +0 -0
  87. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-01/workspace/treats/migrations/__pycache__/__init__.cpython-311.pyc +0 -0
  88. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-01/workspace/treats/models.py +0 -0
  89. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-01/workspace/treats/tests.py +0 -0
  90. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-01/workspace/treats/urls.py +0 -0
  91. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-01/workspace/treats/views.py +0 -0
  92. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-02/.local/share/portacode/keys/id_portacode +0 -0
  93. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-02/.local/share/portacode/keys/id_portacode.pub +0 -0
  94. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-02/workspace/README.md +0 -0
  95. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-02/workspace/galactic_bakeshop/__init__.py +0 -0
  96. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-02/workspace/galactic_bakeshop/asgi.py +0 -0
  97. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-02/workspace/galactic_bakeshop/settings.py +0 -0
  98. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-02/workspace/galactic_bakeshop/urls.py +0 -0
  99. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-02/workspace/galactic_bakeshop/wsgi.py +0 -0
  100. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-02/workspace/manage.py +0 -0
  101. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-02/workspace/requirements.txt +0 -0
  102. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-02/workspace/templates/treats/home.html +0 -0
  103. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-02/workspace/treats/__init__.py +0 -0
  104. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-02/workspace/treats/admin.py +0 -0
  105. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-02/workspace/treats/apps.py +0 -0
  106. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-02/workspace/treats/menu.py +0 -0
  107. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-02/workspace/treats/migrations/__init__.py +0 -0
  108. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-02/workspace/treats/models.py +0 -0
  109. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-02/workspace/treats/tests.py +0 -0
  110. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-02/workspace/treats/urls.py +0 -0
  111. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-02/workspace/treats/views.py +0 -0
  112. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-03/.local/share/portacode/keys/id_portacode +0 -0
  113. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-03/.local/share/portacode/keys/id_portacode.pub +0 -0
  114. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-03/workspace/README.md +0 -0
  115. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-03/workspace/galactic_bakeshop/__init__.py +0 -0
  116. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-03/workspace/galactic_bakeshop/asgi.py +0 -0
  117. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-03/workspace/galactic_bakeshop/settings.py +0 -0
  118. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-03/workspace/galactic_bakeshop/urls.py +0 -0
  119. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-03/workspace/galactic_bakeshop/wsgi.py +0 -0
  120. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-03/workspace/manage.py +0 -0
  121. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-03/workspace/requirements.txt +0 -0
  122. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-03/workspace/templates/treats/home.html +0 -0
  123. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-03/workspace/treats/__init__.py +0 -0
  124. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-03/workspace/treats/admin.py +0 -0
  125. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-03/workspace/treats/apps.py +0 -0
  126. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-03/workspace/treats/menu.py +0 -0
  127. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-03/workspace/treats/migrations/__init__.py +0 -0
  128. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-03/workspace/treats/models.py +0 -0
  129. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-03/workspace/treats/tests.py +0 -0
  130. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-03/workspace/treats/urls.py +0 -0
  131. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-03/workspace/treats/views.py +0 -0
  132. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-04/.local/share/portacode/keys/id_portacode +0 -0
  133. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-04/.local/share/portacode/keys/id_portacode.pub +0 -0
  134. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-04/workspace/README.md +0 -0
  135. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-04/workspace/galactic_bakeshop/__init__.py +0 -0
  136. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-04/workspace/galactic_bakeshop/asgi.py +0 -0
  137. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-04/workspace/galactic_bakeshop/settings.py +0 -0
  138. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-04/workspace/galactic_bakeshop/urls.py +0 -0
  139. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-04/workspace/galactic_bakeshop/wsgi.py +0 -0
  140. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-04/workspace/manage.py +0 -0
  141. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-04/workspace/requirements.txt +0 -0
  142. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-04/workspace/templates/treats/home.html +0 -0
  143. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-04/workspace/treats/__init__.py +0 -0
  144. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-04/workspace/treats/admin.py +0 -0
  145. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-04/workspace/treats/apps.py +0 -0
  146. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-04/workspace/treats/menu.py +0 -0
  147. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-04/workspace/treats/migrations/__init__.py +0 -0
  148. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-04/workspace/treats/models.py +0 -0
  149. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-04/workspace/treats/tests.py +0 -0
  150. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-04/workspace/treats/urls.py +0 -0
  151. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-04/workspace/treats/views.py +0 -0
  152. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-05/.local/share/portacode/keys/id_portacode +0 -0
  153. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-05/.local/share/portacode/keys/id_portacode.pub +0 -0
  154. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-05/workspace/README.md +0 -0
  155. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-05/workspace/galactic_bakeshop/__init__.py +0 -0
  156. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-05/workspace/galactic_bakeshop/asgi.py +0 -0
  157. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-05/workspace/galactic_bakeshop/settings.py +0 -0
  158. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-05/workspace/galactic_bakeshop/urls.py +0 -0
  159. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-05/workspace/galactic_bakeshop/wsgi.py +0 -0
  160. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-05/workspace/manage.py +0 -0
  161. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-05/workspace/requirements.txt +0 -0
  162. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-05/workspace/templates/treats/home.html +0 -0
  163. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-05/workspace/treats/__init__.py +0 -0
  164. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-05/workspace/treats/admin.py +0 -0
  165. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-05/workspace/treats/apps.py +0 -0
  166. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-05/workspace/treats/menu.py +0 -0
  167. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-05/workspace/treats/migrations/__init__.py +0 -0
  168. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-05/workspace/treats/models.py +0 -0
  169. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-05/workspace/treats/tests.py +0 -0
  170. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-05/workspace/treats/urls.py +0 -0
  171. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-05/workspace/treats/views.py +0 -0
  172. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-06/.local/share/portacode/keys/id_portacode +0 -0
  173. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-06/.local/share/portacode/keys/id_portacode.pub +0 -0
  174. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-06/workspace/README.md +0 -0
  175. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-06/workspace/galactic_bakeshop/__init__.py +0 -0
  176. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-06/workspace/galactic_bakeshop/asgi.py +0 -0
  177. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-06/workspace/galactic_bakeshop/settings.py +0 -0
  178. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-06/workspace/galactic_bakeshop/urls.py +0 -0
  179. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-06/workspace/galactic_bakeshop/wsgi.py +0 -0
  180. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-06/workspace/manage.py +0 -0
  181. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-06/workspace/requirements.txt +0 -0
  182. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-06/workspace/templates/treats/home.html +0 -0
  183. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-06/workspace/treats/__init__.py +0 -0
  184. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-06/workspace/treats/admin.py +0 -0
  185. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-06/workspace/treats/apps.py +0 -0
  186. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-06/workspace/treats/menu.py +0 -0
  187. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-06/workspace/treats/migrations/__init__.py +0 -0
  188. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-06/workspace/treats/models.py +0 -0
  189. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-06/workspace/treats/tests.py +0 -0
  190. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-06/workspace/treats/urls.py +0 -0
  191. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-06/workspace/treats/views.py +0 -0
  192. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-07/.local/share/portacode/keys/id_portacode +0 -0
  193. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-07/.local/share/portacode/keys/id_portacode.pub +0 -0
  194. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-07/workspace/README.md +0 -0
  195. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-07/workspace/galactic_bakeshop/__init__.py +0 -0
  196. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-07/workspace/galactic_bakeshop/asgi.py +0 -0
  197. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-07/workspace/galactic_bakeshop/settings.py +0 -0
  198. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-07/workspace/galactic_bakeshop/urls.py +0 -0
  199. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-07/workspace/galactic_bakeshop/wsgi.py +0 -0
  200. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-07/workspace/manage.py +0 -0
  201. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-07/workspace/requirements.txt +0 -0
  202. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-07/workspace/templates/treats/home.html +0 -0
  203. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-07/workspace/treats/__init__.py +0 -0
  204. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-07/workspace/treats/admin.py +0 -0
  205. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-07/workspace/treats/apps.py +0 -0
  206. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-07/workspace/treats/menu.py +0 -0
  207. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-07/workspace/treats/migrations/__init__.py +0 -0
  208. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-07/workspace/treats/models.py +0 -0
  209. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-07/workspace/treats/tests.py +0 -0
  210. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-07/workspace/treats/urls.py +0 -0
  211. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-07/workspace/treats/views.py +0 -0
  212. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-08/.local/share/portacode/keys/id_portacode +0 -0
  213. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-08/.local/share/portacode/keys/id_portacode.pub +0 -0
  214. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-08/workspace/README.md +0 -0
  215. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-08/workspace/galactic_bakeshop/__init__.py +0 -0
  216. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-08/workspace/galactic_bakeshop/asgi.py +0 -0
  217. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-08/workspace/galactic_bakeshop/settings.py +0 -0
  218. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-08/workspace/galactic_bakeshop/urls.py +0 -0
  219. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-08/workspace/galactic_bakeshop/wsgi.py +0 -0
  220. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-08/workspace/manage.py +0 -0
  221. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-08/workspace/requirements.txt +0 -0
  222. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-08/workspace/templates/treats/home.html +0 -0
  223. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-08/workspace/treats/__init__.py +0 -0
  224. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-08/workspace/treats/admin.py +0 -0
  225. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-08/workspace/treats/apps.py +0 -0
  226. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-08/workspace/treats/menu.py +0 -0
  227. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-08/workspace/treats/migrations/__init__.py +0 -0
  228. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-08/workspace/treats/models.py +0 -0
  229. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-08/workspace/treats/tests.py +0 -0
  230. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-08/workspace/treats/urls.py +0 -0
  231. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-08/workspace/treats/views.py +0 -0
  232. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-09/.local/share/portacode/keys/id_portacode +0 -0
  233. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-09/.local/share/portacode/keys/id_portacode.pub +0 -0
  234. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-09/workspace/README.md +0 -0
  235. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-09/workspace/galactic_bakeshop/__init__.py +0 -0
  236. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-09/workspace/galactic_bakeshop/asgi.py +0 -0
  237. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-09/workspace/galactic_bakeshop/settings.py +0 -0
  238. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-09/workspace/galactic_bakeshop/urls.py +0 -0
  239. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-09/workspace/galactic_bakeshop/wsgi.py +0 -0
  240. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-09/workspace/manage.py +0 -0
  241. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-09/workspace/requirements.txt +0 -0
  242. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-09/workspace/templates/treats/home.html +0 -0
  243. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-09/workspace/treats/__init__.py +0 -0
  244. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-09/workspace/treats/admin.py +0 -0
  245. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-09/workspace/treats/apps.py +0 -0
  246. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-09/workspace/treats/menu.py +0 -0
  247. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-09/workspace/treats/migrations/__init__.py +0 -0
  248. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-09/workspace/treats/models.py +0 -0
  249. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-09/workspace/treats/tests.py +0 -0
  250. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-09/workspace/treats/urls.py +0 -0
  251. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-09/workspace/treats/views.py +0 -0
  252. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-10/.local/share/portacode/keys/id_portacode +0 -0
  253. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-10/.local/share/portacode/keys/id_portacode.pub +0 -0
  254. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-10/workspace/README.md +0 -0
  255. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-10/workspace/galactic_bakeshop/__init__.py +0 -0
  256. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-10/workspace/galactic_bakeshop/asgi.py +0 -0
  257. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-10/workspace/galactic_bakeshop/settings.py +0 -0
  258. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-10/workspace/galactic_bakeshop/urls.py +0 -0
  259. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-10/workspace/galactic_bakeshop/wsgi.py +0 -0
  260. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-10/workspace/manage.py +0 -0
  261. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-10/workspace/requirements.txt +0 -0
  262. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-10/workspace/templates/treats/home.html +0 -0
  263. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-10/workspace/treats/__init__.py +0 -0
  264. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-10/workspace/treats/admin.py +0 -0
  265. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-10/workspace/treats/apps.py +0 -0
  266. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-10/workspace/treats/menu.py +0 -0
  267. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-10/workspace/treats/migrations/__init__.py +0 -0
  268. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-10/workspace/treats/models.py +0 -0
  269. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-10/workspace/treats/tests.py +0 -0
  270. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-10/workspace/treats/urls.py +0 -0
  271. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/data/student-10/workspace/treats/views.py +0 -0
  272. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/docker-compose.yaml +0 -0
  273. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/initial_content/README.md +0 -0
  274. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/initial_content/galactic_bakeshop/__init__.py +0 -0
  275. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/initial_content/galactic_bakeshop/asgi.py +0 -0
  276. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/initial_content/galactic_bakeshop/settings.py +0 -0
  277. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/initial_content/galactic_bakeshop/urls.py +0 -0
  278. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/initial_content/galactic_bakeshop/wsgi.py +0 -0
  279. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/initial_content/manage.py +0 -0
  280. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/initial_content/requirements.txt +0 -0
  281. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/initial_content/templates/treats/home.html +0 -0
  282. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/initial_content/treats/__init__.py +0 -0
  283. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/initial_content/treats/admin.py +0 -0
  284. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/initial_content/treats/apps.py +0 -0
  285. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/initial_content/treats/menu.py +0 -0
  286. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/initial_content/treats/migrations/__init__.py +0 -0
  287. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/initial_content/treats/models.py +0 -0
  288. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/initial_content/treats/tests.py +0 -0
  289. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/initial_content/treats/urls.py +0 -0
  290. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/initial_content/treats/views.py +0 -0
  291. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/examples/workshop_fleet/instructions/WELCOME.md +0 -0
  292. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/portacode/README.md +0 -0
  293. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/portacode/__init__.py +0 -0
  294. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/portacode/__main__.py +0 -0
  295. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/portacode/codex_prepare.py +0 -0
  296. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/portacode/codex_usage_limit.py +0 -0
  297. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/portacode/connection/README.md +0 -0
  298. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/portacode/connection/__init__.py +0 -0
  299. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/portacode/connection/handlers/README.md +0 -0
  300. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/portacode/connection/handlers/WEBSOCKET_PROTOCOL.md +0 -0
  301. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/portacode/connection/handlers/automation_v2_handlers.py +0 -0
  302. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/portacode/connection/handlers/base.py +0 -0
  303. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/portacode/connection/handlers/chunked_content.py +0 -0
  304. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/portacode/connection/handlers/cloudflare_forwarding.py +0 -0
  305. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/portacode/connection/handlers/cloudflare_tunnel.py +0 -0
  306. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/portacode/connection/handlers/diff_handlers.py +0 -0
  307. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/portacode/connection/handlers/file_transfer_handlers.py +0 -0
  308. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/portacode/connection/handlers/project_aware_file_handlers.py +0 -0
  309. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/portacode/connection/handlers/project_state/README.md +0 -0
  310. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/portacode/connection/handlers/project_state/__init__.py +0 -0
  311. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/portacode/connection/handlers/project_state/file_system_watcher.py +0 -0
  312. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/portacode/connection/handlers/project_state/git_manager.py +0 -0
  313. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/portacode/connection/handlers/project_state/handlers.py +0 -0
  314. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/portacode/connection/handlers/project_state/manager.py +0 -0
  315. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/portacode/connection/handlers/project_state/models.py +0 -0
  316. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/portacode/connection/handlers/project_state/test_handlers.py +0 -0
  317. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/portacode/connection/handlers/project_state/utils.py +0 -0
  318. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/portacode/connection/handlers/project_state_handlers.py +0 -0
  319. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/portacode/connection/handlers/proxmox_infra.py +0 -0
  320. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/portacode/connection/handlers/registry.py +0 -0
  321. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/portacode/connection/handlers/runtime_user.py +0 -0
  322. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/portacode/connection/handlers/session.py +0 -0
  323. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/portacode/connection/handlers/system_handlers.py +0 -0
  324. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/portacode/connection/handlers/tab_factory.py +0 -0
  325. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/portacode/connection/handlers/terminal_handlers.py +0 -0
  326. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/portacode/connection/handlers/test_cloudflare_forwarding.py +0 -0
  327. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/portacode/connection/handlers/test_proxmox_infra.py +0 -0
  328. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/portacode/connection/handlers/test_runtime_user.py +0 -0
  329. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/portacode/connection/handlers/update_handler.py +0 -0
  330. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/portacode/connection/multiplex.py +0 -0
  331. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/portacode/connection/test_webmin_proxy_config.py +0 -0
  332. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/portacode/connection/webmin_proxy_config.py +0 -0
  333. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/portacode/exit_codes.py +0 -0
  334. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/portacode/keypair.py +0 -0
  335. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/portacode/link_capture/__init__.py +0 -0
  336. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/portacode/link_capture/__pycache__/__init__.cpython-311.pyc +0 -0
  337. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/portacode/link_capture/__pycache__/__init__.cpython-38.pyc +0 -0
  338. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/portacode/link_capture/bin/__pycache__/link_capture_wrapper.cpython-311.pyc +0 -0
  339. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/portacode/link_capture/bin/__pycache__/link_capture_wrapper.cpython-38.pyc +0 -0
  340. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/portacode/link_capture/bin/elinks +0 -0
  341. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/portacode/link_capture/bin/gio-open +0 -0
  342. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/portacode/link_capture/bin/gnome-open +0 -0
  343. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/portacode/link_capture/bin/gvfs-open +0 -0
  344. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/portacode/link_capture/bin/kde-open +0 -0
  345. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/portacode/link_capture/bin/kfmclient +0 -0
  346. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/portacode/link_capture/bin/link_capture_exec.sh +0 -0
  347. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/portacode/link_capture/bin/link_capture_wrapper.py +0 -0
  348. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/portacode/link_capture/bin/links +0 -0
  349. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/portacode/link_capture/bin/links2 +0 -0
  350. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/portacode/link_capture/bin/lynx +0 -0
  351. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/portacode/link_capture/bin/mate-open +0 -0
  352. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/portacode/link_capture/bin/netsurf +0 -0
  353. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/portacode/link_capture/bin/sensible-browser +0 -0
  354. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/portacode/link_capture/bin/w3m +0 -0
  355. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/portacode/link_capture/bin/x-www-browser +0 -0
  356. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/portacode/link_capture/bin/xdg-open +0 -0
  357. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/portacode/logging_categories.py +0 -0
  358. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/portacode/pairing.py +0 -0
  359. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/portacode/restart.py +0 -0
  360. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/portacode/service.py +0 -0
  361. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/portacode/static/js/test-ntp-clock.html +0 -0
  362. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/portacode/static/js/utils/ntp-clock.js +0 -0
  363. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/portacode/test_runtime_paths.py +0 -0
  364. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/portacode/test_service.py +0 -0
  365. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/portacode/test_terminal_exposed_services.py +0 -0
  366. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/portacode/tests/test_codex_prepare.py +0 -0
  367. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/portacode/tunneling/__init__.py +0 -0
  368. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/portacode/tunneling/cloudflared_login.py +0 -0
  369. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/portacode/tunneling/ensure_cloudflared.py +0 -0
  370. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/portacode/tunneling/ensure_pyyaml.py +0 -0
  371. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/portacode/tunneling/forwarding_state.py +0 -0
  372. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/portacode/tunneling/get_domain.py +0 -0
  373. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/portacode/tunneling/privileged.py +0 -0
  374. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/portacode/tunneling/service_install.py +0 -0
  375. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/portacode/tunneling/state.py +0 -0
  376. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/portacode/updater.py +0 -0
  377. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/portacode/utils/NTP_ARCHITECTURE.md +0 -0
  378. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/portacode/utils/__init__.py +0 -0
  379. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/portacode/utils/diff_apply.py +0 -0
  380. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/portacode/utils/diff_renderer.py +0 -0
  381. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/portacode/utils/ntp_clock.py +0 -0
  382. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/portacode/utils/runtime_paths.py +0 -0
  383. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/portacode.egg-info/dependency_links.txt +0 -0
  384. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/portacode.egg-info/requires.txt +0 -0
  385. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/portacode.egg-info/top_level.txt +0 -0
  386. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/portacode_for_school/.gitignore +0 -0
  387. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/portacode_for_school/README.md +0 -0
  388. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/portacode_for_school/persistent_workspace/Dockerfile +0 -0
  389. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/portacode_for_school/persistent_workspace/README.md +0 -0
  390. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/portacode_for_school/persistent_workspace/entrypoint.sh +0 -0
  391. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/portacode_for_school/persistent_workspace/manage_classroom.py +0 -0
  392. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/portacode_for_school/temporary_workspace/Dockerfile +0 -0
  393. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/portacode_for_school/temporary_workspace/README.md +0 -0
  394. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/portacode_for_school/temporary_workspace/docker-compose.yaml +0 -0
  395. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/portacode_for_school/temporary_workspace/docs/device_transfere.png +0 -0
  396. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/portacode_for_school/temporary_workspace/docs/pairing_approval.png +0 -0
  397. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/portacode_for_school/temporary_workspace/entrypoint.sh +0 -0
  398. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/pyproject.toml +0 -0
  399. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/run_tests.py +0 -0
  400. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/scripts/diagnose_codex_gateway.py +0 -0
  401. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/setup.cfg +0 -0
  402. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/test.sh +0 -0
  403. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/test_modules/README.md +0 -0
  404. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/test_modules/__init__.py +0 -0
  405. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/test_modules/test_device_online.py +0 -0
  406. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/test_modules/test_file_operations.py +0 -0
  407. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/test_modules/test_git_status_ui.py +0 -0
  408. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/test_modules/test_login_flow.py +0 -0
  409. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/test_modules/test_navigate_testing_folder.py +0 -0
  410. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/test_modules/test_play_store_screenshots.py +0 -0
  411. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/test_modules/test_terminal_buffer_performance.py +0 -0
  412. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/test_modules/test_terminal_interaction.py +0 -0
  413. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/test_modules/test_terminal_loading_race_condition.py +0 -0
  414. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/test_modules/test_terminal_start.py +0 -0
  415. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/test_request_id.py +0 -0
  416. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/test_session_terminal_shutdown.py +0 -0
  417. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/testing_framework/.env.example +0 -0
  418. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/testing_framework/README.md +0 -0
  419. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/testing_framework/__init__.py +0 -0
  420. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/testing_framework/cli.py +0 -0
  421. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/testing_framework/core/__init__.py +0 -0
  422. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/testing_framework/core/base_test.py +0 -0
  423. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/testing_framework/core/cli_manager.py +0 -0
  424. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/testing_framework/core/hierarchical_runner.py +0 -0
  425. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/testing_framework/core/playwright_manager.py +0 -0
  426. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/testing_framework/core/runner.py +0 -0
  427. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/testing_framework/core/shared_cli_manager.py +0 -0
  428. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/testing_framework/core/test_discovery.py +0 -0
  429. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/testing_framework/requirements.txt +0 -0
  430. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/todo/UI_UX/opening_a_file_on_desktop_results_in_nothing.md +0 -0
  431. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/todo/UI_UX/server_occasionally_stops_communicating_with_all_devices.md +0 -0
  432. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/todo/agent_context_management.md +0 -0
  433. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/todo/django_server_time_sync.md +0 -0
  434. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/todo/issues/device_performance_degradation.md +0 -0
  435. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/todo/issues/git_data_not_captured_in_proxmox.md +0 -0
  436. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/todo/issues/indefinite_resource_loading.md +0 -0
  437. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/todo/issues/portacode_service_silently_down.md +0 -0
  438. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/todo/issues/premature_terminal_exit.md +0 -0
  439. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/todo/issues/project_cpu_hotspots.md +0 -0
  440. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/todo/issues/terminals_exit_upon_starting.md +0 -0
  441. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/todo/issues/websocket_client_silently_dead.md +0 -0
  442. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/todo/issues/wrong_item_classification_on_client_side.md +0 -0
  443. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/todo/smartphone_terminal_input_frustrations.md +0 -0
  444. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/tools/generate_play_store_assets.py +0 -0
  445. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/tools/pairing_tester.py +0 -0
  446. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/tools/run_screenshot_suite.sh +0 -0
  447. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/tools/test_python_ntp_clock.py +0 -0
  448. {portacode-1.5.18.dev0 → portacode-1.5.19.dev1}/validate.sh +0 -0
@@ -10,6 +10,8 @@ client_sessions.json
10
10
  project_state_debug.json
11
11
  test_results
12
12
  logs
13
+ AGENTS.md
14
+ secrets
13
15
 
14
16
  # Example-generated volumes
15
17
  examples/simple_device/data/
@@ -1,6 +1,8 @@
1
1
  # Makefile for Python package release automation
2
2
 
3
- .PHONY: release
3
+ .PHONY: release
4
+
5
+ PUSH_RELEASE_TAG ?= 1
4
6
 
5
7
  release:
6
8
  @echo "Usage: make release VERSION=x.y.z [or just 'make release' for date-based version]"
@@ -15,9 +17,11 @@ else
15
17
  rm -rf dist/*
16
18
  # Ensure we're on a clean state
17
19
  git pull
18
- # Create and push the tag
19
- git tag -f v$(VERSION)
20
- git push -f origin v$(VERSION)
20
+ # Create and push the tag
21
+ git tag -f v$(VERSION)
22
+ ifeq ($(PUSH_RELEASE_TAG),1)
23
+ git push -f origin v$(VERSION)
24
+ endif
21
25
  # Clean any previous builds
22
26
  rm -rf build/ *.egg-info
23
27
  # Build from the tagged state
@@ -26,4 +30,4 @@ else
26
30
  endif
27
31
 
28
32
  # Default target
29
- release: release-real
33
+ release: release-real
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: portacode
3
- Version: 1.5.18.dev0
3
+ Version: 1.5.19.dev1
4
4
  Summary: Portacode CLI client and SDK
5
5
  Home-page: https://github.com/portacode/portacode
6
6
  Author: Meena Erian
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
18
18
  commit_id: str | None
19
19
  __commit_id__: str | None
20
20
 
21
- __version__ = version = '1.5.18.dev0'
22
- __version_tuple__ = version_tuple = (1, 5, 18, 'dev0')
21
+ __version__ = version = '1.5.19.dev1'
22
+ __version_tuple__ = version_tuple = (1, 5, 19, 'dev1')
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -9,13 +9,15 @@ from pathlib import Path
9
9
  import signal
10
10
  import json
11
11
  import socket
12
+ import time
12
13
  from typing import Optional, Tuple
13
14
 
14
15
  import click
15
16
  import pyperclip
16
17
 
17
18
  from . import __version__
18
- from .data import get_pid_file, is_process_running
19
+ from .data import get_gateway_lock_file, get_pid_file, is_process_running
20
+ from .singleton import GatewayLock
19
21
  from .keypair import (
20
22
  get_or_create_keypair,
21
23
  fingerprint_public_key,
@@ -64,6 +66,18 @@ def prepare_codex_command() -> None:
64
66
  )
65
67
 
66
68
 
69
+ @cli.command("github-setup")
70
+ def github_setup_command() -> None:
71
+ """Configure Git to request device-scoped GitHub credentials from Portacode."""
72
+ from .github_credential import configure_git
73
+
74
+ try:
75
+ configure_git()
76
+ except (OSError, subprocess.CalledProcessError) as exc:
77
+ raise click.ClickException(f"Could not configure GitHub access: {exc}") from exc
78
+ click.echo(click.style("GitHub access is configured for this device.", fg="green"))
79
+
80
+
67
81
  @cli.command()
68
82
  @click.option("--gateway", "gateway", "-g", help="Gateway websocket URL (overrides env/ default)")
69
83
  @click.option("--debug", "debug", is_flag=True, help="Enable debug logging")
@@ -164,8 +178,38 @@ def connect(
164
178
  format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
165
179
  )
166
180
 
167
- # 1. Ensure only a single connection per user
181
+ # 1. Ensure only a single connection per host. The lifetime-held lock is
182
+ # atomic; the historical PID file is retained so rolling upgrades can
183
+ # interoperate with older Portacode versions.
168
184
  pid_file = get_pid_file()
185
+ gateway_lock = GatewayLock(get_gateway_lock_file())
186
+ if not gateway_lock.acquire():
187
+ other_pid = gateway_lock.read_pid()
188
+ message = "Another portacode connection is active."
189
+ if other_pid:
190
+ message = f"Another portacode connection (PID {other_pid}) is active."
191
+ click.echo(click.style(message, fg="yellow"))
192
+
193
+ replace = non_interactive
194
+ if not non_interactive:
195
+ replace = click.confirm("Terminate the existing connection?", default=False)
196
+ if not replace:
197
+ click.echo("Aborting.")
198
+ sys.exit(1)
199
+ if not other_pid or not is_process_running(other_pid):
200
+ click.echo(click.style("Unable to identify the lock owner; aborting safely.", fg="red"))
201
+ sys.exit(1)
202
+
203
+ if non_interactive:
204
+ click.echo(click.style("Non-interactive mode: terminating the existing connection automatically.", fg="bright_black"))
205
+ _terminate_process(other_pid)
206
+ deadline = time.monotonic() + 5.0
207
+ while time.monotonic() < deadline and not gateway_lock.acquire():
208
+ time.sleep(0.05)
209
+ if not gateway_lock.held:
210
+ click.echo(click.style("Existing connection did not release the singleton lock; aborting.", fg="red"))
211
+ sys.exit(1)
212
+
169
213
  if pid_file.exists():
170
214
  try:
171
215
  other_pid = int(pid_file.read_text())
@@ -191,13 +235,17 @@ def connect(
191
235
  if non_interactive:
192
236
  click.echo(click.style("Non-interactive mode: terminating the existing connection automatically.", fg="bright_black"))
193
237
  _terminate_process(other_pid)
194
- pid_file.unlink(missing_ok=True)
195
238
  elif click.confirm("Terminate the existing connection?", default=False):
196
239
  _terminate_process(other_pid)
197
- pid_file.unlink(missing_ok=True)
198
240
  else:
199
241
  click.echo("Aborting.")
200
242
  sys.exit(1)
243
+ _wait_for_process_exit(other_pid, timeout=5.0)
244
+ try:
245
+ if int(pid_file.read_text()) == other_pid:
246
+ pid_file.unlink(missing_ok=True)
247
+ except (FileNotFoundError, ValueError, OSError):
248
+ pass
201
249
  else:
202
250
  # Stale pidfile
203
251
  pid_file.unlink(missing_ok=True)
@@ -408,9 +456,11 @@ def connect(
408
456
 
409
457
  if exit_after_setup:
410
458
  click.echo(click.style("Bootstrap steps completed. Exiting without starting a live connection.", fg="green"))
459
+ gateway_lock.release()
411
460
  return
412
461
 
413
462
  pid_file.write_text(str(os.getpid()))
463
+ gateway_lock.write_pid(os.getpid())
414
464
 
415
465
  async def _main() -> None:
416
466
  mgr = ConnectionManager(
@@ -424,7 +474,14 @@ def connect(
424
474
  try:
425
475
  asyncio.run(_main())
426
476
  finally:
427
- pid_file.unlink(missing_ok=True)
477
+ # An older client may have replaced this file while we were shutting
478
+ # down. Never delete a PID file that no longer belongs to us.
479
+ try:
480
+ if int(pid_file.read_text()) == os.getpid():
481
+ pid_file.unlink(missing_ok=True)
482
+ except (FileNotFoundError, ValueError, OSError):
483
+ pass
484
+ gateway_lock.release()
428
485
 
429
486
 
430
487
  def _terminate_process(pid: int):
@@ -442,6 +499,15 @@ def _terminate_process(pid: int):
442
499
  pass
443
500
 
444
501
 
502
+ def _wait_for_process_exit(pid: int, timeout: float) -> bool:
503
+ deadline = time.monotonic() + timeout
504
+ while time.monotonic() < deadline:
505
+ if not is_process_running(pid):
506
+ return True
507
+ time.sleep(0.05)
508
+ return not is_process_running(pid)
509
+
510
+
445
511
  def _is_transient_proxmox_setup_failure(reply: object) -> bool:
446
512
  if not isinstance(reply, dict):
447
513
  return True
@@ -29,13 +29,106 @@ LOGGER = logging.getLogger(__name__)
29
29
  CODEX_LOOPBACK_HOST = "127.0.0.1"
30
30
  CODEX_LOOPBACK_PORT = 61789
31
31
  DEFAULT_GATEWAY_URL = "https://codexapi.portacode.com/v1"
32
- MAX_REQUEST_BYTES = 10 * 1024 * 1024
32
+ # Codex has already serialized local command output by the time it reaches this
33
+ # proxy. The ingress envelope therefore needs to be larger than the payload we
34
+ # are willing to send upstream, otherwise oversized tool output is rejected
35
+ # before ``sanitize_responses_request`` gets a chance to crop it.
36
+ MAX_REQUEST_BYTES = 32 * 1024 * 1024
37
+ MAX_FORWARD_REQUEST_BYTES = 10 * 1024 * 1024
38
+ MAX_TOOL_OUTPUT_BYTES = 64 * 1024
39
+ MAX_TOTAL_TOOL_OUTPUT_BYTES = 256 * 1024
33
40
  ALLOWED_PATHS = {"/health", "/v1/models", "/v1/responses"}
34
41
  MAX_CONCURRENT_UPSTREAM = 8
35
42
 
36
43
  # Streaming read can idle between SSE chunks; connect/write stay bounded.
37
44
  _UPSTREAM_TIMEOUT = httpx.Timeout(connect=20.0, read=600.0, write=60.0, pool=20.0)
38
45
 
46
+ _TOOL_OUTPUT_TYPES = frozenset(
47
+ {
48
+ "function_call_output",
49
+ "computer_call_output",
50
+ "custom_tool_call_output",
51
+ "local_shell_call_output",
52
+ "shell_call_output",
53
+ "command_execution_output",
54
+ }
55
+ )
56
+
57
+
58
+ def _truncate_utf8(value: str, limit: int = MAX_TOOL_OUTPUT_BYTES) -> tuple[str, int]:
59
+ """Keep useful head/tail context while enforcing a UTF-8 byte ceiling."""
60
+ raw = value.encode("utf-8")
61
+ if len(raw) <= limit:
62
+ return value, 0
63
+ if limit <= 0:
64
+ return "", len(raw)
65
+ marker_budget = 160
66
+ content_budget = max(limit - marker_budget, 0)
67
+ head_size = content_budget * 3 // 4
68
+ tail_size = content_budget - head_size
69
+ head = raw[:head_size].decode("utf-8", errors="ignore")
70
+ tail = raw[-tail_size:].decode("utf-8", errors="ignore") if tail_size else ""
71
+ omitted = len(raw) - len(head.encode("utf-8")) - len(tail.encode("utf-8"))
72
+ marker = (
73
+ f"\n\n[Portacode truncated {omitted} bytes of oversized tool output. "
74
+ "Narrow the command or inspect a specific file.]\n\n"
75
+ )
76
+ if len(marker.encode("utf-8")) >= limit:
77
+ return marker.encode("utf-8")[:limit].decode("utf-8", errors="ignore"), len(raw)
78
+ result = f"{head}{marker}{tail}"
79
+ # The dynamic marker can exceed marker_budget by a few bytes.
80
+ while len(result.encode("utf-8")) > limit and head:
81
+ head = head[:-128]
82
+ result = f"{head}{marker}{tail}"
83
+ return result, omitted
84
+
85
+
86
+ def sanitize_responses_request(body: bytes) -> tuple[bytes, int, int]:
87
+ """Crop only recognized Responses API tool-result fields.
88
+
89
+ Unknown JSON shapes pass through untouched so Codex/API upgrades cannot be
90
+ silently corrupted. Returns (body, fields_changed, bytes_omitted).
91
+ """
92
+ try:
93
+ payload = json.loads(body)
94
+ except (json.JSONDecodeError, UnicodeDecodeError):
95
+ return body, 0, 0
96
+ if not isinstance(payload, dict) or not isinstance(payload.get("input"), list):
97
+ return body, 0, 0
98
+
99
+ changed = 0
100
+ omitted_total = 0
101
+ retained_total = 0
102
+ for item in payload["input"]:
103
+ if not isinstance(item, dict) or item.get("type") not in _TOOL_OUTPUT_TYPES:
104
+ continue
105
+ for key in ("output", "content"):
106
+ value = item.get(key)
107
+ if isinstance(value, str):
108
+ remaining = max(MAX_TOTAL_TOOL_OUTPUT_BYTES - retained_total, 0)
109
+ cropped, omitted = _truncate_utf8(value, min(MAX_TOOL_OUTPUT_BYTES, remaining))
110
+ if omitted:
111
+ item[key] = cropped
112
+ changed += 1
113
+ omitted_total += omitted
114
+ retained_total += len(cropped.encode("utf-8"))
115
+ elif isinstance(value, list):
116
+ for part in value:
117
+ if not isinstance(part, dict) or not isinstance(part.get("text"), str):
118
+ continue
119
+ remaining = max(MAX_TOTAL_TOOL_OUTPUT_BYTES - retained_total, 0)
120
+ cropped, omitted = _truncate_utf8(
121
+ part["text"], min(MAX_TOOL_OUTPUT_BYTES, remaining)
122
+ )
123
+ if omitted:
124
+ part["text"] = cropped
125
+ changed += 1
126
+ omitted_total += omitted
127
+ retained_total += len(cropped.encode("utf-8"))
128
+ if not changed:
129
+ return body, 0, 0
130
+ return json.dumps(payload, ensure_ascii=False, separators=(",", ":")).encode("utf-8"), changed, omitted_total
131
+
39
132
  FORWARD_RESPONSE_HEADERS = frozenset(
40
133
  {
41
134
  "content-type",
@@ -366,6 +459,19 @@ class CodexLoopbackProxy:
366
459
  *,
367
460
  req_id: str,
368
461
  ) -> None:
462
+ if method == "POST" and path == "/v1/responses" and body:
463
+ body, cropped_fields, omitted_bytes = sanitize_responses_request(body)
464
+ if cropped_fields:
465
+ LOGGER.warning(
466
+ "codex-proxy[%s] cropped %s oversized tool result field(s), omitting %s bytes",
467
+ req_id,
468
+ cropped_fields,
469
+ omitted_bytes,
470
+ )
471
+ if len(body) > MAX_FORWARD_REQUEST_BYTES:
472
+ raise ValueError(
473
+ "Request body remains too large after local tool-output truncation"
474
+ )
369
475
  headers = self._signature_headers(method, path, body)
370
476
  content_type = request_headers.get("content-type")
371
477
  if content_type:
@@ -213,6 +213,16 @@ class ConnectionManager:
213
213
  else:
214
214
  print("Press Ctrl+C to close the connection.")
215
215
  # Finished authentication flow.
216
+ try:
217
+ from portacode.github_credential import configure_git
218
+
219
+ await asyncio.to_thread(configure_git)
220
+ except Exception as exc:
221
+ logger.warning(
222
+ "Could not configure device-scoped GitHub credentials: %s",
223
+ LogCategory.CONNECTION,
224
+ exc,
225
+ )
216
226
  await self._register_initial_project_paths()
217
227
 
218
228
  async def _start_codex_loopback_proxy(self) -> None:
@@ -14,6 +14,12 @@ from typing import Any, Awaitable, Callable, Dict, List, Optional
14
14
  LOGGER = logging.getLogger(__name__)
15
15
 
16
16
  DEFAULT_CODEX_COMMAND = ["codex", "app-server", "--listen", "stdio://"]
17
+ # JSON-RPC is newline framed and command completion notifications may contain
18
+ # the aggregate output. This ingress envelope must be larger than the UI
19
+ # output limit so the handler can crop the notification instead of readline()
20
+ # dropping it before dispatch. It remains bounded to protect the agent from a
21
+ # runaway app-server process.
22
+ CODEX_STDIO_MESSAGE_LIMIT = 32 * 1024 * 1024
17
23
 
18
24
 
19
25
  class CodexAppServerError(RuntimeError):
@@ -149,6 +155,7 @@ class CodexAppServer:
149
155
  "stdout": asyncio.subprocess.PIPE,
150
156
  "stderr": asyncio.subprocess.PIPE,
151
157
  "env": child_env,
158
+ "limit": CODEX_STDIO_MESSAGE_LIMIT,
152
159
  }
153
160
  if spawn_cwd:
154
161
  spawn_kwargs["cwd"] = spawn_cwd
@@ -292,7 +299,18 @@ class CodexAppServer:
292
299
  assert self._proc is not None and self._proc.stdout is not None
293
300
  try:
294
301
  while True:
295
- line = await self._proc.stdout.readline()
302
+ try:
303
+ line = await self._proc.stdout.readline()
304
+ except ValueError as exc:
305
+ # asyncio converts an over-limit newline frame to
306
+ # ValueError. Drop that one frame but keep consuming future
307
+ # notifications; the app-server process is still healthy.
308
+ LOGGER.error(
309
+ "Dropping Codex app-server JSON-RPC frame larger than %s bytes: %s",
310
+ CODEX_STDIO_MESSAGE_LIMIT,
311
+ exc,
312
+ )
313
+ continue
296
314
  if not line:
297
315
  break
298
316
  line = line.decode("utf-8", errors="replace").strip()
@@ -309,8 +327,8 @@ class CodexAppServer:
309
327
  except Exception:
310
328
  LOGGER.exception("codex app-server read loop failed")
311
329
  finally:
312
- # Process exited; schedule a restart if not closed.
313
- if not self._closed:
330
+ # A reader/protocol failure is not necessarily a process exit.
331
+ if not self._closed and self._proc is not None and self._proc.returncode is not None:
314
332
  asyncio.create_task(self._on_reader_exit())
315
333
 
316
334
  async def _stderr_loop(self) -> None:
@@ -25,6 +25,7 @@ from .file_handlers import (
25
25
  FileCreateHandler,
26
26
  FolderCreateHandler,
27
27
  FileRenameHandler,
28
+ FileMoveCopyHandler,
28
29
  FileSearchHandler,
29
30
  ContentRequestHandler,
30
31
  )
@@ -93,6 +94,7 @@ __all__ = [
93
94
  "FileCreateHandler",
94
95
  "FolderCreateHandler",
95
96
  "FileRenameHandler",
97
+ "FileMoveCopyHandler",
96
98
  "FileSearchHandler",
97
99
  "ContentRequestHandler",
98
100
  "FileApplyDiffHandler",
@@ -3,6 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import asyncio
6
+ import copy
6
7
  import logging
7
8
  import mimetypes
8
9
  from pathlib import Path
@@ -13,6 +14,11 @@ from portacode.connection.codex_app_server import CodexAppServer, CodexAppServer
13
14
  from portacode.connection.handlers.base import AsyncHandler
14
15
 
15
16
  LOGGER = logging.getLogger(__name__)
17
+ MAX_UI_COMMAND_OUTPUT_BYTES = 256 * 1024
18
+ MAX_LIVE_TEXT_BYTES = 512 * 1024
19
+ MAX_LIVE_ITEMS_PER_THREAD = 64
20
+ MAX_LIVE_SNAPSHOT_BYTES = 4 * 1024 * 1024
21
+ MAX_TRACKED_LIVE_THREADS = 64
16
22
 
17
23
  _IMAGE_SUFFIXES = {
18
24
  ".png",
@@ -121,6 +127,9 @@ class CodexChatManager:
121
127
  self._cwd_project: Dict[str, str] = {}
122
128
  self._thread_active_turn: Dict[str, str] = {}
123
129
  self._thread_sessions: Dict[str, set[str]] = {}
130
+ # Materialized in-progress items live with the device agent, not a
131
+ # browser websocket. New/reconnected tabs receive these on resume.
132
+ self._thread_live_items: Dict[str, Dict[str, Dict[str, Any]]] = {}
124
133
  # Survives page reloads via codex_status so the IDE can restore UI.
125
134
  self._prepare_running = False
126
135
  self._prepare_step: Optional[str] = None
@@ -174,8 +183,16 @@ class CodexChatManager:
174
183
  turn_id = params.get("turnId") or turn.get("id")
175
184
  if thread_id and method == "turn/started" and turn_id:
176
185
  self._thread_active_turn[str(thread_id)] = str(turn_id)
186
+ self._thread_live_items[str(thread_id)] = {}
187
+ while len(self._thread_live_items) > MAX_TRACKED_LIVE_THREADS:
188
+ oldest = next(iter(self._thread_live_items))
189
+ if oldest == str(thread_id) and len(self._thread_live_items) > 1:
190
+ oldest = next(key for key in self._thread_live_items if key != str(thread_id))
191
+ self._thread_live_items.pop(oldest, None)
177
192
  elif thread_id and method == "turn/completed":
178
193
  self._thread_active_turn.pop(str(thread_id), None)
194
+ if thread_id:
195
+ params = self._materialize_and_bound_notification(str(thread_id), method, params)
179
196
  project_id = self._resolve_project_id(params)
180
197
  if not project_id:
181
198
  # Unresolved project_id would fan out to every open project tab.
@@ -195,6 +212,98 @@ class CodexChatManager:
195
212
  explicit_sessions=list(self._thread_sessions.get(str(thread_id), set())) if thread_id else None,
196
213
  )
197
214
 
215
+ @staticmethod
216
+ def _bounded_text(value: str, limit: int) -> str:
217
+ raw = value.encode("utf-8")
218
+ if len(raw) <= limit:
219
+ return value
220
+ marker = "\n[Portacode truncated oversized live output.]\n"
221
+ budget = max(limit - len(marker.encode("utf-8")), 0)
222
+ return raw[:budget].decode("utf-8", errors="ignore") + marker
223
+
224
+ def _materialize_and_bound_notification(
225
+ self, thread_id: str, method: str, params: Dict[str, Any]
226
+ ) -> Dict[str, Any]:
227
+ """Bound UI command output and retain a reconnectable live snapshot."""
228
+ params = copy.deepcopy(params)
229
+ raw = params.get("item") if isinstance(params.get("item"), dict) else params
230
+ item_id = raw.get("id") or params.get("itemId") or params.get("id")
231
+ if not item_id or not method.startswith("item/"):
232
+ return params
233
+
234
+ item_id = str(item_id)
235
+ items = self._thread_live_items.setdefault(thread_id, {})
236
+ item = items.setdefault(item_id, {"id": item_id})
237
+ incoming_type = raw.get("type")
238
+ if incoming_type:
239
+ item["type"] = incoming_type
240
+ elif "commandExecution" in method:
241
+ item["type"] = "commandExecution"
242
+ item.setdefault("role", "tool")
243
+ elif "reasoning" in method:
244
+ item["type"] = "reasoning"
245
+ item.setdefault("role", "reasoning")
246
+ elif "agentMessage" in method or "message/delta" in method:
247
+ item["type"] = "agentMessage"
248
+ item.setdefault("role", "assistant")
249
+ for key in ("role", "command", "status"):
250
+ if raw.get(key) is not None:
251
+ item[key] = copy.deepcopy(raw[key])
252
+
253
+ is_command = "commandExecution" in method or item.get("type") == "commandExecution"
254
+ text_key = None
255
+ incoming = ""
256
+ if method == "item/commandExecution/outputDelta":
257
+ text_key, incoming = "output", raw.get("delta") or raw.get("output") or ""
258
+ elif method in {"item/agentMessage/delta", "item/message/delta"}:
259
+ text_key, incoming = "text", raw.get("delta") or raw.get("text") or ""
260
+ elif method in {"item/reasoning/textDelta", "item/reasoning/summaryTextDelta"}:
261
+ text_key, incoming = "reasoning", raw.get("delta") or raw.get("text") or ""
262
+ elif method in {"item/started", "item/completed"}:
263
+ if is_command:
264
+ text_key = "output"
265
+ incoming = raw.get("output") or (raw.get("commandExecution") or {}).get("output") or raw.get("aggregatedOutput") or ""
266
+ else:
267
+ incoming = raw.get("text") or ""
268
+ text_key = "text" if incoming else None
269
+
270
+ if text_key and isinstance(incoming, str):
271
+ limit = MAX_UI_COMMAND_OUTPUT_BYTES if text_key == "output" else MAX_LIVE_TEXT_BYTES
272
+ if method in {"item/started", "item/completed"}:
273
+ combined = incoming
274
+ else:
275
+ combined = str(item.get(text_key) or "") + incoming
276
+ bounded = self._bounded_text(combined, limit)
277
+ item[text_key] = bounded
278
+ # Forward only the portion that fits for deltas; completed items
279
+ # carry a bounded aggregate so the browser cannot restore the blob.
280
+ if method.endswith("/outputDelta") or method.endswith("/delta") or method.endswith("textDelta") or method.endswith("summaryTextDelta"):
281
+ previous = combined[:-len(incoming)] if incoming else combined
282
+ allowed = bounded[len(previous):] if bounded.startswith(previous) else ""
283
+ if "delta" in raw:
284
+ raw["delta"] = allowed
285
+ elif text_key in raw:
286
+ raw[text_key] = allowed
287
+ elif text_key == "output":
288
+ raw["output"] = bounded
289
+ raw.pop("aggregatedOutput", None)
290
+ if isinstance(raw.get("commandExecution"), dict):
291
+ raw["commandExecution"]["output"] = bounded
292
+ while len(items) > MAX_LIVE_ITEMS_PER_THREAD:
293
+ oldest = next(iter(items))
294
+ if oldest == item_id and len(items) > 1:
295
+ oldest = next(key for key in items if key != item_id)
296
+ items.pop(oldest, None)
297
+ while sum(len(str(value).encode("utf-8")) for value in items.values()) > MAX_LIVE_SNAPSHOT_BYTES and len(items) > 1:
298
+ oldest = next(iter(items))
299
+ if oldest == item_id:
300
+ oldest = next(key for key in items if key != item_id)
301
+ items.pop(oldest, None)
302
+ return params
303
+
304
+ def live_items(self, thread_id: str) -> List[Dict[str, Any]]:
305
+ return copy.deepcopy(list(self._thread_live_items.get(str(thread_id), {}).values()))
306
+
198
307
  def _remember_thread_from_params(self, params: Dict[str, Any]) -> None:
199
308
  """Opportunistically map threadId → project when cwd is already known."""
200
309
  if not isinstance(params, dict):
@@ -356,6 +465,7 @@ async def _resume_thread_history(
356
465
  *,
357
466
  thread_id: str,
358
467
  cwd: Optional[str],
468
+ already_active: bool = False,
359
469
  ) -> tuple[Dict[str, Any], list, Optional[Dict[str, Any]]]:
360
470
  """Resume a thread without hydrating the full rollout in one giant payload.
361
471
 
@@ -366,11 +476,12 @@ async def _resume_thread_history(
366
476
  if cwd:
367
477
  resume_params["cwd"] = cwd
368
478
 
369
- resume_result: Dict[str, Any] = {}
479
+ resume_result: Dict[str, Any] = {"thread": {"id": thread_id}}
370
480
  try:
371
- resume_result = await manager.bridge.call(
372
- "thread/resume", resume_params, timeout=120.0
373
- )
481
+ if not already_active:
482
+ resume_result = await manager.bridge.call(
483
+ "thread/resume", resume_params, timeout=120.0
484
+ )
374
485
  except CodexAppServerError as exc:
375
486
  # Older Codex builds may not know excludeTurns — fall back carefully.
376
487
  message = str(exc).lower()
@@ -639,15 +750,23 @@ class CodexThreadResumeHandler(CodexAsyncHandler):
639
750
  manager.subscribe_thread(thread_id, message.get("source_client_session"))
640
751
 
641
752
  resume_result, items, active_turn = await _resume_thread_history(
642
- manager, thread_id=thread_id, cwd=cwd
753
+ manager,
754
+ thread_id=thread_id,
755
+ cwd=cwd,
756
+ already_active=str(thread_id) in manager._thread_active_turn,
643
757
  )
644
758
  thread = _thread_from_result(resume_result)
759
+ if not active_turn:
760
+ known_turn_id = manager._thread_active_turn.get(str(thread_id))
761
+ if known_turn_id:
762
+ active_turn = {"id": known_turn_id, "status": "inProgress"}
645
763
 
646
764
  payload = {
647
765
  "event": "codex_thread_resumed",
648
766
  "threadId": thread_id,
649
767
  "thread": thread,
650
768
  "items": items,
769
+ "liveItems": manager.live_items(thread_id),
651
770
  }
652
771
  if active_turn:
653
772
  payload["activeTurn"] = active_turn
@@ -506,6 +506,60 @@ class FileRenameHandler(SyncHandler):
506
506
  raise RuntimeError(f"Failed to rename: {e}")
507
507
 
508
508
 
509
+ class FileMoveCopyHandler(SyncHandler):
510
+ """Move or copy a file/folder into an existing directory."""
511
+
512
+ @property
513
+ def command_name(self) -> str:
514
+ return "file_move_copy"
515
+
516
+ def execute(self, message: Dict[str, Any]) -> Dict[str, Any]:
517
+ source_raw = message.get("source_path")
518
+ destination_raw = message.get("destination_path")
519
+ operation = message.get("operation", "move")
520
+ if not source_raw or not destination_raw:
521
+ raise ValueError("source_path and destination_path are required")
522
+ if operation not in ("move", "copy"):
523
+ raise ValueError("operation must be 'move' or 'copy'")
524
+
525
+ source = Path(source_raw).resolve()
526
+ destination_dir = Path(destination_raw).resolve()
527
+ if not source.exists():
528
+ raise ValueError(f"Source does not exist: {source_raw}")
529
+ if not destination_dir.is_dir():
530
+ raise ValueError(f"Destination is not a directory: {destination_raw}")
531
+
532
+ target = destination_dir / source.name
533
+ if target.exists():
534
+ raise ValueError(f"An item named '{source.name}' already exists in the destination")
535
+ if source.parent == destination_dir:
536
+ raise ValueError("The item is already in that folder")
537
+ if source.is_dir() and (destination_dir == source or source in destination_dir.parents):
538
+ raise ValueError("A folder cannot be moved or copied into itself")
539
+
540
+ try:
541
+ if operation == "move":
542
+ shutil.move(str(source), str(target))
543
+ elif source.is_dir():
544
+ shutil.copytree(source, target, copy_function=shutil.copy2)
545
+ else:
546
+ shutil.copy2(source, target)
547
+ except PermissionError as exc:
548
+ raise RuntimeError(f"Permission denied: {exc}") from exc
549
+ except OSError as exc:
550
+ raise RuntimeError(f"Failed to {operation}: {exc}") from exc
551
+
552
+ return {
553
+ "event": "file_move_copy_response",
554
+ "source_path": str(source),
555
+ "destination_path": str(destination_dir),
556
+ "target_path": str(target),
557
+ "operation": operation,
558
+ "is_directory": target.is_dir(),
559
+ "success": True,
560
+ }
561
+
562
+
509
563
  class FileSearchHandler(SyncHandler):
510
564
  """Handler for searching text within files under a root directory."""
511
565