delos-cli 2.1.0__tar.gz → 5.9.16__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.
- {delos_cli-2.1.0 → delos_cli-5.9.16}/.gitignore +32 -10
- {delos_cli-2.1.0 → delos_cli-5.9.16}/PKG-INFO +33 -3
- {delos_cli-2.1.0 → delos_cli-5.9.16}/README.md +28 -0
- delos_cli-5.9.16/delos_cli/__main__.py +10 -0
- delos_cli-5.9.16/delos_cli/_build.json +5 -0
- {delos_cli-2.1.0 → delos_cli-5.9.16}/delos_cli/apps/__init__.py +1 -2
- {delos_cli-2.1.0 → delos_cli-5.9.16}/delos_cli/apps/base.py +2 -2
- {delos_cli-2.1.0 → delos_cli-5.9.16}/delos_cli/apps/chat/app.py +32 -2
- {delos_cli-2.1.0 → delos_cli-5.9.16}/delos_cli/apps/chat/commands.py +47 -17
- {delos_cli-2.1.0 → delos_cli-5.9.16}/delos_cli/apps/chat/render.py +10 -8
- {delos_cli-2.1.0 → delos_cli-5.9.16}/delos_cli/apps/replay.py +1 -2
- {delos_cli-2.1.0 → delos_cli-5.9.16}/delos_cli/auth/config.py +35 -7
- {delos_cli-2.1.0 → delos_cli-5.9.16}/delos_cli/commands/builtin.py +36 -26
- {delos_cli-2.1.0 → delos_cli-5.9.16}/delos_cli/commands/prompts.py +2 -2
- {delos_cli-2.1.0 → delos_cli-5.9.16}/delos_cli/ctx.py +33 -0
- {delos_cli-2.1.0 → delos_cli-5.9.16}/delos_cli/main.py +113 -161
- delos_cli-5.9.16/delos_cli/mcp/__init__.py +11 -0
- delos_cli-5.9.16/delos_cli/mcp/client.py +277 -0
- delos_cli-5.9.16/delos_cli/mcp/config.py +170 -0
- delos_cli-5.9.16/delos_cli/mcp/handlers.py +78 -0
- delos_cli-5.9.16/delos_cli/mcp/manager.py +371 -0
- delos_cli-5.9.16/delos_cli/mcp/state.py +98 -0
- {delos_cli-2.1.0 → delos_cli-5.9.16}/delos_cli/public_key.py +1 -1
- {delos_cli-2.1.0 → delos_cli-5.9.16}/delos_cli/serve/confirm.py +3 -0
- {delos_cli-2.1.0 → delos_cli-5.9.16}/delos_cli/serve/rpc.py +76 -17
- {delos_cli-2.1.0 → delos_cli-5.9.16}/delos_cli/serve/server.py +12 -0
- {delos_cli-2.1.0 → delos_cli-5.9.16}/delos_cli/state.py +28 -29
- {delos_cli-2.1.0 → delos_cli-5.9.16}/delos_cli/telemetry.py +4 -0
- {delos_cli-2.1.0 → delos_cli-5.9.16}/delos_cli/tools/glob_tool.py +64 -18
- {delos_cli-2.1.0 → delos_cli-5.9.16}/delos_cli/tools/grep.py +4 -3
- {delos_cli-2.1.0 → delos_cli-5.9.16}/delos_cli/tools/read.py +2 -1
- {delos_cli-2.1.0 → delos_cli-5.9.16}/delos_cli/tools/run_shell.py +102 -6
- delos_cli-5.9.16/delos_cli/transport/documents.py +89 -0
- delos_cli-5.9.16/delos_cli/transport/models.py +62 -0
- {delos_cli-2.1.0 → delos_cli-5.9.16}/delos_cli/ui/chat_picker.py +6 -0
- {delos_cli-2.1.0 → delos_cli-5.9.16}/delos_cli/ui/integrations_picker.py +18 -3
- {delos_cli-2.1.0 → delos_cli-5.9.16}/delos_cli/ui/repl.py +152 -37
- {delos_cli-2.1.0 → delos_cli-5.9.16}/pyproject.toml +12 -4
- delos_cli-5.9.16/tests/__init__.py +0 -0
- delos_cli-5.9.16/tests/mcp/__init__.py +0 -0
- delos_cli-5.9.16/tests/mcp/fake_server.py +85 -0
- delos_cli-5.9.16/tests/mcp/test_client.py +93 -0
- delos_cli-5.9.16/tests/mcp/test_manager.py +207 -0
- delos_cli-2.1.0/.claude/settings.local.json +0 -8
- delos_cli-2.1.0/delos_cli/_build.json +0 -1
- delos_cli-2.1.0/delos_cli/apps/scribe/__init__.py +0 -5
- delos_cli-2.1.0/delos_cli/apps/scribe/app.py +0 -117
- delos_cli-2.1.0/delos_cli/apps/scribe/commands.py +0 -18
- delos_cli-2.1.0/delos_cli/apps/scribe/tools.py +0 -455
- delos_cli-2.1.0/delos_cli/transport/documents.py +0 -175
- delos_cli-2.1.0/delos_cli/transport/models.py +0 -19
- delos_cli-2.1.0/delos_cli/ui/document_picker.py +0 -146
- {delos_cli-2.1.0 → delos_cli-5.9.16}/delos_cli/__init__.py +0 -0
- {delos_cli-2.1.0 → delos_cli-5.9.16}/delos_cli/agent/__init__.py +0 -0
- {delos_cli-2.1.0 → delos_cli-5.9.16}/delos_cli/agent/session.py +0 -0
- {delos_cli-2.1.0 → delos_cli-5.9.16}/delos_cli/agent/tools.py +0 -0
- {delos_cli-2.1.0 → delos_cli-5.9.16}/delos_cli/agent/transport.py +0 -0
- {delos_cli-2.1.0 → delos_cli-5.9.16}/delos_cli/agent/turn.py +0 -0
- {delos_cli-2.1.0 → delos_cli-5.9.16}/delos_cli/apps/chat/__init__.py +0 -0
- {delos_cli-2.1.0 → delos_cli-5.9.16}/delos_cli/auth/__init__.py +0 -0
- {delos_cli-2.1.0 → delos_cli-5.9.16}/delos_cli/auth/mfa.py +0 -0
- {delos_cli-2.1.0 → delos_cli-5.9.16}/delos_cli/auth/oauth.py +0 -0
- {delos_cli-2.1.0 → delos_cli-5.9.16}/delos_cli/auth/token_manager.py +0 -0
- {delos_cli-2.1.0 → delos_cli-5.9.16}/delos_cli/commands/__init__.py +0 -0
- {delos_cli-2.1.0 → delos_cli-5.9.16}/delos_cli/commands/base.py +0 -0
- {delos_cli-2.1.0 → delos_cli-5.9.16}/delos_cli/context_report.py +0 -0
- {delos_cli-2.1.0 → delos_cli-5.9.16}/delos_cli/git_context.py +0 -0
- {delos_cli-2.1.0 → delos_cli-5.9.16}/delos_cli/loop.py +0 -0
- {delos_cli-2.1.0 → delos_cli-5.9.16}/delos_cli/project_context.py +0 -0
- {delos_cli-2.1.0 → delos_cli-5.9.16}/delos_cli/sentry_identity.py +0 -0
- {delos_cli-2.1.0 → delos_cli-5.9.16}/delos_cli/serve/__init__.py +0 -0
- {delos_cli-2.1.0 → delos_cli-5.9.16}/delos_cli/serve/app.py +0 -0
- {delos_cli-2.1.0 → delos_cli-5.9.16}/delos_cli/serve/protocol.py +0 -0
- {delos_cli-2.1.0 → delos_cli-5.9.16}/delos_cli/tools/__init__.py +0 -0
- {delos_cli-2.1.0 → delos_cli-5.9.16}/delos_cli/tools/ask_user_input.py +0 -0
- {delos_cli-2.1.0 → delos_cli-5.9.16}/delos_cli/tools/edit_content.py +0 -0
- {delos_cli-2.1.0 → delos_cli-5.9.16}/delos_cli/tools/explore.py +0 -0
- {delos_cli-2.1.0 → delos_cli-5.9.16}/delos_cli/tools/task.py +0 -0
- {delos_cli-2.1.0 → delos_cli-5.9.16}/delos_cli/tools/todo_write.py +0 -0
- {delos_cli-2.1.0 → delos_cli-5.9.16}/delos_cli/tools/verify.py +0 -0
- {delos_cli-2.1.0 → delos_cli-5.9.16}/delos_cli/tools/write_content.py +0 -0
- {delos_cli-2.1.0 → delos_cli-5.9.16}/delos_cli/transport/__init__.py +0 -0
- {delos_cli-2.1.0 → delos_cli-5.9.16}/delos_cli/transport/chats.py +0 -0
- {delos_cli-2.1.0 → delos_cli-5.9.16}/delos_cli/transport/client.py +0 -0
- {delos_cli-2.1.0 → delos_cli-5.9.16}/delos_cli/transport/integrations.py +0 -0
- {delos_cli-2.1.0 → delos_cli-5.9.16}/delos_cli/ui/__init__.py +0 -0
- {delos_cli-2.1.0 → delos_cli-5.9.16}/delos_cli/ui/banner.py +0 -0
- {delos_cli-2.1.0 → delos_cli-5.9.16}/delos_cli/ui/collapse.py +0 -0
- {delos_cli-2.1.0 → delos_cli-5.9.16}/delos_cli/ui/completer.py +0 -0
- {delos_cli-2.1.0 → delos_cli-5.9.16}/delos_cli/ui/lexer.py +0 -0
- {delos_cli-2.1.0 → delos_cli-5.9.16}/delos_cli/ui/model_picker.py +0 -0
- {delos_cli-2.1.0 → delos_cli-5.9.16}/delos_cli/ui/output.py +0 -0
- {delos_cli-2.1.0 → delos_cli-5.9.16}/delos_cli/ui/style.py +0 -0
- {delos_cli-2.1.0 → delos_cli-5.9.16}/delos_cli/usage_report.py +0 -0
|
@@ -2,6 +2,8 @@ knip.txt
|
|
|
2
2
|
pentest-*
|
|
3
3
|
backend/.vscode
|
|
4
4
|
backend/cosmos_env
|
|
5
|
+
# convert_scribes_to_docx.py run log (projects/1015_scribe_decoupling)
|
|
6
|
+
backend/scribe_conversion.jsonl
|
|
5
7
|
# Allow backend/.env (has safe localhost defaults), but ignore all other .env files
|
|
6
8
|
apps/**/.env
|
|
7
9
|
collab/.env
|
|
@@ -21,26 +23,33 @@ infrastructure/supabase/**/.env
|
|
|
21
23
|
backend/.python-version
|
|
22
24
|
/scripts/
|
|
23
25
|
*/scripts/
|
|
26
|
+
!packages/cms/shared/scripts/
|
|
24
27
|
!tooling/scripts/
|
|
25
28
|
# Re-include the tools-catalog export script (CI drift test imports it)
|
|
26
29
|
!backend/scripts/
|
|
27
30
|
backend/scripts/*
|
|
28
31
|
!backend/scripts/__init__.py
|
|
29
32
|
!backend/scripts/export_tools_catalog.py
|
|
33
|
+
!backend/scripts/export_module_catalog.py
|
|
34
|
+
!backend/scripts/export_microsoft_capabilities.py
|
|
30
35
|
!backend/scripts/check_supabase_models.py
|
|
31
36
|
!backend/scripts/dump_openapi.py
|
|
32
37
|
!backend/scripts/backfill_agent_issues.py
|
|
33
38
|
!backend/scripts/backfill_directive_recurrence.py
|
|
34
39
|
!backend/scripts/analyze_prompt_dumps.py
|
|
35
40
|
!backend/scripts/check_route_response_types.py
|
|
41
|
+
!backend/scripts/check_usage_export.py
|
|
36
42
|
!backend/scripts/fly_source_recovery.py
|
|
37
43
|
!backend/scripts/audit_sprite_regions.py
|
|
38
44
|
!backend/scripts/migrate_vibe_sprites.py
|
|
45
|
+
!backend/scripts/requeue_mail_file_processing.py
|
|
46
|
+
!backend/scripts/dedupe_outlook_mail.py
|
|
39
47
|
|
|
40
48
|
.claude/worktrees
|
|
41
49
|
|
|
42
50
|
data/documents2
|
|
43
51
|
data/logs
|
|
52
|
+
.data/database-service
|
|
44
53
|
**/**0
|
|
45
54
|
_local
|
|
46
55
|
.env.old
|
|
@@ -86,12 +95,9 @@ dist/
|
|
|
86
95
|
*.pem
|
|
87
96
|
|
|
88
97
|
# pnpm is the only package manager here, including the standalone services.
|
|
89
|
-
# One recorded exception: cvn/whatsapp-bridge is an npm-managed runtime sidecar
|
|
90
|
-
# (baileys needs the git-only `libsignal`, which blockExoticSubdeps rejects and
|
|
91
|
-
# pnpm has no scoped exception for — pnpm/pnpm#11799); its lockfile is tracked
|
|
92
|
-
# so the git ref stays pinned.
|
|
93
98
|
package-lock.json
|
|
94
|
-
|
|
99
|
+
# Pinned Node libraries copied into the document authoring Docker images.
|
|
100
|
+
!backend/cosmos/modules/office/pptx/node/package-lock.json
|
|
95
101
|
yarn.lock
|
|
96
102
|
npm-shrinkwrap.json
|
|
97
103
|
|
|
@@ -157,7 +163,8 @@ logs/
|
|
|
157
163
|
# serena
|
|
158
164
|
.serena/
|
|
159
165
|
|
|
160
|
-
# Mobile
|
|
166
|
+
# Mobile app (Expo/React Native) — root-level so prettier/oxlint invoked from
|
|
167
|
+
# the repo root skip the generated native folders (they read only this .gitignore)
|
|
161
168
|
apps/mobile/.expo/
|
|
162
169
|
apps/mobile/dist/
|
|
163
170
|
apps/mobile/web-build/
|
|
@@ -170,13 +177,13 @@ apps/mobile/*.mobileprovision
|
|
|
170
177
|
apps/mobile/*.p12
|
|
171
178
|
apps/mobile/*.key
|
|
172
179
|
apps/mobile/.metro-health-check*
|
|
180
|
+
apps/mobile/.*
|
|
173
181
|
|
|
174
|
-
# Desktop App (Electron) — build output (node_modules/build already ignored above)
|
|
175
|
-
apps/desktop/dist/
|
|
176
|
-
apps/desktop/release/
|
|
177
182
|
|
|
183
|
+
# Desktop App (Electron) — build output (node_modules/build already ignored above)
|
|
184
|
+
apps/ping-desktop/dist/
|
|
185
|
+
apps/ping-desktop/release/
|
|
178
186
|
|
|
179
|
-
.gitnexus
|
|
180
187
|
|
|
181
188
|
# Python cache
|
|
182
189
|
__pycache__/
|
|
@@ -200,3 +207,18 @@ apps/workers-mobile/eas-asc-key.p8
|
|
|
200
207
|
|
|
201
208
|
# backfill_fts.py keyset cursor (local run state)
|
|
202
209
|
projects/0904_drivesearch/.fts-cursor.json
|
|
210
|
+
|
|
211
|
+
# Totem: the Pi's config, pushed by totem/scripts/deploy.sh; the Pi-side lock is local.
|
|
212
|
+
# Its scripts/ are the deploy path itself, not build spill: re-included past `*/scripts/`.
|
|
213
|
+
totem/.env.pi
|
|
214
|
+
totem/uv.lock
|
|
215
|
+
!totem/scripts/
|
|
216
|
+
|
|
217
|
+
# pnpm addin:check report page
|
|
218
|
+
.addin-check/
|
|
219
|
+
|
|
220
|
+
# Corporate templates the PowerPoint evals run on — the customers' own files, kept off git
|
|
221
|
+
backend/evals/data/powerpoint/private/
|
|
222
|
+
|
|
223
|
+
# Delos Code worktrees — full repo copies; tools that walk the tree (ty, ruff, oxlint) must skip them
|
|
224
|
+
.delos/
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: delos-cli
|
|
3
|
-
Version:
|
|
3
|
+
Version: 5.9.16
|
|
4
4
|
Summary: Terminal REPL for the Delos agent — the Claude-Code-style CLI for Delos.
|
|
5
5
|
Project-URL: Homepage, https://delos.so
|
|
6
6
|
Project-URL: Repository, https://github.com/Delos-Intelligence/cosmos-saas
|
|
@@ -24,9 +24,11 @@ Requires-Dist: httpx>=0.27.0
|
|
|
24
24
|
Requires-Dist: prompt-toolkit>=3.0.47
|
|
25
25
|
Requires-Dist: questionary>=2.0.1
|
|
26
26
|
Requires-Dist: rich>=13.8.0
|
|
27
|
-
Requires-Dist: ripgrep<15,>=14; sys_platform == 'darwin'
|
|
28
|
-
Requires-Dist: ripgrep<15,>=14; sys_platform == 'linux' and platform_machine == 'x86_64'
|
|
27
|
+
Requires-Dist: ripgrep<15,>=14; sys_platform == 'darwin'
|
|
28
|
+
Requires-Dist: ripgrep<15,>=14; sys_platform == 'linux' and (platform_machine == 'x86_64' or platform_machine == 'aarch64')
|
|
29
|
+
Requires-Dist: ripgrep<15,>=14; sys_platform == 'win32' and (platform_machine == 'AMD64' or platform_machine == 'x86_64')
|
|
29
30
|
Requires-Dist: sentry-sdk<3,>=2.44.0
|
|
31
|
+
Requires-Dist: truststore>=0.10.0
|
|
30
32
|
Requires-Dist: typer>=0.12.5
|
|
31
33
|
Description-Content-Type: text/markdown
|
|
32
34
|
|
|
@@ -85,6 +87,7 @@ Conversations are scoped to the directory you launch from — running
|
|
|
85
87
|
/help list commands
|
|
86
88
|
/clear wipe the screen and start a fresh conversation
|
|
87
89
|
/rename <new name> rename the current chat
|
|
90
|
+
/mode [name] show or set the mode: ask, plan, auto, build (default: build; shift-tab cycles)
|
|
88
91
|
/delete delete the current chat and go back to the picker
|
|
89
92
|
/resume back to the picker without deleting
|
|
90
93
|
/stop interrupt the agent
|
|
@@ -93,6 +96,33 @@ Conversations are scoped to the directory you launch from — running
|
|
|
93
96
|
/quit exit
|
|
94
97
|
```
|
|
95
98
|
|
|
99
|
+
### Local MCP servers
|
|
100
|
+
|
|
101
|
+
The CLI runs the stdio MCP servers declared in a `.mcp.json` (the Claude Code
|
|
102
|
+
format) and gives their tools to the agent as `mcp__<server>__<tool>`. They
|
|
103
|
+
execute on your machine, after the same permission prompt as shell commands,
|
|
104
|
+
so keys stay local.
|
|
105
|
+
|
|
106
|
+
- global: `~/.config/delos/mcp.json`
|
|
107
|
+
- project: the nearest `.mcp.json` up to the repo root (wins on a name clash)
|
|
108
|
+
|
|
109
|
+
```json
|
|
110
|
+
{
|
|
111
|
+
"mcpServers": {
|
|
112
|
+
"my-server": {
|
|
113
|
+
"command": "uvx",
|
|
114
|
+
"args": ["my-mcp"],
|
|
115
|
+
"env": { "TOKEN": "${MY_TOKEN}" },
|
|
116
|
+
"timeout": 600
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
`timeout` is per call, in seconds. `http`/`sse` entries are skipped for now. A
|
|
123
|
+
project server only starts once you approve it in `/mcp`, and again whenever
|
|
124
|
+
its command, args or env change. Server logs: `~/.config/delos/logs/mcp-<name>.log`.
|
|
125
|
+
|
|
96
126
|
## Headless mode (VSCode extension engine)
|
|
97
127
|
|
|
98
128
|
```bash
|
|
@@ -53,6 +53,7 @@ Conversations are scoped to the directory you launch from — running
|
|
|
53
53
|
/help list commands
|
|
54
54
|
/clear wipe the screen and start a fresh conversation
|
|
55
55
|
/rename <new name> rename the current chat
|
|
56
|
+
/mode [name] show or set the mode: ask, plan, auto, build (default: build; shift-tab cycles)
|
|
56
57
|
/delete delete the current chat and go back to the picker
|
|
57
58
|
/resume back to the picker without deleting
|
|
58
59
|
/stop interrupt the agent
|
|
@@ -61,6 +62,33 @@ Conversations are scoped to the directory you launch from — running
|
|
|
61
62
|
/quit exit
|
|
62
63
|
```
|
|
63
64
|
|
|
65
|
+
### Local MCP servers
|
|
66
|
+
|
|
67
|
+
The CLI runs the stdio MCP servers declared in a `.mcp.json` (the Claude Code
|
|
68
|
+
format) and gives their tools to the agent as `mcp__<server>__<tool>`. They
|
|
69
|
+
execute on your machine, after the same permission prompt as shell commands,
|
|
70
|
+
so keys stay local.
|
|
71
|
+
|
|
72
|
+
- global: `~/.config/delos/mcp.json`
|
|
73
|
+
- project: the nearest `.mcp.json` up to the repo root (wins on a name clash)
|
|
74
|
+
|
|
75
|
+
```json
|
|
76
|
+
{
|
|
77
|
+
"mcpServers": {
|
|
78
|
+
"my-server": {
|
|
79
|
+
"command": "uvx",
|
|
80
|
+
"args": ["my-mcp"],
|
|
81
|
+
"env": { "TOKEN": "${MY_TOKEN}" },
|
|
82
|
+
"timeout": 600
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
`timeout` is per call, in seconds. `http`/`sse` entries are skipped for now. A
|
|
89
|
+
project server only starts once you approve it in `/mcp`, and again whenever
|
|
90
|
+
its command, args or env change. Server logs: `~/.config/delos/logs/mcp-<name>.log`.
|
|
91
|
+
|
|
64
92
|
## Headless mode (VSCode extension engine)
|
|
65
93
|
|
|
66
94
|
```bash
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""``python -m delos_cli`` — how the VS Code extension runs its bundled copy.
|
|
2
|
+
|
|
3
|
+
The extension ships its own interpreter with the CLI installed into it and
|
|
4
|
+
launches it through this module rather than the ``delos`` console script,
|
|
5
|
+
whose launcher is tied to the machine that built it.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from delos_cli.telemetry import main
|
|
9
|
+
|
|
10
|
+
main()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""App + Renderer protocols.
|
|
2
2
|
|
|
3
|
-
Each app (chat,
|
|
3
|
+
Each app (chat, code, …) implements ``App``. The loop is
|
|
4
4
|
app-agnostic: it only knows how to send a turn, get a renderer, and
|
|
5
5
|
ask for a toolbar fragment.
|
|
6
6
|
"""
|
|
@@ -49,7 +49,7 @@ class App(ABC):
|
|
|
49
49
|
Subclasses own their own state (model choice, selected tools, …).
|
|
50
50
|
"""
|
|
51
51
|
|
|
52
|
-
#: Short user-facing name (``"chat"``,
|
|
52
|
+
#: Short user-facing name (``"chat"``, …).
|
|
53
53
|
name: str
|
|
54
54
|
|
|
55
55
|
#: Slash commands specific to this app — merged with the global registry.
|
|
@@ -23,6 +23,7 @@ from delos_cli.agent import AgentSession, AgentTransport, ToolRegistry
|
|
|
23
23
|
from delos_cli.apps.base import App
|
|
24
24
|
from delos_cli.apps.replay import replay_messages
|
|
25
25
|
from delos_cli.git_context import collect_git_context
|
|
26
|
+
from delos_cli.mcp import McpManager, register_tools
|
|
26
27
|
from delos_cli.state import TodoStore
|
|
27
28
|
from delos_cli.tools import (
|
|
28
29
|
handle_ask_user_input,
|
|
@@ -50,7 +51,7 @@ from delos_cli.tools import (
|
|
|
50
51
|
render_write_content,
|
|
51
52
|
)
|
|
52
53
|
from delos_cli.transport.client import TransportError
|
|
53
|
-
from delos_cli.transport.models import
|
|
54
|
+
from delos_cli.transport.models import fetch_picker_models
|
|
54
55
|
|
|
55
56
|
from .commands import CHAT_COMMANDS
|
|
56
57
|
from .render import V6Renderer
|
|
@@ -60,6 +61,7 @@ if TYPE_CHECKING:
|
|
|
60
61
|
|
|
61
62
|
from delos_cli.commands.base import CommandSpec
|
|
62
63
|
from delos_cli.ctx import Ctx
|
|
64
|
+
from delos_cli.transport.client import AuthedClient
|
|
63
65
|
from delos_cli.ui.output import OutputBuffer
|
|
64
66
|
|
|
65
67
|
|
|
@@ -104,6 +106,10 @@ class ChatApp(App):
|
|
|
104
106
|
|
|
105
107
|
name: str = "chat"
|
|
106
108
|
available_models: list[str] = field(default_factory=list)
|
|
109
|
+
#: Model id → Gate's display name (``Claude Sonnet 5.5``); ids missing here fall back to the slug.
|
|
110
|
+
model_titles: dict[str, str] = field(default_factory=dict)
|
|
111
|
+
#: Model new chats start on: the first model of the picker's top section, else its first model.
|
|
112
|
+
default_model: str | None = None
|
|
107
113
|
commands: dict[str, CommandSpec] = field(default_factory=lambda: dict(CHAT_COMMANDS))
|
|
108
114
|
#: Per-tool renderers + handlers. Built-in entries cover the
|
|
109
115
|
#: client-side tools the backend exposes (``run_shell`` for now).
|
|
@@ -119,13 +125,28 @@ class ChatApp(App):
|
|
|
119
125
|
#: Set by ``main._run_repl`` after the picker resolves a chat_id, so
|
|
120
126
|
#: history is rendered the moment the REPL takes over the screen.
|
|
121
127
|
pending_replay: list[dict[str, Any]] = field(default_factory=list)
|
|
128
|
+
#: Local stdio MCP servers (``.mcp.json``); set by ``main._run_repl``.
|
|
129
|
+
mcp: McpManager | None = None
|
|
122
130
|
_current_session: AgentSession | None = field(default=None, init=False, repr=False)
|
|
123
131
|
|
|
132
|
+
async def refresh_models(self, http: AuthedClient) -> None:
|
|
133
|
+
"""Load the ``cli`` picker: available ids, Gate titles and the default model for new chats."""
|
|
134
|
+
models = await fetch_picker_models(http)
|
|
135
|
+
self.available_models = [m["id"] for m in models]
|
|
136
|
+
self.model_titles = {m["id"]: m["title"] for m in models if m["title"]}
|
|
137
|
+
top = next((m for m in models if m["top"]), None)
|
|
138
|
+
first = top or (models[0] if models else None)
|
|
139
|
+
self.default_model = first["id"] if first else None
|
|
140
|
+
|
|
141
|
+
def model_label(self, model: str) -> str:
|
|
142
|
+
"""Gate's display name for ``model``, or the slug itself when unknown."""
|
|
143
|
+
return self.model_titles.get(model, model)
|
|
144
|
+
|
|
124
145
|
@override
|
|
125
146
|
async def on_enter(self, ctx: Ctx) -> None:
|
|
126
147
|
"""Prefetch model list, then replay any staged conversation history."""
|
|
127
148
|
try:
|
|
128
|
-
self.
|
|
149
|
+
await self.refresh_models(ctx.http)
|
|
129
150
|
except TransportError as e:
|
|
130
151
|
sink = ctx.output if ctx.output is not None else ctx.console
|
|
131
152
|
sink.print(Text(f"(could not fetch models: {e})", style="yellow"))
|
|
@@ -163,6 +184,15 @@ class ChatApp(App):
|
|
|
163
184
|
# the agent doesn't burn its first tool call on `git status`.
|
|
164
185
|
if (git_ctx := await collect_git_context(Path.cwd())) is not None:
|
|
165
186
|
body["git_context"] = git_ctx
|
|
187
|
+
# Local MCP tools ride in every request of the turn (continuations
|
|
188
|
+
# re-send the body), never in plan mode — they may write anywhere.
|
|
189
|
+
if self.mcp is not None and ctx.state.permission_mode != "plan":
|
|
190
|
+
specs = await self.mcp.client_tool_specs()
|
|
191
|
+
if specs:
|
|
192
|
+
register_tools(self.tools, self.mcp, specs)
|
|
193
|
+
body["client_tools"] = specs
|
|
194
|
+
if (warning := self.mcp.tool_count_warning()) is not None and ctx.output is not None:
|
|
195
|
+
ctx.output.print(Text(warning, style="yellow"))
|
|
166
196
|
|
|
167
197
|
self._current_session = session
|
|
168
198
|
try:
|
|
@@ -5,28 +5,26 @@ Currently exposes ``/model`` so the user can change the active model
|
|
|
5
5
|
|
|
6
6
|
Two modes:
|
|
7
7
|
|
|
8
|
-
- ``/model`` →
|
|
9
|
-
|
|
10
|
-
Application
|
|
11
|
-
|
|
12
|
-
re-enters the REPL on the same ``conv_id``.
|
|
8
|
+
- ``/model`` → arrow-key picker shown over the input (``ctx.pick``),
|
|
9
|
+
then PATCH the row and update ``ctx.state.model``.
|
|
10
|
+
The REPL Application never exits, so nothing is
|
|
11
|
+
reprinted and the input comes straight back.
|
|
13
12
|
- ``/model <id>`` → validate against the cached
|
|
14
13
|
``app.available_models`` list, PATCH the row,
|
|
15
|
-
update ``ctx.state.model``.
|
|
14
|
+
update ``ctx.state.model``.
|
|
16
15
|
"""
|
|
17
16
|
|
|
18
17
|
from __future__ import annotations
|
|
19
18
|
|
|
20
|
-
import contextlib
|
|
21
19
|
from pathlib import Path
|
|
22
20
|
from typing import TYPE_CHECKING, Any
|
|
23
21
|
|
|
24
|
-
from prompt_toolkit.application import get_app
|
|
25
22
|
from rich.text import Text
|
|
26
23
|
|
|
27
24
|
from delos_cli.commands.base import CommandSpec, registry_from
|
|
28
25
|
from delos_cli.commands.prompts import EXECUTE_PROMPT, INIT_PROMPT_NEW, INIT_PROMPT_UPDATE
|
|
29
26
|
from delos_cli.context_report import fetch_context_report, render_report
|
|
27
|
+
from delos_cli.ctx import PickOption
|
|
30
28
|
from delos_cli.git_context import collect_git_context
|
|
31
29
|
from delos_cli.transport.chats import patch_chat_model
|
|
32
30
|
from delos_cli.transport.client import TransportError
|
|
@@ -48,17 +46,47 @@ def _available_models(ctx: Ctx) -> list[str]:
|
|
|
48
46
|
return list(getattr(ctx.app, "available_models", []) or [])
|
|
49
47
|
|
|
50
48
|
|
|
49
|
+
async def _pick_model(ctx: Ctx) -> None:
|
|
50
|
+
"""Show the model picker over the input, then apply the choice."""
|
|
51
|
+
sink = _sink(ctx)
|
|
52
|
+
if ctx.pick is None:
|
|
53
|
+
sink.print(Text("no interactive picker here — use /model <id>", style="yellow"))
|
|
54
|
+
return
|
|
55
|
+
|
|
56
|
+
# ``on_enter`` normally fills the cache; if that fetch failed (or came back
|
|
57
|
+
# empty) the picker would be empty — fetch lazily so /model stays reliable.
|
|
58
|
+
refresh = getattr(ctx.app, "refresh_models", None)
|
|
59
|
+
if not _available_models(ctx) and refresh is not None:
|
|
60
|
+
try:
|
|
61
|
+
await refresh(ctx.http)
|
|
62
|
+
except TransportError as e:
|
|
63
|
+
sink.print(Text(f"(could not fetch models: {e})", style="yellow"))
|
|
64
|
+
models = _available_models(ctx)
|
|
65
|
+
if not models:
|
|
66
|
+
sink.print(Text("no models available", style="yellow"))
|
|
67
|
+
return
|
|
68
|
+
|
|
69
|
+
titles: dict[str, str] = getattr(ctx.app, "model_titles", {})
|
|
70
|
+
options = [PickOption(m, titles.get(m, m), m if m in titles else "") for m in models]
|
|
71
|
+
chosen = await ctx.pick("Select a model", options, ctx.state.model)
|
|
72
|
+
if chosen is None or chosen == ctx.state.model:
|
|
73
|
+
return
|
|
74
|
+
try:
|
|
75
|
+
await patch_chat_model(ctx.http, chat_id=ctx.state.conv_id, model=chosen)
|
|
76
|
+
except TransportError as e:
|
|
77
|
+
sink.print(Text(f"failed to set model: {e}", style="red"))
|
|
78
|
+
return
|
|
79
|
+
ctx.state.model = chosen
|
|
80
|
+
sink.print(Text(f"model → {titles.get(chosen, chosen)}", style="dim"))
|
|
81
|
+
|
|
82
|
+
|
|
51
83
|
async def _handle_model(ctx: Ctx, args: str) -> None:
|
|
52
84
|
"""Open the picker (no arg) or set directly (``/model <id>``)."""
|
|
53
85
|
sink = _sink(ctx)
|
|
54
86
|
arg = args.strip()
|
|
55
87
|
|
|
56
88
|
if not arg:
|
|
57
|
-
|
|
58
|
-
# inside the live REPL). /resume uses the same exit pattern.
|
|
59
|
-
ctx.state.model_pick_requested = True
|
|
60
|
-
with contextlib.suppress(Exception):
|
|
61
|
-
get_app().exit()
|
|
89
|
+
await _pick_model(ctx)
|
|
62
90
|
return
|
|
63
91
|
|
|
64
92
|
models = _available_models(ctx)
|
|
@@ -74,7 +102,9 @@ async def _handle_model(ctx: Ctx, args: str) -> None:
|
|
|
74
102
|
return
|
|
75
103
|
|
|
76
104
|
ctx.state.model = arg
|
|
77
|
-
|
|
105
|
+
titles: dict[str, str] = getattr(ctx.app, "model_titles", {})
|
|
106
|
+
label = titles.get(arg, arg)
|
|
107
|
+
sink.print(Text(f"model → {label}", style="dim"))
|
|
78
108
|
|
|
79
109
|
|
|
80
110
|
def _complete_model(ctx: Ctx, prefix: str) -> Iterable[str]:
|
|
@@ -115,11 +145,11 @@ INIT = CommandSpec(
|
|
|
115
145
|
|
|
116
146
|
|
|
117
147
|
async def _handle_execute(ctx: Ctx, args: str) -> None: # noqa: RUF029 — CommandSpec handlers are async
|
|
118
|
-
"""Leave
|
|
148
|
+
"""Leave Plan mode (back to Build) and stage the execute prompt."""
|
|
119
149
|
_ = args
|
|
120
150
|
if ctx.state.permission_mode == "plan":
|
|
121
|
-
ctx.state.permission_mode = "
|
|
122
|
-
_sink(ctx).print(Text("
|
|
151
|
+
ctx.state.permission_mode = "build"
|
|
152
|
+
_sink(ctx).print(Text("mode → Build", style="dim"))
|
|
123
153
|
ctx.state.staged_message = EXECUTE_PROMPT
|
|
124
154
|
|
|
125
155
|
|
|
@@ -201,7 +201,7 @@ class V6Renderer:
|
|
|
201
201
|
)
|
|
202
202
|
|
|
203
203
|
elif etype == "data-usage":
|
|
204
|
-
# Accumulated, not drawn: ``
|
|
204
|
+
# Accumulated, not drawn: exposed as ``usage_label`` once the turn ends.
|
|
205
205
|
payload = event.get("data") or {}
|
|
206
206
|
self._input_tokens += int(payload.get("inputTokens") or 0)
|
|
207
207
|
self._cached_tokens += int(payload.get("cachedTokens") or 0)
|
|
@@ -247,16 +247,18 @@ class V6Renderer:
|
|
|
247
247
|
self._output.commit_live()
|
|
248
248
|
else:
|
|
249
249
|
self._output.discard_live()
|
|
250
|
-
self._render_usage()
|
|
251
250
|
|
|
252
|
-
|
|
253
|
-
|
|
251
|
+
@property
|
|
252
|
+
def usage_label(self) -> str:
|
|
253
|
+
"""The turn's prompt tokens and cache hit rate (``⚡ cache 80% · 48k in``), or ``""``.
|
|
254
|
+
|
|
255
|
+
Not printed into the scrollback: the REPL reads it after ``close()``
|
|
256
|
+
and shows it in the bottom toolbar.
|
|
257
|
+
"""
|
|
254
258
|
if self._input_tokens <= 0:
|
|
255
|
-
return
|
|
259
|
+
return ""
|
|
256
260
|
hit = round(100 * self._cached_tokens / self._input_tokens)
|
|
257
|
-
self.
|
|
258
|
-
Text(f"⚡ cache {hit}% · {_fmt_tokens(self._input_tokens)} in", style="dim"),
|
|
259
|
-
)
|
|
261
|
+
return f"⚡ cache {hit}% · {_fmt_tokens(self._input_tokens)} in"
|
|
260
262
|
|
|
261
263
|
@property
|
|
262
264
|
def assistant_text(self) -> str:
|
|
@@ -6,8 +6,7 @@ on REPL entry, routing tool calls through the active app's
|
|
|
6
6
|
turns as for live ones.
|
|
7
7
|
|
|
8
8
|
Each app supplies the message list itself (chat fetches a normalized
|
|
9
|
-
``app_cli_messages`` table
|
|
10
|
-
of ``app_scribe_chats``) — once a list is in hand the rendering is
|
|
9
|
+
``app_cli_messages`` table) — once a list is in hand the rendering is
|
|
11
10
|
identical, so it lives here instead of duplicated per app.
|
|
12
11
|
"""
|
|
13
12
|
|
|
@@ -15,9 +15,9 @@ monorepo so each developer's local ngrok tunnel is picked up automatically.
|
|
|
15
15
|
from __future__ import annotations
|
|
16
16
|
|
|
17
17
|
import contextlib
|
|
18
|
-
import fcntl # POSIX only; Windows support skipped for v1
|
|
19
18
|
import json
|
|
20
19
|
import os
|
|
20
|
+
import sys
|
|
21
21
|
import tempfile
|
|
22
22
|
from contextlib import contextmanager
|
|
23
23
|
from dataclasses import asdict, dataclass, field
|
|
@@ -49,10 +49,10 @@ class RegionUrls(TypedDict):
|
|
|
49
49
|
|
|
50
50
|
# Supabase anon keys are public; same values live in apps/web/.env.production.*
|
|
51
51
|
# and are embedded in the web/extension bundles. Safe to ship here.
|
|
52
|
-
_EU_ANON = "
|
|
52
|
+
_EU_ANON = "sb_publishable_k0h1VrcR5Wlo7uHypgzp6EPXBM6AmCJYaty1HdlwHkc"
|
|
53
53
|
_US_ANON = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzU5MjY5NjAwLCJleHAiOjE5MTcwMzYwMDB9.VYuiRrB3q9GaR3qsZauOm1oIH_bZ3iuOzmNQfDcqo40"
|
|
54
54
|
_AE_ANON = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzY1MTQ4NDAwLCJleHAiOjE5MjI5MTQ4MDB9.TlD6gF-Q5K1drdnh-XPQgXGs410h5NcHJBRgV6aNXrY"
|
|
55
|
-
_STAGING_ANON = "
|
|
55
|
+
_STAGING_ANON = "sb_publishable_FXOYjigrAP8eOtVoJsxJs-X6X6oEL0UfEIi3n-3r_4o"
|
|
56
56
|
_DEV_ANON = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0"
|
|
57
57
|
|
|
58
58
|
|
|
@@ -343,6 +343,33 @@ def save_tokens(cfg: Config) -> Path:
|
|
|
343
343
|
return save(disk)
|
|
344
344
|
|
|
345
345
|
|
|
346
|
+
if sys.platform == "win32":
|
|
347
|
+
import msvcrt
|
|
348
|
+
|
|
349
|
+
def _lock(f: IO[str]) -> None:
|
|
350
|
+
# LK_LOCK gives up after ~10 s of retries; loop for flock's blocking semantics.
|
|
351
|
+
f.seek(0)
|
|
352
|
+
while True:
|
|
353
|
+
try:
|
|
354
|
+
msvcrt.locking(f.fileno(), msvcrt.LK_LOCK, 1)
|
|
355
|
+
except OSError:
|
|
356
|
+
continue
|
|
357
|
+
return
|
|
358
|
+
|
|
359
|
+
def _unlock(f: IO[str]) -> None:
|
|
360
|
+
f.seek(0)
|
|
361
|
+
msvcrt.locking(f.fileno(), msvcrt.LK_UNLCK, 1)
|
|
362
|
+
|
|
363
|
+
else:
|
|
364
|
+
import fcntl
|
|
365
|
+
|
|
366
|
+
def _lock(f: IO[str]) -> None:
|
|
367
|
+
fcntl.flock(f.fileno(), fcntl.LOCK_EX)
|
|
368
|
+
|
|
369
|
+
def _unlock(f: IO[str]) -> None:
|
|
370
|
+
fcntl.flock(f.fileno(), fcntl.LOCK_UN)
|
|
371
|
+
|
|
372
|
+
|
|
346
373
|
@contextmanager
|
|
347
374
|
def config_file_lock() -> Iterator[None]:
|
|
348
375
|
"""Advisory cross-process exclusive lock on ``~/.config/delos/.config.lock``.
|
|
@@ -352,17 +379,18 @@ def config_file_lock() -> Iterator[None]:
|
|
|
352
379
|
against the same stale refresh_token (whichever loses the race would
|
|
353
380
|
otherwise be kicked out with ``invalid_grant``).
|
|
354
381
|
|
|
355
|
-
|
|
356
|
-
left in place; only the lock state
|
|
382
|
+
``fcntl.flock`` on POSIX, ``msvcrt.locking`` on byte 0 on Windows. The
|
|
383
|
+
lockfile is created on first use and left in place; only the lock state
|
|
384
|
+
matters.
|
|
357
385
|
"""
|
|
358
386
|
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
|
359
387
|
f: IO[str] = LOCK_PATH.open("a+")
|
|
360
388
|
try:
|
|
361
|
-
|
|
389
|
+
_lock(f)
|
|
362
390
|
yield
|
|
363
391
|
finally:
|
|
364
392
|
with contextlib.suppress(OSError):
|
|
365
|
-
|
|
393
|
+
_unlock(f)
|
|
366
394
|
f.close()
|
|
367
395
|
|
|
368
396
|
|