pycodex-ws 0.5.1__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 (115) hide show
  1. {pycodex_ws-0.5.1 → pycodex_ws-0.6.0}/PKG-INFO +69 -5
  2. pycodex_ws-0.6.0/README.md +174 -0
  3. {pycodex_ws-0.5.1 → pycodex_ws-0.6.0}/pycodex_router/server.py +15 -15
  4. {pycodex_ws-0.5.1 → 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.1 → pycodex_ws-0.6.0}/pycodex_ws/feishu_card.py +18 -243
  7. pycodex_ws-0.5.1/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.1 → 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.1 → pycodex_ws-0.6.0}/pyproject.toml +2 -2
  14. pycodex_ws-0.6.0/workspace_server/WORKSPACE_API.md +315 -0
  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.5.1 → pycodex_ws-0.6.0}/workspace_server/customization.py +1 -1
  19. pycodex_ws-0.6.0/workspace_server/frontend/content_views.css +176 -0
  20. {pycodex_ws-0.5.1 → pycodex_ws-0.6.0}/workspace_server/frontend/content_views.mjs +103 -6
  21. pycodex_ws-0.6.0/workspace_server/frontend/transcript.mjs +385 -0
  22. pycodex_ws-0.6.0/workspace_server/optional_features/CONTRIBUTING.md +157 -0
  23. pycodex_ws-0.6.0/workspace_server/optional_features/README.md +30 -0
  24. pycodex_ws-0.6.0/workspace_server/optional_features/clock-ticks/README.md +107 -0
  25. pycodex_ws-0.6.0/workspace_server/optional_features/clock-ticks/preview-mobile.png +0 -0
  26. pycodex_ws-0.6.0/workspace_server/optional_features/clock-ticks/preview.html +43 -0
  27. pycodex_ws-0.6.0/workspace_server/optional_features/clock-ticks/preview.png +0 -0
  28. pycodex_ws-0.6.0/workspace_server/optional_features/clock-ticks/source/custom.css +67 -0
  29. pycodex_ws-0.6.0/workspace_server/optional_features/clock-ticks/source/custom.js +102 -0
  30. pycodex_ws-0.6.0/workspace_server/optional_features/clock-ticks/source/fixture.mjs +85 -0
  31. pycodex_ws-0.6.0/workspace_server/optional_features/clock-ticks/source/verify.mjs +82 -0
  32. pycodex_ws-0.6.0/workspace_server/optional_features/consecutive-tool-groups/README.md +118 -0
  33. pycodex_ws-0.6.0/workspace_server/optional_features/consecutive-tool-groups/preview-mobile.png +0 -0
  34. pycodex_ws-0.6.0/workspace_server/optional_features/consecutive-tool-groups/preview.html +48 -0
  35. pycodex_ws-0.6.0/workspace_server/optional_features/consecutive-tool-groups/preview.png +0 -0
  36. pycodex_ws-0.6.0/workspace_server/optional_features/consecutive-tool-groups/source/fixture.mjs +11 -0
  37. pycodex_ws-0.6.0/workspace_server/optional_features/consecutive-tool-groups/source/tool-groups.css +11 -0
  38. pycodex_ws-0.6.0/workspace_server/optional_features/consecutive-tool-groups/source/tool-groups.js +111 -0
  39. pycodex_ws-0.6.0/workspace_server/optional_features/consecutive-tool-groups/source/verify.mjs +61 -0
  40. pycodex_ws-0.6.0/workspace_server/optional_features/exec-status/README.md +85 -0
  41. pycodex_ws-0.6.0/workspace_server/optional_features/exec-status/preview-mobile.png +0 -0
  42. pycodex_ws-0.6.0/workspace_server/optional_features/exec-status/preview.html +33 -0
  43. pycodex_ws-0.6.0/workspace_server/optional_features/exec-status/preview.png +0 -0
  44. pycodex_ws-0.6.0/workspace_server/optional_features/exec-status/source/custom.css +5 -0
  45. pycodex_ws-0.6.0/workspace_server/optional_features/exec-status/source/custom.js +75 -0
  46. pycodex_ws-0.6.0/workspace_server/optional_features/exec-status/source/fixture.mjs +18 -0
  47. pycodex_ws-0.6.0/workspace_server/optional_features/exec-status/source/verify.mjs +80 -0
  48. pycodex_ws-0.6.0/workspace_server/optional_features/reasoning-display/README.md +88 -0
  49. pycodex_ws-0.6.0/workspace_server/optional_features/reasoning-display/preview-mobile.png +0 -0
  50. pycodex_ws-0.6.0/workspace_server/optional_features/reasoning-display/preview.html +34 -0
  51. pycodex_ws-0.6.0/workspace_server/optional_features/reasoning-display/preview.png +0 -0
  52. pycodex_ws-0.6.0/workspace_server/optional_features/reasoning-display/source/custom.css +6 -0
  53. pycodex_ws-0.6.0/workspace_server/optional_features/reasoning-display/source/custom.js +95 -0
  54. pycodex_ws-0.6.0/workspace_server/optional_features/reasoning-display/source/fixture.mjs +26 -0
  55. pycodex_ws-0.6.0/workspace_server/optional_features/reasoning-display/source/verify.mjs +80 -0
  56. pycodex_ws-0.6.0/workspace_server/optional_features/session-tips/README.md +126 -0
  57. pycodex_ws-0.6.0/workspace_server/optional_features/session-tips/preview-mobile.png +0 -0
  58. pycodex_ws-0.6.0/workspace_server/optional_features/session-tips/preview.html +57 -0
  59. pycodex_ws-0.6.0/workspace_server/optional_features/session-tips/preview.png +0 -0
  60. pycodex_ws-0.6.0/workspace_server/optional_features/session-tips/source/custom.css +46 -0
  61. pycodex_ws-0.6.0/workspace_server/optional_features/session-tips/source/custom.js +49 -0
  62. pycodex_ws-0.6.0/workspace_server/optional_features/session-tips/source/fixture.mjs +25 -0
  63. pycodex_ws-0.6.0/workspace_server/optional_features/session-tips/source/session-tips.js +165 -0
  64. pycodex_ws-0.6.0/workspace_server/optional_features/session-tips/source/verify.mjs +71 -0
  65. pycodex_ws-0.6.0/workspace_server/optional_features/tool-calls/README.md +92 -0
  66. pycodex_ws-0.6.0/workspace_server/optional_features/tool-calls/preview-mobile.png +0 -0
  67. pycodex_ws-0.6.0/workspace_server/optional_features/tool-calls/preview.html +34 -0
  68. pycodex_ws-0.6.0/workspace_server/optional_features/tool-calls/preview.png +0 -0
  69. pycodex_ws-0.6.0/workspace_server/optional_features/tool-calls/source/custom.css +6 -0
  70. pycodex_ws-0.6.0/workspace_server/optional_features/tool-calls/source/custom.js +112 -0
  71. pycodex_ws-0.6.0/workspace_server/optional_features/tool-calls/source/fixture.mjs +25 -0
  72. pycodex_ws-0.6.0/workspace_server/optional_features/tool-calls/source/verify.mjs +80 -0
  73. pycodex_ws-0.6.0/workspace_server/routes.py +694 -0
  74. {pycodex_ws-0.5.1 → pycodex_ws-0.6.0}/workspace_server/runtime_info.py +3 -1
  75. pycodex_ws-0.6.0/workspace_server/session_api.py +143 -0
  76. pycodex_ws-0.6.0/workspace_server/session_view.py +669 -0
  77. pycodex_ws-0.6.0/workspace_server/sessions.py +435 -0
  78. pycodex_ws-0.6.0/workspace_server/sharing.py +93 -0
  79. pycodex_ws-0.6.0/workspace_server/view_sources.py +251 -0
  80. {pycodex_ws-0.5.1 → pycodex_ws-0.6.0}/workspace_server/workspace.html +821 -113
  81. pycodex_ws-0.6.0/workspace_server/workspace_views.py +304 -0
  82. {pycodex_ws-0.5.1 → pycodex_ws-0.6.0}/workspace_server/workspaces.py +37 -199
  83. pycodex_ws-0.5.1/README.md +0 -110
  84. pycodex_ws-0.5.1/workspace_server/app.py +0 -2151
  85. pycodex_ws-0.5.1/workspace_server/content_views.css +0 -1
  86. pycodex_ws-0.5.1/workspace_server/content_views.mjs +0 -32
  87. pycodex_ws-0.5.1/workspace_server/frontend/content_views.css +0 -107
  88. pycodex_ws-0.5.1/workspace_server/workspace_views.py +0 -534
  89. {pycodex_ws-0.5.1 → pycodex_ws-0.6.0}/.gitignore +0 -0
  90. {pycodex_ws-0.5.1 → pycodex_ws-0.6.0}/LICENSE +0 -0
  91. {pycodex_ws-0.5.1 → pycodex_ws-0.6.0}/pycodex_router/__init__.py +0 -0
  92. {pycodex_ws-0.5.1 → pycodex_ws-0.6.0}/pycodex_router/__main__.py +0 -0
  93. {pycodex_ws-0.5.1 → pycodex_ws-0.6.0}/pycodex_router/browser.py +0 -0
  94. {pycodex_ws-0.5.1 → pycodex_ws-0.6.0}/pycodex_router/client.py +0 -0
  95. {pycodex_ws-0.5.1 → pycodex_ws-0.6.0}/pycodex_router/index.html +0 -0
  96. {pycodex_ws-0.5.1 → pycodex_ws-0.6.0}/pycodex_router/protocol.py +0 -0
  97. {pycodex_ws-0.5.1 → pycodex_ws-0.6.0}/workspace_server/__init__.py +0 -0
  98. {pycodex_ws-0.5.1 → pycodex_ws-0.6.0}/workspace_server/__main__.py +0 -0
  99. {pycodex_ws-0.5.1 → pycodex_ws-0.6.0}/workspace_server/agent_access.py +0 -0
  100. {pycodex_ws-0.5.1 → pycodex_ws-0.6.0}/workspace_server/content_views.LICENSE.txt +0 -0
  101. {pycodex_ws-0.5.1 → pycodex_ws-0.6.0}/workspace_server/editor.css +0 -0
  102. {pycodex_ws-0.5.1 → pycodex_ws-0.6.0}/workspace_server/editor.mjs +0 -0
  103. {pycodex_ws-0.5.1 → pycodex_ws-0.6.0}/workspace_server/frontend/.gitignore +0 -0
  104. {pycodex_ws-0.5.1 → pycodex_ws-0.6.0}/workspace_server/frontend/build.mjs +0 -0
  105. {pycodex_ws-0.5.1 → pycodex_ws-0.6.0}/workspace_server/frontend/composer.mjs +0 -0
  106. {pycodex_ws-0.5.1 → pycodex_ws-0.6.0}/workspace_server/frontend/editor.css +0 -0
  107. {pycodex_ws-0.5.1 → pycodex_ws-0.6.0}/workspace_server/frontend/editor.mjs +0 -0
  108. {pycodex_ws-0.5.1 → pycodex_ws-0.6.0}/workspace_server/frontend/editor_context.mjs +0 -0
  109. {pycodex_ws-0.5.1 → pycodex_ws-0.6.0}/workspace_server/frontend/package-lock.json +0 -0
  110. {pycodex_ws-0.5.1 → pycodex_ws-0.6.0}/workspace_server/frontend/package.json +0 -0
  111. {pycodex_ws-0.5.1 → pycodex_ws-0.6.0}/workspace_server/frontend/prompt_history.mjs +0 -0
  112. {pycodex_ws-0.5.1 → pycodex_ws-0.6.0}/workspace_server/frontend/touch_selection.mjs +0 -0
  113. {pycodex_ws-0.5.1 → pycodex_ws-0.6.0}/workspace_server/frontend/view_labels.mjs +0 -0
  114. {pycodex_ws-0.5.1 → pycodex_ws-0.6.0}/workspace_server/owned_process.py +0 -0
  115. {pycodex_ws-0.5.1 → 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.1
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.1
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
@@ -52,6 +55,12 @@ running sessions and unread replies. `/cd <directory>` changes the current
52
55
  session's working directory while idle; `/model`, `/resume`, `/compact` and
53
56
  other core session commands are also available.
54
57
 
58
+ Consecutive model reconnection diagnostics share one collapsed `Model retries`
59
+ row per sequence. The summary shows the number of recorded retries, the latest
60
+ attempt and its HTTP status when available. Expand it to read the original errors
61
+ in a scrollable log. New retries update the same row and preserve its open state;
62
+ refreshing starts collapsed. Other model diagnostics and final errors stay visible.
63
+
55
64
  Click Markdown links to open files and comparisons, for example
56
65
  `[source](app.py:42)` or `[changes](diff:HEAD:app.py)`. In a code or diff view,
57
66
  select text and choose **Insert into chat** to add editable context to the draft.
@@ -62,12 +71,22 @@ When using a router, its directory groups workspaces by device and lists unread
62
71
  workspaces at the top. Device handles reorder the list; pencil buttons edit
63
72
  device names.
64
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
+
65
83
  The [VS Code extension](extensions/vscode/README.md) uses this backend and is
66
84
  distributed separately as a VSIX.
67
85
 
68
86
  ## Customize the interface
69
87
 
70
- Click the gear button to start a customization conversation with the Agent.
88
+ Click the gear button and choose **Frontend customization** to start a
89
+ customization conversation with the Agent.
71
90
  Custom files live in the server user's `~/.pycodex/frontend/`:
72
91
 
73
92
  - `custom.css` and `custom.js`: optional styles and an ES module, loaded automatically.
@@ -78,6 +97,43 @@ Refresh to apply changes and keep the selected conversation. Open the workspace
78
97
  with `?builtin=1` to bypass all customizations; click “Re-enable customizations”
79
98
  to return. Changes apply to all workspaces for that OS user.
80
99
  See the [customization contract](../../docs/RUNTIME.md#frontend-customization).
100
+ For custom interfaces, the [resource API](../../docs/WORKSPACE_API.md) exposes
101
+ state, model catalogs, asynchronous command results, paginated history, event
102
+ replay and sub-agent/background-work inventories. A `mount(host)` export in
103
+ `custom.js` can access the built-in page's session, draft and view controls.
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
+
110
+ Ask the Agent to show the [feature catalog](workspace_server/optional_features/README.md)
111
+ in the view area, then choose features by name or ID in chat. The Agent adapts
112
+ their source into your frontend. To share a local change, ask it to publish a
113
+ feature PR on GitHub; the [contribution guide](workspace_server/optional_features/CONTRIBUTING.md)
114
+ specifies the source, documentation, validation and PR format.
115
+
116
+ ## Share a workspace
117
+
118
+ Start the workspace server with an administrator password (`--password`), then
119
+ choose **Settings → Share** in a workspace. Set a different share password and
120
+ send the displayed workspace link and password to the recipient.
121
+
122
+ The existing login page accepts either the administrator password or the share
123
+ password for the workspace being opened. Visitors can use all its conversations,
124
+ Canvas, files, APIs and WebSocket connections, but cannot access workspace
125
+ management, another workspace or sharing settings. The settings button and
126
+ management navigation are hidden for visitors.
127
+
128
+ Passwords, verification values and visitor tokens are kept only in process
129
+ memory. Changing the password or stopping sharing invalidates existing visitor
130
+ logins and disconnects their WebSockets. Restarting the process clears all
131
+ shares. Already accepted Agent work continues.
132
+
133
+ This restricts web access; Agent tools retain their existing file and execution
134
+ permissions. An unprotected server must configure its administrator password
135
+ before enabling sharing. Router authentication is unchanged: a link through a
136
+ router still requires its normal login.
81
137
 
82
138
  ## Storage
83
139
 
@@ -105,6 +161,11 @@ token in `~/.codex/.feishu_refresh_token`. In a workspace session, use
105
161
  `/link <feishu-email|open_id|chat_id>` to connect a card and `/unlink` to disconnect.
106
162
  Other workspace features work without the SDK.
107
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
+
108
169
  ## Development
109
170
 
110
171
  From the repository root:
@@ -116,6 +177,9 @@ uv run --dev pytest packages/pycodex-ws/tests
116
177
  uv build packages/pycodex-ws
117
178
  ```
118
179
 
119
- Python integrations use `pycodex_ws.build_runtime(agent)` to include workspace
120
- 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
121
185
  [test documentation](../../tests/TESTS.md).
@@ -0,0 +1,174 @@
1
+ # pycodex-ws
2
+
3
+ Browser conversations, a live HTML Canvas, read-only file/diff viewers and
4
+ optional Feishu integration for
5
+ [`python-codex`](https://pypi.org/project/python-codex/).
6
+ Requires Python 3.7+ and installs the matching core version.
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
+
11
+ ## Quick start
12
+
13
+ ```bash
14
+ python3 -m pip install pycodex-ws
15
+ ```
16
+
17
+ Create `workspaces.json`:
18
+
19
+ ```json
20
+ {
21
+ "workspaces": [
22
+ {"id": "demo", "work_dir": ".", "state_path": "demo", "enable_board": true}
23
+ ]
24
+ }
25
+ ```
26
+
27
+ ```bash
28
+ pycodex-ws --listen 127.0.0.1:6007 --workspace-config workspaces.json
29
+ ```
30
+
31
+ Open `http://127.0.0.1:6007`. Use `python3 -m workspace_server` if the command
32
+ is not on `PATH`. Keep stdin open while the server runs; EOF initiates shutdown.
33
+ Model configuration comes from `~/.codex/config.toml`.
34
+
35
+ ## Conversations and views
36
+
37
+ The session sidebar starts collapsed. Hover to preview it, click to keep it
38
+ open, and click the toggle or empty space again to close it. Expansion narrows
39
+ the conversation while keeping the board fixed. On phones, use the header
40
+ buttons to switch between chat and board.
41
+
42
+ Use the expanded sidebar to create, rename or close sessions. Status dots show
43
+ running sessions and unread replies. `/cd <directory>` changes the current
44
+ session's working directory while idle; `/model`, `/resume`, `/compact` and
45
+ other core session commands are also available.
46
+
47
+ Consecutive model reconnection diagnostics share one collapsed `Model retries`
48
+ row per sequence. The summary shows the number of recorded retries, the latest
49
+ attempt and its HTTP status when available. Expand it to read the original errors
50
+ in a scrollable log. New retries update the same row and preserve its open state;
51
+ refreshing starts collapsed. Other model diagnostics and final errors stay visible.
52
+
53
+ Click Markdown links to open files and comparisons, for example
54
+ `[source](app.py:42)` or `[changes](diff:HEAD:app.py)`. In a code or diff view,
55
+ select text and choose **Insert into chat** to add editable context to the draft.
56
+ Click a context block to edit or remove it before sending.
57
+ See [view links and supported formats](../../docs/RUNTIME.md#workspace-content-views).
58
+
59
+ When using a router, its directory groups workspaces by device and lists unread
60
+ workspaces at the top. Device handles reorder the list; pencil buttons edit
61
+ device names.
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
+
72
+ The [VS Code extension](extensions/vscode/README.md) uses this backend and is
73
+ distributed separately as a VSIX.
74
+
75
+ ## Customize the interface
76
+
77
+ Click the gear button and choose **Frontend customization** to start a
78
+ customization conversation with the Agent.
79
+ Custom files live in the server user's `~/.pycodex/frontend/`:
80
+
81
+ - `custom.css` and `custom.js`: optional styles and an ES module, loaded automatically.
82
+ - `workspace.html`: optional full-page override; otherwise the packaged page is used.
83
+ - Other files: available at workspace-relative `frontend/<path>` URLs.
84
+
85
+ Refresh to apply changes and keep the selected conversation. Open the workspace
86
+ with `?builtin=1` to bypass all customizations; click “Re-enable customizations”
87
+ to return. Changes apply to all workspaces for that OS user.
88
+ See the [customization contract](../../docs/RUNTIME.md#frontend-customization).
89
+ For custom interfaces, the [resource API](../../docs/WORKSPACE_API.md) exposes
90
+ state, model catalogs, asynchronous command results, paginated history, event
91
+ replay and sub-agent/background-work inventories. A `mount(host)` export in
92
+ `custom.js` can access the built-in page's session, draft and view controls.
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
+
99
+ Ask the Agent to show the [feature catalog](workspace_server/optional_features/README.md)
100
+ in the view area, then choose features by name or ID in chat. The Agent adapts
101
+ their source into your frontend. To share a local change, ask it to publish a
102
+ feature PR on GitHub; the [contribution guide](workspace_server/optional_features/CONTRIBUTING.md)
103
+ specifies the source, documentation, validation and PR format.
104
+
105
+ ## Share a workspace
106
+
107
+ Start the workspace server with an administrator password (`--password`), then
108
+ choose **Settings → Share** in a workspace. Set a different share password and
109
+ send the displayed workspace link and password to the recipient.
110
+
111
+ The existing login page accepts either the administrator password or the share
112
+ password for the workspace being opened. Visitors can use all its conversations,
113
+ Canvas, files, APIs and WebSocket connections, but cannot access workspace
114
+ management, another workspace or sharing settings. The settings button and
115
+ management navigation are hidden for visitors.
116
+
117
+ Passwords, verification values and visitor tokens are kept only in process
118
+ memory. Changing the password or stopping sharing invalidates existing visitor
119
+ logins and disconnects their WebSockets. Restarting the process clears all
120
+ shares. Already accepted Agent work continues.
121
+
122
+ This restricts web access; Agent tools retain their existing file and execution
123
+ permissions. An unprotected server must configure its administrator password
124
+ before enabling sharing. Router authentication is unchanged: a link through a
125
+ router still requires its normal login.
126
+
127
+ ## Storage
128
+
129
+ Paths in `workspaces.json` resolve from the configuration directory.
130
+ `state_path` is a unique filename prefix: the example saves sessions in
131
+ `demo.pycodex-ws.json`, file views in `demo.pycodex-views.json`, and Canvas in
132
+ `demo.html`. Suffixes are appended to the complete prefix, including any dots.
133
+
134
+ Set `enable_board` to `false` to disable Canvas while retaining conversations
135
+ and file views. Session directories and history survive server restarts;
136
+ changing a session's directory does not move Canvas or workspace state files.
137
+ Configuration and restoration details are in the [runtime guide](../../docs/RUNTIME.md).
138
+
139
+ ## Feishu
140
+
141
+ Install the optional SDK in the workspace's Python environment, then authorize:
142
+
143
+ ```bash
144
+ python3 -m pip install lark-oapi
145
+ python3 -m pycodex_ws.feishu_oauth
146
+ ```
147
+
148
+ Authorization reads app credentials from `~/.codex/.env` and stores the refresh
149
+ token in `~/.codex/.feishu_refresh_token`. In a workspace session, use
150
+ `/link <feishu-email|open_id|chat_id>` to connect a card and `/unlink` to disconnect.
151
+ Other workspace features work without the SDK.
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
+
158
+ ## Development
159
+
160
+ From the repository root:
161
+
162
+ ```bash
163
+ uv sync --dev
164
+ uv run pycodex-ws --help
165
+ uv run --dev pytest packages/pycodex-ws/tests
166
+ uv build packages/pycodex-ws
167
+ ```
168
+
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
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))