codex-telegram 0.5.9__tar.gz → 0.6.1__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.
- {codex_telegram-0.5.9 → codex_telegram-0.6.1}/PKG-INFO +12 -1
- {codex_telegram-0.5.9 → codex_telegram-0.6.1}/README.md +11 -0
- {codex_telegram-0.5.9 → codex_telegram-0.6.1}/TELEGRAM.md +19 -2
- {codex_telegram-0.5.9 → codex_telegram-0.6.1}/WEB.md +48 -13
- {codex_telegram-0.5.9 → codex_telegram-0.6.1}/codex_telegram/__init__.py +1 -1
- {codex_telegram-0.5.9 → codex_telegram-0.6.1}/codex_telegram.egg-info/PKG-INFO +12 -1
- {codex_telegram-0.5.9 → codex_telegram-0.6.1}/codex_telegram.egg-info/SOURCES.txt +4 -2
- codex_telegram-0.6.1/tests/test_project_config.py +42 -0
- codex_telegram-0.6.1/tests/test_project_routes.py +59 -0
- {codex_telegram-0.5.9 → codex_telegram-0.6.1}/tests/test_web_server_local_command.py +44 -0
- {codex_telegram-0.5.9 → codex_telegram-0.6.1}/utils/config.py +101 -1
- {codex_telegram-0.5.9 → codex_telegram-0.6.1}/web/routes.py +59 -12
- codex_telegram-0.6.1/web/static/dist/assets/index-0u8Hjl9v.js +256 -0
- codex_telegram-0.6.1/web/static/dist/assets/index-Bh3KHDwf.css +1 -0
- {codex_telegram-0.5.9 → codex_telegram-0.6.1}/web/static/dist/index.html +2 -2
- codex_telegram-0.5.9/web/static/dist/assets/index-DIOICBNe.js +0 -251
- codex_telegram-0.5.9/web/static/dist/assets/index-DW1AcoLg.css +0 -1
- {codex_telegram-0.5.9 → codex_telegram-0.6.1}/LICENSE +0 -0
- {codex_telegram-0.5.9 → codex_telegram-0.6.1}/MANIFEST.in +0 -0
- {codex_telegram-0.5.9 → codex_telegram-0.6.1}/app_runtime/__init__.py +0 -0
- {codex_telegram-0.5.9 → codex_telegram-0.6.1}/app_runtime/bootstrap.py +0 -0
- {codex_telegram-0.5.9 → codex_telegram-0.6.1}/app_runtime/telegram_app.py +0 -0
- {codex_telegram-0.5.9 → codex_telegram-0.6.1}/app_runtime/web_server.py +0 -0
- {codex_telegram-0.5.9 → codex_telegram-0.6.1}/bot/__init__.py +0 -0
- {codex_telegram-0.5.9 → codex_telegram-0.6.1}/bot/callbacks.py +0 -0
- {codex_telegram-0.5.9 → codex_telegram-0.6.1}/bot/features_ui.py +0 -0
- {codex_telegram-0.5.9 → codex_telegram-0.6.1}/bot/handlers.py +0 -0
- {codex_telegram-0.5.9 → codex_telegram-0.6.1}/bot/keyboard.py +0 -0
- {codex_telegram-0.5.9 → codex_telegram-0.6.1}/bot/projects_ui.py +0 -0
- {codex_telegram-0.5.9 → codex_telegram-0.6.1}/bot/skills_ui.py +0 -0
- {codex_telegram-0.5.9 → codex_telegram-0.6.1}/bot/thread_ui.py +0 -0
- {codex_telegram-0.5.9 → codex_telegram-0.6.1}/codex/__init__.py +0 -0
- {codex_telegram-0.5.9 → codex_telegram-0.6.1}/codex/approval_flow.py +0 -0
- {codex_telegram-0.5.9 → codex_telegram-0.6.1}/codex/approval_guardian.py +0 -0
- {codex_telegram-0.5.9 → codex_telegram-0.6.1}/codex/client.py +0 -0
- {codex_telegram-0.5.9 → codex_telegram-0.6.1}/codex/client_pool.py +0 -0
- {codex_telegram-0.5.9 → codex_telegram-0.6.1}/codex/collaboration_mode.py +0 -0
- {codex_telegram-0.5.9 → codex_telegram-0.6.1}/codex/command_router/__init__.py +0 -0
- {codex_telegram-0.5.9 → codex_telegram-0.6.1}/codex/command_router/common.py +0 -0
- {codex_telegram-0.5.9 → codex_telegram-0.6.1}/codex/command_router/context.py +0 -0
- {codex_telegram-0.5.9 → codex_telegram-0.6.1}/codex/command_router/contracts.py +0 -0
- {codex_telegram-0.5.9 → codex_telegram-0.6.1}/codex/command_router/core.py +0 -0
- {codex_telegram-0.5.9 → codex_telegram-0.6.1}/codex/command_router/projects.py +0 -0
- {codex_telegram-0.5.9 → codex_telegram-0.6.1}/codex/command_router/review.py +0 -0
- {codex_telegram-0.5.9 → codex_telegram-0.6.1}/codex/command_router/system.py +0 -0
- {codex_telegram-0.5.9 → codex_telegram-0.6.1}/codex/command_router/threads.py +0 -0
- {codex_telegram-0.5.9 → codex_telegram-0.6.1}/codex/commands.py +0 -0
- {codex_telegram-0.5.9 → codex_telegram-0.6.1}/codex/event_forwarding.py +0 -0
- {codex_telegram-0.5.9 → codex_telegram-0.6.1}/codex/events.py +0 -0
- {codex_telegram-0.5.9 → codex_telegram-0.6.1}/codex/protocol.py +0 -0
- {codex_telegram-0.5.9 → codex_telegram-0.6.1}/codex_telegram/cli.py +0 -0
- {codex_telegram-0.5.9 → codex_telegram-0.6.1}/codex_telegram.egg-info/dependency_links.txt +0 -0
- {codex_telegram-0.5.9 → codex_telegram-0.6.1}/codex_telegram.egg-info/entry_points.txt +0 -0
- {codex_telegram-0.5.9 → codex_telegram-0.6.1}/codex_telegram.egg-info/requires.txt +0 -0
- {codex_telegram-0.5.9 → codex_telegram-0.6.1}/codex_telegram.egg-info/top_level.txt +0 -0
- {codex_telegram-0.5.9 → codex_telegram-0.6.1}/conf.toml.example +0 -0
- {codex_telegram-0.5.9 → codex_telegram-0.6.1}/main.py +0 -0
- {codex_telegram-0.5.9 → codex_telegram-0.6.1}/models/__init__.py +0 -0
- {codex_telegram-0.5.9 → codex_telegram-0.6.1}/models/state.py +0 -0
- {codex_telegram-0.5.9 → codex_telegram-0.6.1}/models/telegram_bridge.py +0 -0
- {codex_telegram-0.5.9 → codex_telegram-0.6.1}/models/thread.py +0 -0
- {codex_telegram-0.5.9 → codex_telegram-0.6.1}/models/user.py +0 -0
- {codex_telegram-0.5.9 → codex_telegram-0.6.1}/pyproject.toml +0 -0
- {codex_telegram-0.5.9 → codex_telegram-0.6.1}/setup.cfg +0 -0
- {codex_telegram-0.5.9 → codex_telegram-0.6.1}/tests/test_approval_guardian.py +0 -0
- {codex_telegram-0.5.9 → codex_telegram-0.6.1}/tests/test_approval_policy.py +0 -0
- {codex_telegram-0.5.9 → codex_telegram-0.6.1}/tests/test_bootstrap_update_check.py +0 -0
- {codex_telegram-0.5.9 → codex_telegram-0.6.1}/tests/test_callbacks_results.py +0 -0
- {codex_telegram-0.5.9 → codex_telegram-0.6.1}/tests/test_cli_entrypoint.py +0 -0
- {codex_telegram-0.5.9 → codex_telegram-0.6.1}/tests/test_codex_client.py +0 -0
- {codex_telegram-0.5.9 → codex_telegram-0.6.1}/tests/test_codex_client_pool.py +0 -0
- {codex_telegram-0.5.9 → codex_telegram-0.6.1}/tests/test_command_router.py +0 -0
- {codex_telegram-0.5.9 → codex_telegram-0.6.1}/tests/test_guardian_config.py +0 -0
- {codex_telegram-0.5.9 → codex_telegram-0.6.1}/tests/test_handlers_results.py +0 -0
- {codex_telegram-0.5.9 → codex_telegram-0.6.1}/tests/test_keyboard.py +0 -0
- {codex_telegram-0.5.9 → codex_telegram-0.6.1}/tests/test_main_approval_flow.py +0 -0
- {codex_telegram-0.5.9 → codex_telegram-0.6.1}/tests/test_normalize.py +0 -0
- {codex_telegram-0.5.9 → codex_telegram-0.6.1}/tests/test_pypi_check.py +0 -0
- {codex_telegram-0.5.9 → codex_telegram-0.6.1}/tests/test_single_instance.py +0 -0
- {codex_telegram-0.5.9 → codex_telegram-0.6.1}/tests/test_telegram_bot_config.py +0 -0
- {codex_telegram-0.5.9 → codex_telegram-0.6.1}/tests/test_thread_history.py +0 -0
- {codex_telegram-0.5.9 → codex_telegram-0.6.1}/tests/test_web_runtime.py +0 -0
- {codex_telegram-0.5.9 → codex_telegram-0.6.1}/utils/__init__.py +0 -0
- {codex_telegram-0.5.9 → codex_telegram-0.6.1}/utils/approval_policy.py +0 -0
- {codex_telegram-0.5.9 → codex_telegram-0.6.1}/utils/local_command.py +0 -0
- {codex_telegram-0.5.9 → codex_telegram-0.6.1}/utils/logger.py +0 -0
- {codex_telegram-0.5.9 → codex_telegram-0.6.1}/utils/normalize.py +0 -0
- {codex_telegram-0.5.9 → codex_telegram-0.6.1}/utils/pypi_check.py +0 -0
- {codex_telegram-0.5.9 → codex_telegram-0.6.1}/utils/single_instance.py +0 -0
- {codex_telegram-0.5.9 → codex_telegram-0.6.1}/utils/web_config.py +0 -0
- {codex_telegram-0.5.9 → codex_telegram-0.6.1}/web/__init__.py +0 -0
- {codex_telegram-0.5.9 → codex_telegram-0.6.1}/web/dependencies.py +0 -0
- {codex_telegram-0.5.9 → codex_telegram-0.6.1}/web/runtime.py +0 -0
- {codex_telegram-0.5.9 → codex_telegram-0.6.1}/web/server.py +0 -0
- {codex_telegram-0.5.9 → codex_telegram-0.6.1}/web/static/dist/assets/codex-telegram-icon-black.svg +0 -0
- {codex_telegram-0.5.9 → codex_telegram-0.6.1}/web/static/dist/assets/codex-telegram-icon-ivory.svg +0 -0
- {codex_telegram-0.5.9 → codex_telegram-0.6.1}/web/static/dist/assets/codex-telegram-login-image.png +0 -0
- {codex_telegram-0.5.9 → codex_telegram-0.6.1}/web/static/dist/favicon.ico +0 -0
- {codex_telegram-0.5.9 → codex_telegram-0.6.1}/web/telegram_sync.py +0 -0
- {codex_telegram-0.5.9 → codex_telegram-0.6.1}/web/telegram_thread_selection.py +0 -0
- {codex_telegram-0.5.9 → codex_telegram-0.6.1}/web/thread_history.py +0 -0
- {codex_telegram-0.5.9 → codex_telegram-0.6.1}/web/workspace.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codex-telegram
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.1
|
|
4
4
|
Summary: Bridge for controlling the Codex App Server from Telegram and Web UI
|
|
5
5
|
Author: heungtae
|
|
6
6
|
License: Apache License
|
|
@@ -382,6 +382,17 @@ cp conf.toml.example ~/.config/codex-telegram/conf.toml
|
|
|
382
382
|
./run_build_and_main.sh
|
|
383
383
|
```
|
|
384
384
|
|
|
385
|
+
## Publishing a Release
|
|
386
|
+
|
|
387
|
+
1. Update `codex_telegram/__init__.py` (`__version__`), validate the changes, and push the commits.
|
|
388
|
+
2. In GitHub Releases, create a draft with a matching tag (for example, `v0.6.0` for version `0.6.0`) targeting the intended commit. The commit must contain `.github/workflows/pypi-publish.yml` with the release trigger.
|
|
389
|
+
3. Write the release notes and publish the Release. Drafts and tag pushes alone do not deploy.
|
|
390
|
+
4. Wait for **Publish release to PyPI** to succeed: it builds the Web UI, builds/checks the wheel and sdist (including UI assets), publishes to PyPI, then attaches the same distributions and `SHA256SUMS` to the Release.
|
|
391
|
+
|
|
392
|
+
Configure `PYPI_API_TOKEN` as a repository secret or a secret on the `pypi` environment before publishing. Existing environment protection rules still apply; remove required reviewers from that environment if fully automatic publishing is desired. Published prereleases also trigger this workflow, so use a matching Python prerelease version such as `0.6.1rc1` / `v0.6.1rc1`.
|
|
393
|
+
|
|
394
|
+
A published GitHub Release alone does not mean PyPI deployment succeeded; check the workflow result. If attachment fails after PyPI publishing succeeds, rerun only failed jobs to attach the retained files without republishing. For code changes after a successful PyPI upload, use a new version and Release.
|
|
395
|
+
|
|
385
396
|
## Documentation
|
|
386
397
|
|
|
387
398
|
| File | Description |
|
|
@@ -157,6 +157,17 @@ cp conf.toml.example ~/.config/codex-telegram/conf.toml
|
|
|
157
157
|
./run_build_and_main.sh
|
|
158
158
|
```
|
|
159
159
|
|
|
160
|
+
## Publishing a Release
|
|
161
|
+
|
|
162
|
+
1. Update `codex_telegram/__init__.py` (`__version__`), validate the changes, and push the commits.
|
|
163
|
+
2. In GitHub Releases, create a draft with a matching tag (for example, `v0.6.0` for version `0.6.0`) targeting the intended commit. The commit must contain `.github/workflows/pypi-publish.yml` with the release trigger.
|
|
164
|
+
3. Write the release notes and publish the Release. Drafts and tag pushes alone do not deploy.
|
|
165
|
+
4. Wait for **Publish release to PyPI** to succeed: it builds the Web UI, builds/checks the wheel and sdist (including UI assets), publishes to PyPI, then attaches the same distributions and `SHA256SUMS` to the Release.
|
|
166
|
+
|
|
167
|
+
Configure `PYPI_API_TOKEN` as a repository secret or a secret on the `pypi` environment before publishing. Existing environment protection rules still apply; remove required reviewers from that environment if fully automatic publishing is desired. Published prereleases also trigger this workflow, so use a matching Python prerelease version such as `0.6.1rc1` / `v0.6.1rc1`.
|
|
168
|
+
|
|
169
|
+
A published GitHub Release alone does not mean PyPI deployment succeeded; check the workflow result. If attachment fails after PyPI publishing succeeds, rerun only failed jobs to attach the retained files without republishing. For code changes after a successful PyPI upload, use a new version and Release.
|
|
170
|
+
|
|
160
171
|
## Documentation
|
|
161
172
|
|
|
162
173
|
| File | Description |
|
|
@@ -24,14 +24,17 @@ Execute Codex commands using slash commands in any chat with the bot:
|
|
|
24
24
|
| `/compact <id>` | thread/compact/start | Compact conversation history |
|
|
25
25
|
| `/rollback <n>` | thread/rollback | Roll back last N turns |
|
|
26
26
|
| `/interrupt` | turn/interrupt | Interrupt running turn |
|
|
27
|
-
| `/review` | review/start | Start code review |
|
|
27
|
+
| `/review [uncommittedChanges\|baseBranch\|commit\|custom]` | review/start | Start code review |
|
|
28
28
|
| `/exec <cmd>` | command/exec | Run command |
|
|
29
29
|
| `/models` | model/list | List available models |
|
|
30
30
|
| `/features` | experimentalFeature/list + command/exec | Show beta features |
|
|
31
31
|
| `/guardian` | local config | Show guardian summary |
|
|
32
32
|
| `/modes` | collaborationMode/list | List collaboration modes |
|
|
33
33
|
| `/collab` | collaborationMode/list | List collaboration modes |
|
|
34
|
-
| `/
|
|
34
|
+
| `/mode [toggle\|plan\|build]` | - | Show or set collaboration mode |
|
|
35
|
+
| `/plan` | - | Switch collaboration mode to plan |
|
|
36
|
+
| `/build` | - | Switch collaboration mode to build (default) |
|
|
37
|
+
| `/skills [cwd]` | skills/list | List skills |
|
|
35
38
|
| `/apps` | app/list | List apps |
|
|
36
39
|
| `/mcp` | mcpServerStatus/list | List MCP servers |
|
|
37
40
|
| `/config` | config/read | Read configuration |
|
|
@@ -58,6 +61,7 @@ Configure which events to receive via `telegram.forwarding.app_server_event_allo
|
|
|
58
61
|
- Archive old threads to keep your list clean
|
|
59
62
|
- Compact conversation history to save context
|
|
60
63
|
- Rollback turns to undo mistakes
|
|
64
|
+
- Start code reviews with `/review [uncommittedChanges|baseBranch|commit|custom]`
|
|
61
65
|
|
|
62
66
|
### Project Switching
|
|
63
67
|
|
|
@@ -68,6 +72,19 @@ Manage multiple projects:
|
|
|
68
72
|
3. Use `/project <name>` to switch
|
|
69
73
|
4. Each project maintains separate thread history
|
|
70
74
|
|
|
75
|
+
### Collaboration Mode
|
|
76
|
+
|
|
77
|
+
Control how Codex approaches tasks:
|
|
78
|
+
|
|
79
|
+
| Command | Description |
|
|
80
|
+
|---------|-------------|
|
|
81
|
+
| `/mode` | Show current mode or set to `toggle`, `plan`, or `build` |
|
|
82
|
+
| `/plan` | Switch to plan mode (analyze, don't execute) |
|
|
83
|
+
| `/build` | Switch to build mode (execute changes, default) |
|
|
84
|
+
| `/modes` / `/collab` | List available collaboration modes |
|
|
85
|
+
|
|
86
|
+
Modes are session-local and persist per user. The current mode is shown in the welcome message and when switching projects.
|
|
87
|
+
|
|
71
88
|
## Configuration
|
|
72
89
|
|
|
73
90
|
### Basic Setup
|
|
@@ -20,6 +20,7 @@ A ChatGPT-style web interface for interacting with Codex App Server. Provides re
|
|
|
20
20
|
- **Thread tabs**: Multiple conversations per project, each with independent chat state
|
|
21
21
|
- **Thread states**: Visual indicators for idle, running, completed, failed, or cancelled
|
|
22
22
|
- **Unread notifications**: Badge indicators for new completions
|
|
23
|
+
- **Project management**: Use the project menu → Modify Project to edit its name/path. The key remains fixed to preserve chat associations. Add/Modify Project includes a Default project checkbox. The default project is marked D and its Remove menu is disabled; choose another default before removing it. Changing the default updates the shared configuration without switching existing open chats.
|
|
23
24
|
- **Persistent state**: Thread and project tabs preserved across sessions
|
|
24
25
|
|
|
25
26
|
### Workspace Browser
|
|
@@ -47,6 +48,7 @@ The Web UI receives live updates via SSE for:
|
|
|
47
48
|
| `approval_required` | Guardian/approval needed |
|
|
48
49
|
| `file_change` | File modifications with diffs |
|
|
49
50
|
| `app_event` | Tool execution events |
|
|
51
|
+
| `user_message` | User message sent |
|
|
50
52
|
|
|
51
53
|
### Authentication
|
|
52
54
|
|
|
@@ -96,29 +98,39 @@ Guardian provides automated safety checks before approval decisions:
|
|
|
96
98
|
|
|
97
99
|
| Shortcut | Action |
|
|
98
100
|
|----------|--------|
|
|
99
|
-
| `Ctrl/Cmd + Enter` | Send message |
|
|
101
|
+
| `Ctrl/Cmd + Enter` | Send message (or Shift+Enter for newline) |
|
|
100
102
|
| `Tab` | Toggle Build/Plan mode |
|
|
103
|
+
| `Escape` | Close modals / cancel project picker |
|
|
101
104
|
|
|
102
|
-
#### Alt Key Shortcuts
|
|
105
|
+
#### Alt Key Shortcuts (when not in input)
|
|
103
106
|
|
|
104
107
|
| Shortcut | Action |
|
|
105
108
|
|----------|--------|
|
|
106
|
-
| `Alt + N` | New chat |
|
|
107
|
-
| `Alt + T` | New thread tab |
|
|
108
|
-
| `Alt + W` | Close current thread |
|
|
109
|
-
| `Alt + P` | Open project picker |
|
|
110
|
-
| `Alt + [` | Previous thread |
|
|
111
|
-
| `Alt + ]` | Next thread |
|
|
112
|
-
| `Alt + 1-9` | Switch to thread 1-9 |
|
|
113
|
-
| `Alt + V` | Toggle workspace panel |
|
|
109
|
+
| `Alt + N` | New chat (focus composer, start thread) |
|
|
110
|
+
| `Alt + T` | New thread tab (start thread) |
|
|
111
|
+
| `Alt + W` | Close current thread tab |
|
|
112
|
+
| `Alt + P` | Open project picker modal |
|
|
113
|
+
| `Alt + [` | Previous thread in project |
|
|
114
|
+
| `Alt + ]` | Next thread in project |
|
|
115
|
+
| `Alt + 1-9` | Switch to thread 1-9 in current project |
|
|
116
|
+
| `Alt + V` | Toggle workspace panel (compact layout) |
|
|
114
117
|
|
|
115
|
-
####
|
|
118
|
+
#### Ctrl/Cmd Key Shortcuts (when not in input)
|
|
116
119
|
|
|
117
120
|
| Shortcut | Action |
|
|
118
121
|
|----------|--------|
|
|
119
122
|
| `Ctrl/Cmd + B` | Switch to Build mode |
|
|
120
123
|
| `Ctrl/Cmd + Shift + P` | Switch to Plan mode |
|
|
121
124
|
|
|
125
|
+
#### Project Picker Modal
|
|
126
|
+
|
|
127
|
+
| Shortcut | Action |
|
|
128
|
+
|----------|--------|
|
|
129
|
+
| `Up/Down` | Navigate projects |
|
|
130
|
+
| `Enter` | Select project |
|
|
131
|
+
| `Esc` / `Backspace` | Close modal |
|
|
132
|
+
| `Type` | Filter projects by name/key |
|
|
133
|
+
|
|
122
134
|
## API Endpoints
|
|
123
135
|
|
|
124
136
|
### Authentication
|
|
@@ -151,9 +163,12 @@ Guardian provides automated safety checks before approval decisions:
|
|
|
151
163
|
| Method | Endpoint | Description |
|
|
152
164
|
|--------|----------|-------------|
|
|
153
165
|
| GET | `/api/projects` | List projects |
|
|
154
|
-
| POST | `/api/projects` | Add project |
|
|
166
|
+
| POST | `/api/projects` | Add project (`default: true` optionally sets the default) |
|
|
167
|
+
| PUT | `/api/projects/{key}` | Modify name/path and optionally set default |
|
|
168
|
+
| DELETE | `/api/projects/{key}` | Remove a non-default project |
|
|
155
169
|
| POST | `/api/projects/select` | Switch project |
|
|
156
170
|
| POST | `/api/projects/open-thread` | Open thread in project |
|
|
171
|
+
| POST | `/api/projects/open-explorer` | Open project folder in OS file explorer |
|
|
157
172
|
|
|
158
173
|
### Workspace
|
|
159
174
|
|
|
@@ -172,7 +187,8 @@ Guardian provides automated safety checks before approval decisions:
|
|
|
172
187
|
| GET | `/api/features` | List features |
|
|
173
188
|
| POST | `/api/features/{key}` | Toggle feature |
|
|
174
189
|
| GET | `/api/models` | List models |
|
|
175
|
-
| GET | `/api/modes` | List modes |
|
|
190
|
+
| GET | `/api/modes` | List collaboration modes |
|
|
191
|
+
| GET | `/api/collab` | List collaboration modes (alias) |
|
|
176
192
|
| GET | `/api/skills` | List skills |
|
|
177
193
|
| GET | `/api/apps` | List apps |
|
|
178
194
|
| GET | `/api/mcp` | List MCP servers |
|
|
@@ -181,6 +197,25 @@ Guardian provides automated safety checks before approval decisions:
|
|
|
181
197
|
| GET | `/api/config` | Get config |
|
|
182
198
|
| POST | `/api/approvals/{id}` | Submit approval |
|
|
183
199
|
|
|
200
|
+
### Approvals
|
|
201
|
+
|
|
202
|
+
| Method | Endpoint | Description |
|
|
203
|
+
|--------|----------|-------------|
|
|
204
|
+
| GET | `/api/approvals` | List pending approval requests |
|
|
205
|
+
| POST | `/api/approvals/{request_id}` | Submit approval decision (approve|session|deny) |
|
|
206
|
+
|
|
207
|
+
### Session
|
|
208
|
+
|
|
209
|
+
| Method | Endpoint | Description |
|
|
210
|
+
|--------|----------|-------------|
|
|
211
|
+
| GET | `/api/session/summary` | Get current session state (active thread, workspace, project, mode, agents) |
|
|
212
|
+
|
|
213
|
+
### Commands
|
|
214
|
+
|
|
215
|
+
| Method | Endpoint | Description |
|
|
216
|
+
|--------|----------|-------------|
|
|
217
|
+
| POST | `/api/command` | Execute any slash command |
|
|
218
|
+
|
|
184
219
|
### Events
|
|
185
220
|
|
|
186
221
|
| Method | Endpoint | Description |
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codex-telegram
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.1
|
|
4
4
|
Summary: Bridge for controlling the Codex App Server from Telegram and Web UI
|
|
5
5
|
Author: heungtae
|
|
6
6
|
License: Apache License
|
|
@@ -382,6 +382,17 @@ cp conf.toml.example ~/.config/codex-telegram/conf.toml
|
|
|
382
382
|
./run_build_and_main.sh
|
|
383
383
|
```
|
|
384
384
|
|
|
385
|
+
## Publishing a Release
|
|
386
|
+
|
|
387
|
+
1. Update `codex_telegram/__init__.py` (`__version__`), validate the changes, and push the commits.
|
|
388
|
+
2. In GitHub Releases, create a draft with a matching tag (for example, `v0.6.0` for version `0.6.0`) targeting the intended commit. The commit must contain `.github/workflows/pypi-publish.yml` with the release trigger.
|
|
389
|
+
3. Write the release notes and publish the Release. Drafts and tag pushes alone do not deploy.
|
|
390
|
+
4. Wait for **Publish release to PyPI** to succeed: it builds the Web UI, builds/checks the wheel and sdist (including UI assets), publishes to PyPI, then attaches the same distributions and `SHA256SUMS` to the Release.
|
|
391
|
+
|
|
392
|
+
Configure `PYPI_API_TOKEN` as a repository secret or a secret on the `pypi` environment before publishing. Existing environment protection rules still apply; remove required reviewers from that environment if fully automatic publishing is desired. Published prereleases also trigger this workflow, so use a matching Python prerelease version such as `0.6.1rc1` / `v0.6.1rc1`.
|
|
393
|
+
|
|
394
|
+
A published GitHub Release alone does not mean PyPI deployment succeeded; check the workflow result. If attachment fails after PyPI publishing succeeds, rerun only failed jobs to attach the retained files without republishing. For code changes after a successful PyPI upload, use a new version and Release.
|
|
395
|
+
|
|
385
396
|
## Documentation
|
|
386
397
|
|
|
387
398
|
| File | Description |
|
|
@@ -63,6 +63,8 @@ tests/test_handlers_results.py
|
|
|
63
63
|
tests/test_keyboard.py
|
|
64
64
|
tests/test_main_approval_flow.py
|
|
65
65
|
tests/test_normalize.py
|
|
66
|
+
tests/test_project_config.py
|
|
67
|
+
tests/test_project_routes.py
|
|
66
68
|
tests/test_pypi_check.py
|
|
67
69
|
tests/test_single_instance.py
|
|
68
70
|
tests/test_telegram_bot_config.py
|
|
@@ -92,5 +94,5 @@ web/static/dist/index.html
|
|
|
92
94
|
web/static/dist/assets/codex-telegram-icon-black.svg
|
|
93
95
|
web/static/dist/assets/codex-telegram-icon-ivory.svg
|
|
94
96
|
web/static/dist/assets/codex-telegram-login-image.png
|
|
95
|
-
web/static/dist/assets/index-
|
|
96
|
-
web/static/dist/assets/index-
|
|
97
|
+
web/static/dist/assets/index-0u8Hjl9v.js
|
|
98
|
+
web/static/dist/assets/index-Bh3KHDwf.css
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import tomllib
|
|
2
|
+
from unittest.mock import patch
|
|
3
|
+
|
|
4
|
+
import pytest
|
|
5
|
+
|
|
6
|
+
from utils.config import remove_project_profile, save_project_profile, update_project_profile
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@pytest.fixture
|
|
10
|
+
def project_config(tmp_path):
|
|
11
|
+
path = tmp_path / "conf.toml"
|
|
12
|
+
path.write_text('project = "alpha"\n\n[projects.alpha]\nname = "Alpha"\npath = "/alpha"\nextra = true\n\n[web]\nenabled = true\n')
|
|
13
|
+
with patch("utils.config._get_config_path", return_value=path):
|
|
14
|
+
yield path
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def test_modify_and_change_default_preserves_other_settings(project_config):
|
|
18
|
+
save_project_profile("beta", "Beta", "/beta")
|
|
19
|
+
update_project_profile("beta", 'New "Beta"', r"/new\beta", make_default=True)
|
|
20
|
+
parsed = tomllib.loads(project_config.read_text())
|
|
21
|
+
assert parsed["project"] == "beta"
|
|
22
|
+
assert parsed["projects"]["beta"] == {"name": 'New "Beta"', "path": r"/new\beta"}
|
|
23
|
+
assert parsed["projects"]["alpha"]["extra"] is True
|
|
24
|
+
assert parsed["web"]["enabled"] is True
|
|
25
|
+
with pytest.raises(ValueError, match="default project"):
|
|
26
|
+
remove_project_profile("beta")
|
|
27
|
+
remove_project_profile("alpha")
|
|
28
|
+
assert "alpha" not in tomllib.loads(project_config.read_text())["projects"]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_add_default_and_modify_without_unsetting_default(project_config):
|
|
32
|
+
save_project_profile("beta", "Beta", "/beta", make_default=True)
|
|
33
|
+
update_project_profile("beta", "Changed", "/changed")
|
|
34
|
+
assert tomllib.loads(project_config.read_text())["project"] == "beta"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@pytest.mark.parametrize("key,name,path", [("missing", "Name", "/path"), ("alpha", " ", "/path"), ("alpha", "Name", " ")])
|
|
38
|
+
def test_invalid_modify_does_not_write(project_config, key, name, path):
|
|
39
|
+
original = project_config.read_text()
|
|
40
|
+
with pytest.raises(ValueError):
|
|
41
|
+
update_project_profile(key, name, path, make_default=True)
|
|
42
|
+
assert project_config.read_text() == original
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import unittest
|
|
3
|
+
from types import SimpleNamespace
|
|
4
|
+
from unittest.mock import AsyncMock, patch
|
|
5
|
+
|
|
6
|
+
from fastapi import FastAPI, HTTPException
|
|
7
|
+
|
|
8
|
+
from web.routes import register_project_routes
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ProjectRouteTests(unittest.TestCase):
|
|
12
|
+
def setUp(self):
|
|
13
|
+
app = FastAPI()
|
|
14
|
+
register_project_routes(app)
|
|
15
|
+
self.modify = next(route.endpoint for route in app.routes if "PUT" in getattr(route, "methods", set()))
|
|
16
|
+
self.add = next(route.endpoint for route in app.routes if getattr(route, "path", "") == "/api/projects" and "POST" in route.methods)
|
|
17
|
+
|
|
18
|
+
def test_modify_saves_default_and_reloads(self):
|
|
19
|
+
with (
|
|
20
|
+
patch("web.routes.session_from_request", new=AsyncMock(return_value=SimpleNamespace(user_id=1))),
|
|
21
|
+
patch("web.routes.update_project_profile") as save,
|
|
22
|
+
patch("web.routes.reload") as reload_config,
|
|
23
|
+
patch("web.routes.route_command", new=AsyncMock(return_value={"ok": True})),
|
|
24
|
+
):
|
|
25
|
+
result = asyncio.run(self.modify("alpha", {"name": "Alpha", "path": "/alpha", "default": True}, object()))
|
|
26
|
+
save.assert_called_once_with("alpha", "Alpha", "/alpha", make_default=True)
|
|
27
|
+
reload_config.assert_called_once()
|
|
28
|
+
self.assertTrue(result["ok"])
|
|
29
|
+
|
|
30
|
+
def test_add_can_set_default(self):
|
|
31
|
+
with (
|
|
32
|
+
patch("web.routes.session_from_request", new=AsyncMock(return_value=SimpleNamespace(user_id=1))),
|
|
33
|
+
patch("web.routes.save_project_profile") as save,
|
|
34
|
+
patch("web.routes.reload"),
|
|
35
|
+
patch("web.routes.route_command", new=AsyncMock(return_value={})),
|
|
36
|
+
):
|
|
37
|
+
asyncio.run(self.add({"key": "alpha", "name": "Alpha", "path": "/alpha", "default": True}, object()))
|
|
38
|
+
save.assert_called_once_with("alpha", "Alpha", "/alpha", make_default=True)
|
|
39
|
+
|
|
40
|
+
def test_modify_reports_invalid_project(self):
|
|
41
|
+
with (
|
|
42
|
+
patch("web.routes.session_from_request", new=AsyncMock(return_value=SimpleNamespace(user_id=1))),
|
|
43
|
+
patch("web.routes.update_project_profile", side_effect=ValueError("not found")),
|
|
44
|
+
patch("web.routes.reload") as reload_config,
|
|
45
|
+
):
|
|
46
|
+
with self.assertRaises(HTTPException) as raised:
|
|
47
|
+
asyncio.run(self.modify("missing", {"name": "Name", "path": "/path"}, object()))
|
|
48
|
+
self.assertEqual(400, raised.exception.status_code)
|
|
49
|
+
reload_config.assert_not_called()
|
|
50
|
+
|
|
51
|
+
def test_modify_requires_authentication_before_writing(self):
|
|
52
|
+
with (
|
|
53
|
+
patch("web.routes.session_from_request", new=AsyncMock(side_effect=HTTPException(401))),
|
|
54
|
+
patch("web.routes.update_project_profile") as save,
|
|
55
|
+
):
|
|
56
|
+
with self.assertRaises(HTTPException) as raised:
|
|
57
|
+
asyncio.run(self.modify("alpha", {"name": "Name", "path": "/path"}, object()))
|
|
58
|
+
self.assertEqual(401, raised.exception.status_code)
|
|
59
|
+
save.assert_not_called()
|
|
@@ -1274,6 +1274,50 @@ path_glob_any = ["helm/**"]
|
|
|
1274
1274
|
body["items"],
|
|
1275
1275
|
)
|
|
1276
1276
|
|
|
1277
|
+
def test_thread_summaries_paginates_before_project_limit_and_offset(self):
|
|
1278
|
+
app = create_web_app()
|
|
1279
|
+
endpoint = next(route.endpoint for route in app.routes
|
|
1280
|
+
if getattr(route, "path", None) == "/api/threads/summaries")
|
|
1281
|
+
request = SimpleNamespace(cookies={COOKIE_NAME: self.session.token})
|
|
1282
|
+
user_manager.get(self.session.user_id).selected_project_key = "default"
|
|
1283
|
+
for tid in ("t-1", "t-2", "t-3"):
|
|
1284
|
+
user_manager.bind_thread_project(tid, "default")
|
|
1285
|
+
user_manager.bind_thread_project("other", "other")
|
|
1286
|
+
for offset, expected in ((0, ["t-1", "t-2"]), (1, ["t-2", "t-3"])):
|
|
1287
|
+
with self.subTest(offset=offset):
|
|
1288
|
+
state.codex_client.call = AsyncMock(side_effect=[
|
|
1289
|
+
{"data": [{"id": "other"}], "nextCursor": "page-2"},
|
|
1290
|
+
{"data": [{"id": "t-1"}], "nextCursor": "page-3"},
|
|
1291
|
+
{"data": [{"id": "t-1"}, {"id": "t-2"}, {"id": "t-3"}],
|
|
1292
|
+
"nextCursor": "unused"},
|
|
1293
|
+
{"thread": {}}, {"thread": {}},
|
|
1294
|
+
])
|
|
1295
|
+
body = asyncio.run(endpoint(request, limit=2, offset=offset))
|
|
1296
|
+
self.assertEqual(expected, [item["id"] for item in body["items"]])
|
|
1297
|
+
list_calls = [c for c in state.codex_client.call.await_args_list
|
|
1298
|
+
if c.args[0] == "thread/list"]
|
|
1299
|
+
self.assertEqual(3, len(list_calls))
|
|
1300
|
+
self.assertNotIn("cursor", list_calls[0].args[1])
|
|
1301
|
+
self.assertEqual("page-2", list_calls[1].args[1]["cursor"])
|
|
1302
|
+
self.assertEqual("page-3", list_calls[2].args[1]["cursor"])
|
|
1303
|
+
|
|
1304
|
+
def test_thread_summaries_stops_on_exhaustion_or_repeated_cursor(self):
|
|
1305
|
+
app = create_web_app()
|
|
1306
|
+
endpoint = next(route.endpoint for route in app.routes
|
|
1307
|
+
if getattr(route, "path", None) == "/api/threads/summaries")
|
|
1308
|
+
request = SimpleNamespace(cookies={COOKIE_NAME: self.session.token})
|
|
1309
|
+
user_manager.get(self.session.user_id).selected_project_key = "default"
|
|
1310
|
+
user_manager.bind_thread_project("other", "other")
|
|
1311
|
+
for page in ({"data": [{"id": "other"}]},
|
|
1312
|
+
{"data": None, "nextCursor": "repeat"}):
|
|
1313
|
+
with self.subTest(page=page):
|
|
1314
|
+
state.codex_client.call = AsyncMock(return_value=page)
|
|
1315
|
+
body = asyncio.run(endpoint(request, limit=2, archived=True))
|
|
1316
|
+
self.assertEqual([], body["items"])
|
|
1317
|
+
self.assertEqual(2 if page.get("nextCursor") else 1,
|
|
1318
|
+
state.codex_client.call.await_count)
|
|
1319
|
+
self.assertTrue(state.codex_client.call.await_args.args[1]["archived"])
|
|
1320
|
+
|
|
1277
1321
|
def test_thread_summaries_use_user_request_excerpt_as_title(self):
|
|
1278
1322
|
app = create_web_app()
|
|
1279
1323
|
endpoint = next(
|
|
@@ -682,7 +682,9 @@ def save_guardian_settings(
|
|
|
682
682
|
return get_guardian_settings()
|
|
683
683
|
|
|
684
684
|
|
|
685
|
-
def save_project_profile(
|
|
685
|
+
def save_project_profile(
|
|
686
|
+
key: str, name: str, path: str, *, make_default: bool = False
|
|
687
|
+
) -> None:
|
|
686
688
|
if not re.fullmatch(r"[A-Za-z0-9_-]+", key):
|
|
687
689
|
raise ValueError("Invalid project key. Use letters, numbers, '_' or '-'.")
|
|
688
690
|
if not name.strip():
|
|
@@ -711,4 +713,102 @@ def save_project_profile(key: str, name: str, path: str) -> None:
|
|
|
711
713
|
f'path = "{_escape_toml_string(path)}"\n'
|
|
712
714
|
)
|
|
713
715
|
|
|
716
|
+
if make_default:
|
|
717
|
+
updated = _with_default_project(updated, key)
|
|
718
|
+
tomllib.loads(updated)
|
|
719
|
+
config_path.write_text(updated, encoding="utf-8")
|
|
720
|
+
|
|
721
|
+
|
|
722
|
+
def _with_default_project(raw: str, key: str) -> str:
|
|
723
|
+
lines = raw.splitlines(keepends=True)
|
|
724
|
+
for index, line in enumerate(lines):
|
|
725
|
+
if line.lstrip().startswith("["):
|
|
726
|
+
break
|
|
727
|
+
if re.match(r"^\s*project\s*=", line):
|
|
728
|
+
lines[index] = f'project = "{_escape_toml_string(key)}"\n'
|
|
729
|
+
return "".join(lines)
|
|
730
|
+
return f'project = "{_escape_toml_string(key)}"\n' + raw
|
|
731
|
+
|
|
732
|
+
|
|
733
|
+
def update_project_profile(
|
|
734
|
+
key: str, name: str, path: str, *, make_default: bool = False
|
|
735
|
+
) -> None:
|
|
736
|
+
if not name.strip() or not path.strip():
|
|
737
|
+
raise ValueError("Project name and path must not be empty.")
|
|
738
|
+
_ensure_config_exists()
|
|
739
|
+
config_path = _get_config_path()
|
|
740
|
+
raw = config_path.read_text(encoding="utf-8")
|
|
741
|
+
parsed = tomllib.loads(raw)
|
|
742
|
+
if key not in parsed.get("projects", {}):
|
|
743
|
+
raise ValueError(f"Project key '{key}' not found.")
|
|
744
|
+
lines = raw.splitlines(keepends=True)
|
|
745
|
+
start = None
|
|
746
|
+
end = len(lines)
|
|
747
|
+
for index, line in enumerate(lines):
|
|
748
|
+
if not line.lstrip().startswith("["):
|
|
749
|
+
continue
|
|
750
|
+
if start is not None:
|
|
751
|
+
end = index
|
|
752
|
+
break
|
|
753
|
+
try:
|
|
754
|
+
header = tomllib.loads(line.strip())
|
|
755
|
+
except tomllib.TOMLDecodeError:
|
|
756
|
+
continue
|
|
757
|
+
if header.get("projects", {}).get(key) == {}:
|
|
758
|
+
start = index
|
|
759
|
+
if start is None:
|
|
760
|
+
raise ValueError("Project table could not be updated.")
|
|
761
|
+
body = "".join(lines[start + 1:end])
|
|
762
|
+
for field, value in (("name", name), ("path", path)):
|
|
763
|
+
pattern = rf"(?m)^\s*{field}\s*=.*$"
|
|
764
|
+
replacement = f'{field} = "{_escape_toml_string(value)}"'
|
|
765
|
+
if re.search(pattern, body):
|
|
766
|
+
body = re.sub(pattern, lambda match: replacement, body)
|
|
767
|
+
else:
|
|
768
|
+
body = body.rstrip() + "\n" + replacement + "\n"
|
|
769
|
+
updated = "".join(lines[:start + 1]) + body + "".join(lines[end:])
|
|
770
|
+
tomllib.loads(updated)
|
|
771
|
+
if make_default:
|
|
772
|
+
updated = _with_default_project(updated, key)
|
|
773
|
+
tomllib.loads(updated)
|
|
774
|
+
config_path.write_text(updated, encoding="utf-8")
|
|
775
|
+
|
|
776
|
+
|
|
777
|
+
def remove_project_profile(key: str) -> None:
|
|
778
|
+
_ensure_config_exists()
|
|
779
|
+
config_path = _get_config_path()
|
|
780
|
+
raw = config_path.read_text(encoding="utf-8")
|
|
781
|
+
parsed = tomllib.loads(raw) if raw.strip() else {}
|
|
782
|
+
|
|
783
|
+
projects = parsed.get("projects")
|
|
784
|
+
if not isinstance(projects, dict) or key not in projects:
|
|
785
|
+
raise ValueError(f"Project key '{key}' not found.")
|
|
786
|
+
|
|
787
|
+
if parsed.get("project") == key:
|
|
788
|
+
raise ValueError(
|
|
789
|
+
f"Cannot remove the default project '{key}'. "
|
|
790
|
+
"Set another default project first."
|
|
791
|
+
)
|
|
792
|
+
|
|
793
|
+
lines = raw.splitlines(keepends=True)
|
|
794
|
+
result: list[str] = []
|
|
795
|
+
i = 0
|
|
796
|
+
while i < len(lines):
|
|
797
|
+
line = lines[i]
|
|
798
|
+
stripped = line.strip()
|
|
799
|
+
if stripped == f"[projects.{key}]":
|
|
800
|
+
i += 1
|
|
801
|
+
while i < len(lines):
|
|
802
|
+
next_line = lines[i]
|
|
803
|
+
next_stripped = next_line.strip()
|
|
804
|
+
if next_stripped.startswith("[") and not next_stripped.startswith("[projects."):
|
|
805
|
+
break
|
|
806
|
+
if next_stripped.startswith("[projects.") and next_stripped != f"[projects.{key}]":
|
|
807
|
+
break
|
|
808
|
+
i += 1
|
|
809
|
+
continue
|
|
810
|
+
result.append(line)
|
|
811
|
+
i += 1
|
|
812
|
+
|
|
813
|
+
updated = "".join(result).rstrip() + "\n"
|
|
714
814
|
config_path.write_text(updated, encoding="utf-8")
|
|
@@ -16,7 +16,14 @@ from codex_telegram import __version__
|
|
|
16
16
|
from codex.collaboration_mode import codex_mode_name
|
|
17
17
|
from models import state
|
|
18
18
|
from models.user import user_manager
|
|
19
|
-
from utils.config import
|
|
19
|
+
from utils.config import (
|
|
20
|
+
get,
|
|
21
|
+
get_web_password,
|
|
22
|
+
reload,
|
|
23
|
+
remove_project_profile,
|
|
24
|
+
save_project_profile,
|
|
25
|
+
update_project_profile,
|
|
26
|
+
)
|
|
20
27
|
from utils.local_command import resolve_command_cwd
|
|
21
28
|
from utils.normalize import clamp_int, parse_bool
|
|
22
29
|
from web.dependencies import (
|
|
@@ -322,10 +329,6 @@ def register_thread_routes(app: FastAPI) -> None:
|
|
|
322
329
|
params: dict[str, Any] = {"limit": min(100, safe_limit + safe_offset)}
|
|
323
330
|
if archived:
|
|
324
331
|
params["archived"] = True
|
|
325
|
-
result = await state.codex_client.call("thread/list", params)
|
|
326
|
-
rows = result.get("data", [])
|
|
327
|
-
if not isinstance(rows, list):
|
|
328
|
-
rows = []
|
|
329
332
|
state_user = user_manager.get(session.user_id)
|
|
330
333
|
current_profile_key = project_key.strip()
|
|
331
334
|
if not current_profile_key:
|
|
@@ -334,12 +337,30 @@ def register_thread_routes(app: FastAPI) -> None:
|
|
|
334
337
|
default_key = get("project")
|
|
335
338
|
if isinstance(default_key, str) and default_key.strip():
|
|
336
339
|
current_profile_key = default_key.strip()
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
340
|
+
# Apply the limit to matching threads, rather than the global first page.
|
|
341
|
+
rows: list[dict[str, Any]] = []
|
|
342
|
+
seen_ids: set[str] = set()
|
|
343
|
+
seen_cursors: set[str] = set()
|
|
344
|
+
while len(rows) < safe_offset + safe_limit:
|
|
345
|
+
result = await state.codex_client.call("thread/list", dict(params))
|
|
346
|
+
page = result.get("data", [])
|
|
347
|
+
if not isinstance(page, list):
|
|
348
|
+
page = []
|
|
349
|
+
for thread in page:
|
|
350
|
+
if not isinstance(thread, dict):
|
|
351
|
+
continue
|
|
352
|
+
tid = thread.get("id")
|
|
353
|
+
if not isinstance(tid, str) or not tid or tid in seen_ids:
|
|
354
|
+
continue
|
|
355
|
+
seen_ids.add(tid)
|
|
356
|
+
if current_profile_key and thread_profile_key(thread, current_profile_key) != current_profile_key:
|
|
357
|
+
continue
|
|
358
|
+
rows.append(thread)
|
|
359
|
+
cursor = result.get("nextCursor")
|
|
360
|
+
if not isinstance(cursor, str) or not cursor or cursor in seen_cursors:
|
|
361
|
+
break
|
|
362
|
+
seen_cursors.add(cursor)
|
|
363
|
+
params["cursor"] = cursor
|
|
343
364
|
if safe_offset:
|
|
344
365
|
rows = rows[safe_offset:]
|
|
345
366
|
rows = rows[:safe_limit]
|
|
@@ -692,7 +713,33 @@ def register_project_routes(app: FastAPI) -> None:
|
|
|
692
713
|
name = _required_str(payload, "name")
|
|
693
714
|
path = _required_str(payload, "path")
|
|
694
715
|
try:
|
|
695
|
-
save_project_profile(key, name, path)
|
|
716
|
+
save_project_profile(key, name, path, make_default=payload.get("default") is True)
|
|
717
|
+
reload()
|
|
718
|
+
except ValueError as exc:
|
|
719
|
+
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
|
720
|
+
return await route_command("/projects", ["--list"], session.user_id)
|
|
721
|
+
|
|
722
|
+
@app.put("/api/projects/{project_key}")
|
|
723
|
+
async def modify_project(
|
|
724
|
+
project_key: str, payload: dict[str, Any], request: Request
|
|
725
|
+
) -> dict[str, Any]:
|
|
726
|
+
session = await session_from_request(request)
|
|
727
|
+
name = _required_str(payload, "name")
|
|
728
|
+
path = _required_str(payload, "path")
|
|
729
|
+
try:
|
|
730
|
+
update_project_profile(
|
|
731
|
+
project_key, name, path, make_default=payload.get("default") is True
|
|
732
|
+
)
|
|
733
|
+
reload()
|
|
734
|
+
except ValueError as exc:
|
|
735
|
+
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
|
736
|
+
return await route_command("/projects", ["--list"], session.user_id)
|
|
737
|
+
|
|
738
|
+
@app.delete("/api/projects/{project_key}")
|
|
739
|
+
async def remove_project(project_key: str, request: Request) -> dict[str, Any]:
|
|
740
|
+
session = await session_from_request(request)
|
|
741
|
+
try:
|
|
742
|
+
remove_project_profile(project_key)
|
|
696
743
|
reload()
|
|
697
744
|
except ValueError as exc:
|
|
698
745
|
raise HTTPException(status_code=400, detail=str(exc)) from exc
|