pycodex-ws 0.5.2__tar.gz → 0.6.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (121) hide show
  1. {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/PKG-INFO +29 -4
  2. {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/README.md +27 -2
  3. {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/pycodex_router/server.py +15 -15
  4. {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/pycodex_ws/__init__.py +2 -5
  5. pycodex_ws-0.6.0/pycodex_ws/feishu_auth.py +30 -0
  6. {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/pycodex_ws/feishu_card.py +18 -243
  7. pycodex_ws-0.5.2/pycodex_ws/feishu_link.py → pycodex_ws-0.6.0/pycodex_ws/feishu_connection.py +15 -166
  8. pycodex_ws-0.6.0/pycodex_ws/feishu_link.py +157 -0
  9. {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/pycodex_ws/feishu_oauth.py +3 -4
  10. pycodex_ws-0.6.0/pycodex_ws/feishu_render.py +215 -0
  11. pycodex_ws-0.6.0/pycodex_ws/server_support.py +299 -0
  12. pycodex_ws-0.6.0/pycodex_ws/workspace_config.py +186 -0
  13. {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/pyproject.toml +2 -2
  14. {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/workspace_server/WORKSPACE_API.md +71 -13
  15. pycodex_ws-0.6.0/workspace_server/app.py +438 -0
  16. pycodex_ws-0.6.0/workspace_server/content_views.css +1 -0
  17. pycodex_ws-0.6.0/workspace_server/content_views.mjs +37 -0
  18. pycodex_ws-0.6.0/workspace_server/frontend/content_views.css +176 -0
  19. {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/workspace_server/frontend/content_views.mjs +69 -6
  20. pycodex_ws-0.6.0/workspace_server/frontend/transcript.mjs +385 -0
  21. {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/workspace_server/optional_features/README.md +4 -1
  22. pycodex_ws-0.6.0/workspace_server/optional_features/clock-ticks/README.md +107 -0
  23. pycodex_ws-0.6.0/workspace_server/optional_features/clock-ticks/preview-mobile.png +0 -0
  24. pycodex_ws-0.6.0/workspace_server/optional_features/clock-ticks/preview.html +43 -0
  25. pycodex_ws-0.6.0/workspace_server/optional_features/clock-ticks/preview.png +0 -0
  26. pycodex_ws-0.6.0/workspace_server/optional_features/clock-ticks/source/custom.css +67 -0
  27. pycodex_ws-0.6.0/workspace_server/optional_features/clock-ticks/source/custom.js +102 -0
  28. pycodex_ws-0.6.0/workspace_server/optional_features/clock-ticks/source/fixture.mjs +85 -0
  29. pycodex_ws-0.6.0/workspace_server/optional_features/clock-ticks/source/verify.mjs +82 -0
  30. pycodex_ws-0.6.0/workspace_server/routes.py +694 -0
  31. {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/workspace_server/runtime_info.py +3 -1
  32. {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/workspace_server/session_api.py +2 -1
  33. pycodex_ws-0.6.0/workspace_server/session_view.py +669 -0
  34. pycodex_ws-0.6.0/workspace_server/sessions.py +435 -0
  35. pycodex_ws-0.6.0/workspace_server/view_sources.py +251 -0
  36. {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/workspace_server/workspace.html +268 -70
  37. {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/workspace_server/workspace_views.py +5 -248
  38. {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/workspace_server/workspaces.py +15 -198
  39. pycodex_ws-0.5.2/workspace_server/app.py +0 -2519
  40. pycodex_ws-0.5.2/workspace_server/content_views.css +0 -1
  41. pycodex_ws-0.5.2/workspace_server/content_views.mjs +0 -32
  42. pycodex_ws-0.5.2/workspace_server/frontend/content_views.css +0 -113
  43. pycodex_ws-0.5.2/workspace_server/optional_features/subagent-status/README.md +0 -87
  44. pycodex_ws-0.5.2/workspace_server/optional_features/subagent-status/preview-mobile.png +0 -0
  45. pycodex_ws-0.5.2/workspace_server/optional_features/subagent-status/preview.html +0 -33
  46. pycodex_ws-0.5.2/workspace_server/optional_features/subagent-status/preview.png +0 -0
  47. pycodex_ws-0.5.2/workspace_server/optional_features/subagent-status/source/custom.css +0 -8
  48. pycodex_ws-0.5.2/workspace_server/optional_features/subagent-status/source/custom.js +0 -92
  49. pycodex_ws-0.5.2/workspace_server/optional_features/subagent-status/source/fixture.mjs +0 -22
  50. pycodex_ws-0.5.2/workspace_server/optional_features/tool-calls/source/verify.mjs +0 -80
  51. {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/.gitignore +0 -0
  52. {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/LICENSE +0 -0
  53. {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/pycodex_router/__init__.py +0 -0
  54. {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/pycodex_router/__main__.py +0 -0
  55. {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/pycodex_router/browser.py +0 -0
  56. {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/pycodex_router/client.py +0 -0
  57. {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/pycodex_router/index.html +0 -0
  58. {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/pycodex_router/protocol.py +0 -0
  59. {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/workspace_server/__init__.py +0 -0
  60. {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/workspace_server/__main__.py +0 -0
  61. {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/workspace_server/agent_access.py +0 -0
  62. {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/workspace_server/content_views.LICENSE.txt +0 -0
  63. {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/workspace_server/customization.py +0 -0
  64. {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/workspace_server/editor.css +0 -0
  65. {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/workspace_server/editor.mjs +0 -0
  66. {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/workspace_server/frontend/.gitignore +0 -0
  67. {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/workspace_server/frontend/build.mjs +0 -0
  68. {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/workspace_server/frontend/composer.mjs +0 -0
  69. {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/workspace_server/frontend/editor.css +0 -0
  70. {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/workspace_server/frontend/editor.mjs +0 -0
  71. {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/workspace_server/frontend/editor_context.mjs +0 -0
  72. {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/workspace_server/frontend/package-lock.json +0 -0
  73. {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/workspace_server/frontend/package.json +0 -0
  74. {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/workspace_server/frontend/prompt_history.mjs +0 -0
  75. {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/workspace_server/frontend/touch_selection.mjs +0 -0
  76. {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/workspace_server/frontend/view_labels.mjs +0 -0
  77. {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/workspace_server/optional_features/CONTRIBUTING.md +0 -0
  78. {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/workspace_server/optional_features/consecutive-tool-groups/README.md +0 -0
  79. {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/workspace_server/optional_features/consecutive-tool-groups/preview-mobile.png +0 -0
  80. {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/workspace_server/optional_features/consecutive-tool-groups/preview.html +0 -0
  81. {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/workspace_server/optional_features/consecutive-tool-groups/preview.png +0 -0
  82. {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/workspace_server/optional_features/consecutive-tool-groups/source/fixture.mjs +0 -0
  83. {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/workspace_server/optional_features/consecutive-tool-groups/source/tool-groups.css +0 -0
  84. {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/workspace_server/optional_features/consecutive-tool-groups/source/tool-groups.js +0 -0
  85. {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/workspace_server/optional_features/consecutive-tool-groups/source/verify.mjs +0 -0
  86. {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/workspace_server/optional_features/exec-status/README.md +0 -0
  87. {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/workspace_server/optional_features/exec-status/preview-mobile.png +0 -0
  88. {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/workspace_server/optional_features/exec-status/preview.html +0 -0
  89. {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/workspace_server/optional_features/exec-status/preview.png +0 -0
  90. {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/workspace_server/optional_features/exec-status/source/custom.css +0 -0
  91. {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/workspace_server/optional_features/exec-status/source/custom.js +0 -0
  92. {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/workspace_server/optional_features/exec-status/source/fixture.mjs +0 -0
  93. {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/workspace_server/optional_features/exec-status/source/verify.mjs +0 -0
  94. {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/workspace_server/optional_features/reasoning-display/README.md +0 -0
  95. {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/workspace_server/optional_features/reasoning-display/preview-mobile.png +0 -0
  96. {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/workspace_server/optional_features/reasoning-display/preview.html +0 -0
  97. {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/workspace_server/optional_features/reasoning-display/preview.png +0 -0
  98. {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/workspace_server/optional_features/reasoning-display/source/custom.css +0 -0
  99. {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/workspace_server/optional_features/reasoning-display/source/custom.js +0 -0
  100. {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/workspace_server/optional_features/reasoning-display/source/fixture.mjs +0 -0
  101. {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/workspace_server/optional_features/reasoning-display/source/verify.mjs +0 -0
  102. {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/workspace_server/optional_features/session-tips/README.md +0 -0
  103. {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/workspace_server/optional_features/session-tips/preview-mobile.png +0 -0
  104. {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/workspace_server/optional_features/session-tips/preview.html +0 -0
  105. {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/workspace_server/optional_features/session-tips/preview.png +0 -0
  106. {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/workspace_server/optional_features/session-tips/source/custom.css +0 -0
  107. {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/workspace_server/optional_features/session-tips/source/custom.js +0 -0
  108. {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/workspace_server/optional_features/session-tips/source/fixture.mjs +0 -0
  109. {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/workspace_server/optional_features/session-tips/source/session-tips.js +0 -0
  110. {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/workspace_server/optional_features/session-tips/source/verify.mjs +0 -0
  111. {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/workspace_server/optional_features/tool-calls/README.md +0 -0
  112. {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/workspace_server/optional_features/tool-calls/preview-mobile.png +0 -0
  113. {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/workspace_server/optional_features/tool-calls/preview.html +0 -0
  114. {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/workspace_server/optional_features/tool-calls/preview.png +0 -0
  115. {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/workspace_server/optional_features/tool-calls/source/custom.css +0 -0
  116. {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/workspace_server/optional_features/tool-calls/source/custom.js +0 -0
  117. {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/workspace_server/optional_features/tool-calls/source/fixture.mjs +0 -0
  118. {pycodex_ws-0.5.2/workspace_server/optional_features/subagent-status → pycodex_ws-0.6.0/workspace_server/optional_features/tool-calls}/source/verify.mjs +0 -0
  119. {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/workspace_server/owned_process.py +0 -0
  120. {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/workspace_server/sharing.py +0 -0
  121. {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/workspace_server/workspaces.html +0 -0
@@ -1,11 +1,11 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pycodex-ws
3
- Version: 0.5.2
3
+ Version: 0.6.0
4
4
  Summary: Browser workspace and optional Feishu integration for python-codex
5
5
  License-File: LICENSE
6
6
  Requires-Python: >=3.7
7
7
  Requires-Dist: pillow>=9.0.0
8
- Requires-Dist: python-codex==0.5.2
8
+ Requires-Dist: python-codex==0.6.0
9
9
  Requires-Dist: websockets<16,>=11
10
10
  Description-Content-Type: text/markdown
11
11
 
@@ -16,6 +16,9 @@ optional Feishu integration for
16
16
  [`python-codex`](https://pypi.org/project/python-codex/).
17
17
  Requires Python 3.7+ and installs the matching core version.
18
18
 
19
+ See the [0.6.0 release notes](https://github.com/Randomizez/pycodex/blob/v0.6.0/docs/RELEASE_0.6.0.md) for changes and
20
+ integration migration from 0.5.0.
21
+
19
22
  ## Quick start
20
23
 
21
24
  ```bash
@@ -68,6 +71,15 @@ When using a router, its directory groups workspaces by device and lists unread
68
71
  workspaces at the top. Device handles reorder the list; pencil buttons edit
69
72
  device names.
70
73
 
74
+ The runtime information shared between Agents recommends plain messages, which
75
+ steer active work at the next safe boundary. Use `/queue` explicitly for an
76
+ independent follow-up task that should wait for current work to finish.
77
+
78
+ The message API also accepts a string `name`, defaulting to `"user"`, for
79
+ identifying the source of a user message. The history API returns it, and
80
+ recorded sessions preserve it across resume. It is metadata only: the model request and frontend display do
81
+ not include it. See the [message API contract](workspace_server/WORKSPACE_API.md#commands-and-results).
82
+
71
83
  The [VS Code extension](extensions/vscode/README.md) uses this backend and is
72
84
  distributed separately as a VSIX.
73
85
 
@@ -90,6 +102,11 @@ state, model catalogs, asynchronous command results, paginated history, event
90
102
  replay and sub-agent/background-work inventories. A `mount(host)` export in
91
103
  `custom.js` can access the built-in page's session, draft and view controls.
92
104
 
105
+ Backend module ownership and the preserved session interfaces are documented in
106
+ the [runtime guide](../../docs/RUNTIME.md#workspace-session-api).
107
+ File/Git source resolution and view storage retain their existing public classes;
108
+ see [content views](../../docs/RUNTIME.md#workspace-content-views).
109
+
93
110
  Ask the Agent to show the [feature catalog](workspace_server/optional_features/README.md)
94
111
  in the view area, then choose features by name or ID in chat. The Agent adapts
95
112
  their source into your frontend. To share a local change, ask it to publish a
@@ -144,6 +161,11 @@ token in `~/.codex/.feishu_refresh_token`. In a workspace session, use
144
161
  `/link <feishu-email|open_id|chat_id>` to connect a card and `/unlink` to disconnect.
145
162
  Other workspace features work without the SDK.
146
163
 
164
+ The adapter separates card rendering, refresh-token storage and SDK connections.
165
+ `PycodexCard` and `PycodexRuntimeLink` keep their existing imports and interfaces;
166
+ see [connection lifecycle](../../docs/RUNTIME.md#feishu-connection-lifecycle)
167
+ and [event presentation](../../docs/RUNTIME.md#observation).
168
+
147
169
  ## Development
148
170
 
149
171
  From the repository root:
@@ -155,6 +177,9 @@ uv run --dev pytest packages/pycodex-ws/tests
155
177
  uv build packages/pycodex-ws
156
178
  ```
157
179
 
158
- Python integrations use `pycodex_ws.build_runtime(agent)` to include workspace
159
- commands. See [runtime contracts](../../docs/RUNTIME.md) and
180
+ Python integrations construct a runtime through `AgentsManager.create_root_runtime()`
181
+ and call `pycodex_ws.register_feishu_commands(runtime)` to include `/link` and
182
+ `/unlink`. The context indicator shows tokens against the auto-compaction limit;
183
+ its expandable Agent tree shows each task's status and latest assistant text.
184
+ See [runtime contracts](../../docs/RUNTIME.md) and
160
185
  [test documentation](../../tests/TESTS.md).
@@ -5,6 +5,9 @@ optional Feishu integration for
5
5
  [`python-codex`](https://pypi.org/project/python-codex/).
6
6
  Requires Python 3.7+ and installs the matching core version.
7
7
 
8
+ See the [0.6.0 release notes](https://github.com/Randomizez/pycodex/blob/v0.6.0/docs/RELEASE_0.6.0.md) for changes and
9
+ integration migration from 0.5.0.
10
+
8
11
  ## Quick start
9
12
 
10
13
  ```bash
@@ -57,6 +60,15 @@ When using a router, its directory groups workspaces by device and lists unread
57
60
  workspaces at the top. Device handles reorder the list; pencil buttons edit
58
61
  device names.
59
62
 
63
+ The runtime information shared between Agents recommends plain messages, which
64
+ steer active work at the next safe boundary. Use `/queue` explicitly for an
65
+ independent follow-up task that should wait for current work to finish.
66
+
67
+ The message API also accepts a string `name`, defaulting to `"user"`, for
68
+ identifying the source of a user message. The history API returns it, and
69
+ recorded sessions preserve it across resume. It is metadata only: the model request and frontend display do
70
+ not include it. See the [message API contract](workspace_server/WORKSPACE_API.md#commands-and-results).
71
+
60
72
  The [VS Code extension](extensions/vscode/README.md) uses this backend and is
61
73
  distributed separately as a VSIX.
62
74
 
@@ -79,6 +91,11 @@ state, model catalogs, asynchronous command results, paginated history, event
79
91
  replay and sub-agent/background-work inventories. A `mount(host)` export in
80
92
  `custom.js` can access the built-in page's session, draft and view controls.
81
93
 
94
+ Backend module ownership and the preserved session interfaces are documented in
95
+ the [runtime guide](../../docs/RUNTIME.md#workspace-session-api).
96
+ File/Git source resolution and view storage retain their existing public classes;
97
+ see [content views](../../docs/RUNTIME.md#workspace-content-views).
98
+
82
99
  Ask the Agent to show the [feature catalog](workspace_server/optional_features/README.md)
83
100
  in the view area, then choose features by name or ID in chat. The Agent adapts
84
101
  their source into your frontend. To share a local change, ask it to publish a
@@ -133,6 +150,11 @@ token in `~/.codex/.feishu_refresh_token`. In a workspace session, use
133
150
  `/link <feishu-email|open_id|chat_id>` to connect a card and `/unlink` to disconnect.
134
151
  Other workspace features work without the SDK.
135
152
 
153
+ The adapter separates card rendering, refresh-token storage and SDK connections.
154
+ `PycodexCard` and `PycodexRuntimeLink` keep their existing imports and interfaces;
155
+ see [connection lifecycle](../../docs/RUNTIME.md#feishu-connection-lifecycle)
156
+ and [event presentation](../../docs/RUNTIME.md#observation).
157
+
136
158
  ## Development
137
159
 
138
160
  From the repository root:
@@ -144,6 +166,9 @@ uv run --dev pytest packages/pycodex-ws/tests
144
166
  uv build packages/pycodex-ws
145
167
  ```
146
168
 
147
- Python integrations use `pycodex_ws.build_runtime(agent)` to include workspace
148
- commands. See [runtime contracts](../../docs/RUNTIME.md) and
169
+ Python integrations construct a runtime through `AgentsManager.create_root_runtime()`
170
+ and call `pycodex_ws.register_feishu_commands(runtime)` to include `/link` and
171
+ `/unlink`. The context indicator shows tokens against the auto-compaction limit;
172
+ its expandable Agent tree shows each task's status and latest assistant text.
173
+ See [runtime contracts](../../docs/RUNTIME.md) and
149
174
  [test documentation](../../tests/TESTS.md).
@@ -15,15 +15,15 @@ from starlette.requests import HTTPConnection
15
15
  from starlette.responses import HTMLResponse, JSONResponse
16
16
  from starlette.routing import Mount, Route, WebSocketRoute
17
17
  from starlette.websockets import WebSocketDisconnect, WebSocketState
18
- from pycodex.portable.client import _base58_encode
18
+ from pycodex.portable.protocol import base58_encode
19
19
  from pycodex.utils.session_persist import resolve_codex_home
20
- from workspace_server.app import (
21
- _auth_cookie_matches,
22
- _install_auth,
23
- _json_object_body,
20
+ from pycodex_ws.server_support import (
21
+ auth_cookie_matches,
22
+ install_auth,
23
+ json_object_body,
24
24
  parse_listen,
25
25
  )
26
- from workspace_server.workspaces import normalize_workspace_id
26
+ from pycodex_ws.workspace_config import normalize_workspace_id
27
27
 
28
28
  from .browser import BrowserAccess, require_login
29
29
  from .protocol import (
@@ -52,7 +52,7 @@ def load_registration_key(path):
52
52
  except FileExistsError:
53
53
  value = path.read_bytes().strip()
54
54
  else:
55
- value = _base58_encode(os.urandom(16)).encode("ascii")
55
+ value = base58_encode(os.urandom(16)).encode("ascii")
56
56
  with os.fdopen(descriptor, "wb") as handle:
57
57
  handle.write(value + b"\n")
58
58
  if not re.fullmatch(b"[1-9A-HJ-NP-Za-km-z]{1,22}", value):
@@ -162,7 +162,7 @@ class Router:
162
162
  self.api_url = normalize_api_url(api_url) if api_url is not None else None
163
163
  self.api_port = api_port
164
164
  self.registration_key = (
165
- _base58_encode(os.urandom(16))
165
+ base58_encode(os.urandom(16))
166
166
  if registration_key is None
167
167
  else registration_key
168
168
  )
@@ -208,11 +208,11 @@ class Router:
208
208
  self.aliases = aliases
209
209
 
210
210
  async def reorder_devices(self, request):
211
- if not self.browser_token or not _auth_cookie_matches(
211
+ if not self.browser_token or not auth_cookie_matches(
212
212
  self.browser_token, request.cookies.get("pycodex_router_auth")
213
213
  ):
214
214
  raise HTTPException(401, "Authentication required.")
215
- payload = await _json_object_body(request, max_bytes=64 * 1024)
215
+ payload = await json_object_body(request, max_bytes=64 * 1024)
216
216
  order = payload.get("order")
217
217
  if (
218
218
  not isinstance(order, list)
@@ -235,14 +235,14 @@ class Router:
235
235
  return JSONResponse({"order": order})
236
236
 
237
237
  async def rename_device(self, request):
238
- if not self.browser_token or not _auth_cookie_matches(
238
+ if not self.browser_token or not auth_cookie_matches(
239
239
  self.browser_token, request.cookies.get("pycodex_router_auth")
240
240
  ):
241
241
  raise HTTPException(401, "Authentication required.")
242
242
  node_id = request.path_params["node_id"]
243
243
  if node_id not in self.nodes:
244
244
  raise HTTPException(404, "Device is not connected.")
245
- payload = await _json_object_body(request)
245
+ payload = await json_object_body(request)
246
246
  try:
247
247
  alias = normalize_alias(payload.get("alias"))
248
248
  except ValueError as exc:
@@ -407,7 +407,7 @@ class Router:
407
407
  agent_access = trusted_agent_request(scope)
408
408
  if blocked or not (
409
409
  agent_access
410
- or _auth_cookie_matches(
410
+ or auth_cookie_matches(
411
411
  self.browser_token, connection.cookies.get("pycodex_router_auth")
412
412
  )
413
413
  ):
@@ -535,7 +535,7 @@ def create_app(
535
535
  )
536
536
 
537
537
  async def registration_key_info(request):
538
- if not router.browser_token or not _auth_cookie_matches(
538
+ if not router.browser_token or not auth_cookie_matches(
539
539
  router.browser_token, request.cookies.get("pycodex_router_auth")
540
540
  ):
541
541
  return JSONResponse({"error": "authentication required"}, status_code=401)
@@ -553,7 +553,7 @@ def create_app(
553
553
  Route("/api/nodes/{node_id}/alias", router.rename_device, methods=["POST"]),
554
554
  ]
555
555
  )
556
- router.browser_token = _install_auth(
556
+ router.browser_token = install_auth(
557
557
  web,
558
558
  password,
559
559
  cookie_name="pycodex_router_auth",
@@ -2,11 +2,9 @@
2
2
 
3
3
  from importlib import import_module
4
4
 
5
- from pycodex.bootstrap import build_runtime as build_core_runtime
6
5
 
7
-
8
- def build_runtime(agent):
9
- runtime = build_core_runtime(agent)
6
+ def register_feishu_commands(runtime):
7
+ """Attach Feishu commands and connection cleanup to an existing runtime."""
10
8
  link = None
11
9
 
12
10
  async def unlink(argument):
@@ -52,4 +50,3 @@ def build_runtime(agent):
52
50
  runtime.register_command("link", connect)
53
51
  runtime.register_command("unlink", unlink)
54
52
  runtime.add_close_handler(close_link)
55
- return runtime
@@ -0,0 +1,30 @@
1
+ """Shared Feishu endpoints and refresh-token storage."""
2
+
3
+ import os
4
+ import typing
5
+ from pathlib import Path
6
+
7
+ FEISHU_API_BASE = "https://open.feishu.cn/open-apis"
8
+ FEISHU_DOMAIN = "https://open.feishu.cn"
9
+ FEISHU_REFRESH_TOKEN_FILE = "~/.codex/.feishu_refresh_token"
10
+
11
+
12
+ def refresh_token_path() -> Path:
13
+ return Path(FEISHU_REFRESH_TOKEN_FILE).expanduser()
14
+
15
+
16
+ def read_refresh_token() -> "typing.Union[str, None]":
17
+ path = refresh_token_path()
18
+ if not path.exists():
19
+ return None
20
+ value = path.read_text(encoding="utf-8", errors="replace").strip()
21
+ return value or None
22
+
23
+
24
+ def write_refresh_token(value: str) -> None:
25
+ path = refresh_token_path()
26
+ path.parent.mkdir(parents=True, exist_ok=True)
27
+ descriptor = os.open(str(path), os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
28
+ with os.fdopen(descriptor, "w", encoding="utf-8") as stream:
29
+ path.chmod(0o600)
30
+ stream.write("{0}\n".format(value))
@@ -2,7 +2,6 @@ import json
2
2
  import os
3
3
  import time
4
4
  import typing
5
- from pathlib import Path
6
5
 
7
6
  import requests
8
7
 
@@ -22,12 +21,21 @@ from pycodex.events import (
22
21
  )
23
22
  from pycodex.utils.event_helpers import completed_history
24
23
 
25
- FEISHU_API_BASE = "https://open.feishu.cn/open-apis"
26
- FEISHU_DOMAIN = "https://open.feishu.cn"
27
- FEISHU_REFRESH_TOKEN_FILE = "~/.codex/.feishu_refresh_token"
28
- CARD_OUTPUT_LIMIT = 6500
29
- CARD_OUTPUT_MODE_MARKDOWN = "markdown"
30
- CARD_OUTPUT_MODE_CODE = "code"
24
+ from .feishu_auth import (
25
+ FEISHU_API_BASE,
26
+ FEISHU_DOMAIN,
27
+ FEISHU_REFRESH_TOKEN_FILE,
28
+ read_refresh_token as _read_refresh_token,
29
+ refresh_token_path as _refresh_token_path,
30
+ write_refresh_token as _write_refresh_token,
31
+ )
32
+ from .feishu_render import (
33
+ CARD_OUTPUT_LIMIT,
34
+ CARD_OUTPUT_MODE_CODE,
35
+ CARD_OUTPUT_MODE_MARKDOWN,
36
+ render_card,
37
+ )
38
+
31
39
  CARD_CONTENT_ERROR_MARKER = "Failed to create card content"
32
40
 
33
41
 
@@ -240,97 +248,7 @@ class PycodexCard:
240
248
  def render(
241
249
  self, output_mode: str = CARD_OUTPUT_MODE_MARKDOWN
242
250
  ) -> "typing.Dict[str, object]":
243
- status = "Closed" if self.display.closed else self.status
244
- input_disabled = not self.accepts_input or self.detached or self.display.closed
245
- output = _truncate(self.output_text, CARD_OUTPUT_LIMIT) or "Ready."
246
- working_output = _truncate(self.working_output, CARD_OUTPUT_LIMIT)
247
- color = _status_color("Detached" if self.detached else status or "Idle")
248
- body_elements = [
249
- _output_box(
250
- "answer_box",
251
- "answer_md",
252
- _render_output_content(output, output_mode),
253
- "grey-50",
254
- ),
255
- ]
256
- if working_output:
257
- body_elements.append(
258
- _output_box(
259
- "working_output_box",
260
- "working_output_md",
261
- _render_output_content(working_output, output_mode),
262
- "green-50",
263
- )
264
- )
265
- if self.activity_text:
266
- body_elements.append(
267
- {
268
- "tag": "markdown",
269
- "element_id": "activity_md",
270
- "content": _render_output_content(
271
- _truncate(self.activity_text, CARD_OUTPUT_LIMIT), output_mode
272
- ),
273
- }
274
- )
275
- if self.user_prompt:
276
- body_elements.insert(
277
- 0,
278
- {
279
- "tag": "markdown",
280
- "element_id": "user_prompt_md",
281
- "content": _render_output_content(
282
- "user> " + _truncate(self.user_prompt, CARD_OUTPUT_LIMIT),
283
- output_mode,
284
- ),
285
- },
286
- )
287
- history = self.earlier_turns()
288
- if history:
289
- index = (
290
- len(history) - 1 if self.history_index is None else self.history_index
291
- )
292
- body_elements.insert(
293
- 0,
294
- _history_panel(
295
- history,
296
- index,
297
- self.history_index is not None,
298
- output_mode,
299
- not self.detached,
300
- ),
301
- )
302
- card = {
303
- "schema": "2.0",
304
- "config": {"update_multi": True},
305
- "header": {
306
- "title": {
307
- "tag": "plain_text",
308
- "content": self.display.title or "pycodex",
309
- },
310
- "template": color,
311
- },
312
- "body": {
313
- "elements": body_elements,
314
- },
315
- }
316
- if not self.detached:
317
- card["body"]["elements"].append(
318
- {
319
- "tag": "input",
320
- "element_id": "prompt_input",
321
- "name": "prompt",
322
- "input_type": "text",
323
- "width": "fill",
324
- "disabled": input_disabled,
325
- "placeholder": {
326
- "tag": "plain_text",
327
- "content": self.prompt_text + (status or self.model_name),
328
- },
329
- "behaviors": [{"type": "callback", "value": {"action": "send"}}],
330
- "value": {"action": "send"},
331
- },
332
- )
333
- return card
251
+ return render_card(self, output_mode)
334
252
 
335
253
  def earlier_turns(self):
336
254
  if self._active_turn_id is not None:
@@ -389,9 +307,7 @@ class PycodexCard:
389
307
  aliases = _user_id_aliases()
390
308
  if normalized in aliases:
391
309
  return aliases[normalized]
392
- if normalized.startswith("ou_"):
393
- return normalized
394
- if normalized.startswith("oc_"):
310
+ if normalized.startswith(("ou_", "oc_")):
395
311
  return normalized
396
312
  if normalized.isnumeric():
397
313
  return self._lookup_user({"mobiles": [normalized]})
@@ -477,7 +393,7 @@ class PycodexCard:
477
393
  use_user_token=use_user_token,
478
394
  )
479
395
  except Exception as exc:
480
- if not _is_card_content_error(exc):
396
+ if CARD_CONTENT_ERROR_MARKER not in str(exc):
481
397
  raise
482
398
  return self._request(
483
399
  method,
@@ -616,126 +532,6 @@ def _display_user_name(user: "typing.Any") -> "typing.Union[str, None]":
616
532
  return None
617
533
 
618
534
 
619
- def _status_color(status: str) -> str:
620
- normalized = status.lower()
621
- if normalized in {"detached", "closed"}:
622
- return "grey"
623
- if normalized in {"idle", "idle: sleeping"}:
624
- return "green"
625
- return "blue"
626
-
627
-
628
- def _truncate(text: str, limit: int) -> str:
629
- value = str(text or "")
630
- if len(value) <= limit:
631
- return value
632
- return value[: max(0, limit - 32)] + "\n...[truncated]"
633
-
634
-
635
- def _escape_code_block(text: str) -> str:
636
- return str(text or "").replace("```", "'''")
637
-
638
-
639
- def _render_output_content(output: str, output_mode: str) -> str:
640
- if output_mode == CARD_OUTPUT_MODE_CODE:
641
- return "```text\n{0}\n```".format(_escape_code_block(output))
642
- return output
643
-
644
-
645
- def _history_panel(history, index, expanded, output_mode, interactive):
646
- prompt, response = history[index]
647
- content = _truncate(
648
- "user> {0}\n\nassistant> {1}".format(prompt, response), CARD_OUTPUT_LIMIT
649
- )
650
- columns = []
651
- for label, target, disabled in (
652
- ("Previous", index - 1, index == 0),
653
- ("Next", index + 1, index == len(history) - 1),
654
- ):
655
- columns.append(
656
- {
657
- "tag": "column",
658
- "width": "auto",
659
- "elements": [
660
- {
661
- "tag": "button",
662
- "text": {"tag": "plain_text", "content": label},
663
- "type": "default",
664
- "disabled": disabled or not interactive,
665
- "behaviors": [
666
- {
667
- "type": "callback",
668
- "value": {
669
- "action": "history",
670
- "history_index": target,
671
- },
672
- }
673
- ],
674
- }
675
- ],
676
- }
677
- )
678
- return {
679
- "tag": "collapsible_panel",
680
- "element_id": "history_panel",
681
- "expanded": expanded,
682
- "header": {
683
- "icon": {
684
- "tag": "standard_icon",
685
- "token": "down-small-ccm_outlined",
686
- "size": "16px 16px",
687
- },
688
- "title": {
689
- "tag": "plain_text",
690
- "content": "History · {0} / {1}".format(index + 1, len(history)),
691
- },
692
- },
693
- "elements": [
694
- {
695
- "tag": "markdown",
696
- "element_id": "history_md",
697
- "content": _render_output_content(content, output_mode),
698
- },
699
- {"tag": "column_set", "columns": columns},
700
- ],
701
- }
702
-
703
-
704
- def _output_box(
705
- box_id: str,
706
- markdown_id: str,
707
- content: str,
708
- background_style: str,
709
- ) -> "typing.Dict[str, object]":
710
- return {
711
- "tag": "column_set",
712
- "element_id": box_id,
713
- "background_style": background_style,
714
- "horizontal_spacing": "8px",
715
- "horizontal_align": "left",
716
- "columns": [
717
- {
718
- "tag": "column",
719
- "width": "auto",
720
- "elements": [
721
- {
722
- "tag": "markdown",
723
- "element_id": markdown_id,
724
- "content": content,
725
- },
726
- ],
727
- "vertical_spacing": "8px",
728
- "horizontal_align": "left",
729
- "vertical_align": "top",
730
- }
731
- ],
732
- }
733
-
734
-
735
- def _is_card_content_error(exc: "BaseException") -> bool:
736
- return CARD_CONTENT_ERROR_MARKER in str(exc)
737
-
738
-
739
535
  def _dig(value: "typing.Any", *path: str) -> "typing.Any":
740
536
  current = value
741
537
  for key in path:
@@ -753,27 +549,6 @@ def _env(*names: str) -> "typing.Union[str, None]":
753
549
  return None
754
550
 
755
551
 
756
- def _refresh_token_path() -> Path:
757
- return Path(FEISHU_REFRESH_TOKEN_FILE).expanduser()
758
-
759
-
760
- def _read_refresh_token() -> "typing.Union[str, None]":
761
- path = _refresh_token_path()
762
- if not path.exists():
763
- return None
764
- value = path.read_text(encoding="utf-8", errors="replace").strip()
765
- return value or None
766
-
767
-
768
- def _write_refresh_token(value: str) -> None:
769
- path = _refresh_token_path()
770
- path.parent.mkdir(parents=True, exist_ok=True)
771
- descriptor = os.open(str(path), os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
772
- with os.fdopen(descriptor, "w", encoding="utf-8") as stream:
773
- path.chmod(0o600)
774
- stream.write("{0}\n".format(value))
775
-
776
-
777
552
  def _api_base_to_domain(api_base: str) -> str:
778
553
  base = str(api_base or FEISHU_API_BASE).rstrip("/")
779
554
  marker = "/open-apis"