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.
Files changed (94) hide show
  1. {delos_cli-2.1.0 → delos_cli-5.9.16}/.gitignore +32 -10
  2. {delos_cli-2.1.0 → delos_cli-5.9.16}/PKG-INFO +33 -3
  3. {delos_cli-2.1.0 → delos_cli-5.9.16}/README.md +28 -0
  4. delos_cli-5.9.16/delos_cli/__main__.py +10 -0
  5. delos_cli-5.9.16/delos_cli/_build.json +5 -0
  6. {delos_cli-2.1.0 → delos_cli-5.9.16}/delos_cli/apps/__init__.py +1 -2
  7. {delos_cli-2.1.0 → delos_cli-5.9.16}/delos_cli/apps/base.py +2 -2
  8. {delos_cli-2.1.0 → delos_cli-5.9.16}/delos_cli/apps/chat/app.py +32 -2
  9. {delos_cli-2.1.0 → delos_cli-5.9.16}/delos_cli/apps/chat/commands.py +47 -17
  10. {delos_cli-2.1.0 → delos_cli-5.9.16}/delos_cli/apps/chat/render.py +10 -8
  11. {delos_cli-2.1.0 → delos_cli-5.9.16}/delos_cli/apps/replay.py +1 -2
  12. {delos_cli-2.1.0 → delos_cli-5.9.16}/delos_cli/auth/config.py +35 -7
  13. {delos_cli-2.1.0 → delos_cli-5.9.16}/delos_cli/commands/builtin.py +36 -26
  14. {delos_cli-2.1.0 → delos_cli-5.9.16}/delos_cli/commands/prompts.py +2 -2
  15. {delos_cli-2.1.0 → delos_cli-5.9.16}/delos_cli/ctx.py +33 -0
  16. {delos_cli-2.1.0 → delos_cli-5.9.16}/delos_cli/main.py +113 -161
  17. delos_cli-5.9.16/delos_cli/mcp/__init__.py +11 -0
  18. delos_cli-5.9.16/delos_cli/mcp/client.py +277 -0
  19. delos_cli-5.9.16/delos_cli/mcp/config.py +170 -0
  20. delos_cli-5.9.16/delos_cli/mcp/handlers.py +78 -0
  21. delos_cli-5.9.16/delos_cli/mcp/manager.py +371 -0
  22. delos_cli-5.9.16/delos_cli/mcp/state.py +98 -0
  23. {delos_cli-2.1.0 → delos_cli-5.9.16}/delos_cli/public_key.py +1 -1
  24. {delos_cli-2.1.0 → delos_cli-5.9.16}/delos_cli/serve/confirm.py +3 -0
  25. {delos_cli-2.1.0 → delos_cli-5.9.16}/delos_cli/serve/rpc.py +76 -17
  26. {delos_cli-2.1.0 → delos_cli-5.9.16}/delos_cli/serve/server.py +12 -0
  27. {delos_cli-2.1.0 → delos_cli-5.9.16}/delos_cli/state.py +28 -29
  28. {delos_cli-2.1.0 → delos_cli-5.9.16}/delos_cli/telemetry.py +4 -0
  29. {delos_cli-2.1.0 → delos_cli-5.9.16}/delos_cli/tools/glob_tool.py +64 -18
  30. {delos_cli-2.1.0 → delos_cli-5.9.16}/delos_cli/tools/grep.py +4 -3
  31. {delos_cli-2.1.0 → delos_cli-5.9.16}/delos_cli/tools/read.py +2 -1
  32. {delos_cli-2.1.0 → delos_cli-5.9.16}/delos_cli/tools/run_shell.py +102 -6
  33. delos_cli-5.9.16/delos_cli/transport/documents.py +89 -0
  34. delos_cli-5.9.16/delos_cli/transport/models.py +62 -0
  35. {delos_cli-2.1.0 → delos_cli-5.9.16}/delos_cli/ui/chat_picker.py +6 -0
  36. {delos_cli-2.1.0 → delos_cli-5.9.16}/delos_cli/ui/integrations_picker.py +18 -3
  37. {delos_cli-2.1.0 → delos_cli-5.9.16}/delos_cli/ui/repl.py +152 -37
  38. {delos_cli-2.1.0 → delos_cli-5.9.16}/pyproject.toml +12 -4
  39. delos_cli-5.9.16/tests/__init__.py +0 -0
  40. delos_cli-5.9.16/tests/mcp/__init__.py +0 -0
  41. delos_cli-5.9.16/tests/mcp/fake_server.py +85 -0
  42. delos_cli-5.9.16/tests/mcp/test_client.py +93 -0
  43. delos_cli-5.9.16/tests/mcp/test_manager.py +207 -0
  44. delos_cli-2.1.0/.claude/settings.local.json +0 -8
  45. delos_cli-2.1.0/delos_cli/_build.json +0 -1
  46. delos_cli-2.1.0/delos_cli/apps/scribe/__init__.py +0 -5
  47. delos_cli-2.1.0/delos_cli/apps/scribe/app.py +0 -117
  48. delos_cli-2.1.0/delos_cli/apps/scribe/commands.py +0 -18
  49. delos_cli-2.1.0/delos_cli/apps/scribe/tools.py +0 -455
  50. delos_cli-2.1.0/delos_cli/transport/documents.py +0 -175
  51. delos_cli-2.1.0/delos_cli/transport/models.py +0 -19
  52. delos_cli-2.1.0/delos_cli/ui/document_picker.py +0 -146
  53. {delos_cli-2.1.0 → delos_cli-5.9.16}/delos_cli/__init__.py +0 -0
  54. {delos_cli-2.1.0 → delos_cli-5.9.16}/delos_cli/agent/__init__.py +0 -0
  55. {delos_cli-2.1.0 → delos_cli-5.9.16}/delos_cli/agent/session.py +0 -0
  56. {delos_cli-2.1.0 → delos_cli-5.9.16}/delos_cli/agent/tools.py +0 -0
  57. {delos_cli-2.1.0 → delos_cli-5.9.16}/delos_cli/agent/transport.py +0 -0
  58. {delos_cli-2.1.0 → delos_cli-5.9.16}/delos_cli/agent/turn.py +0 -0
  59. {delos_cli-2.1.0 → delos_cli-5.9.16}/delos_cli/apps/chat/__init__.py +0 -0
  60. {delos_cli-2.1.0 → delos_cli-5.9.16}/delos_cli/auth/__init__.py +0 -0
  61. {delos_cli-2.1.0 → delos_cli-5.9.16}/delos_cli/auth/mfa.py +0 -0
  62. {delos_cli-2.1.0 → delos_cli-5.9.16}/delos_cli/auth/oauth.py +0 -0
  63. {delos_cli-2.1.0 → delos_cli-5.9.16}/delos_cli/auth/token_manager.py +0 -0
  64. {delos_cli-2.1.0 → delos_cli-5.9.16}/delos_cli/commands/__init__.py +0 -0
  65. {delos_cli-2.1.0 → delos_cli-5.9.16}/delos_cli/commands/base.py +0 -0
  66. {delos_cli-2.1.0 → delos_cli-5.9.16}/delos_cli/context_report.py +0 -0
  67. {delos_cli-2.1.0 → delos_cli-5.9.16}/delos_cli/git_context.py +0 -0
  68. {delos_cli-2.1.0 → delos_cli-5.9.16}/delos_cli/loop.py +0 -0
  69. {delos_cli-2.1.0 → delos_cli-5.9.16}/delos_cli/project_context.py +0 -0
  70. {delos_cli-2.1.0 → delos_cli-5.9.16}/delos_cli/sentry_identity.py +0 -0
  71. {delos_cli-2.1.0 → delos_cli-5.9.16}/delos_cli/serve/__init__.py +0 -0
  72. {delos_cli-2.1.0 → delos_cli-5.9.16}/delos_cli/serve/app.py +0 -0
  73. {delos_cli-2.1.0 → delos_cli-5.9.16}/delos_cli/serve/protocol.py +0 -0
  74. {delos_cli-2.1.0 → delos_cli-5.9.16}/delos_cli/tools/__init__.py +0 -0
  75. {delos_cli-2.1.0 → delos_cli-5.9.16}/delos_cli/tools/ask_user_input.py +0 -0
  76. {delos_cli-2.1.0 → delos_cli-5.9.16}/delos_cli/tools/edit_content.py +0 -0
  77. {delos_cli-2.1.0 → delos_cli-5.9.16}/delos_cli/tools/explore.py +0 -0
  78. {delos_cli-2.1.0 → delos_cli-5.9.16}/delos_cli/tools/task.py +0 -0
  79. {delos_cli-2.1.0 → delos_cli-5.9.16}/delos_cli/tools/todo_write.py +0 -0
  80. {delos_cli-2.1.0 → delos_cli-5.9.16}/delos_cli/tools/verify.py +0 -0
  81. {delos_cli-2.1.0 → delos_cli-5.9.16}/delos_cli/tools/write_content.py +0 -0
  82. {delos_cli-2.1.0 → delos_cli-5.9.16}/delos_cli/transport/__init__.py +0 -0
  83. {delos_cli-2.1.0 → delos_cli-5.9.16}/delos_cli/transport/chats.py +0 -0
  84. {delos_cli-2.1.0 → delos_cli-5.9.16}/delos_cli/transport/client.py +0 -0
  85. {delos_cli-2.1.0 → delos_cli-5.9.16}/delos_cli/transport/integrations.py +0 -0
  86. {delos_cli-2.1.0 → delos_cli-5.9.16}/delos_cli/ui/__init__.py +0 -0
  87. {delos_cli-2.1.0 → delos_cli-5.9.16}/delos_cli/ui/banner.py +0 -0
  88. {delos_cli-2.1.0 → delos_cli-5.9.16}/delos_cli/ui/collapse.py +0 -0
  89. {delos_cli-2.1.0 → delos_cli-5.9.16}/delos_cli/ui/completer.py +0 -0
  90. {delos_cli-2.1.0 → delos_cli-5.9.16}/delos_cli/ui/lexer.py +0 -0
  91. {delos_cli-2.1.0 → delos_cli-5.9.16}/delos_cli/ui/model_picker.py +0 -0
  92. {delos_cli-2.1.0 → delos_cli-5.9.16}/delos_cli/ui/output.py +0 -0
  93. {delos_cli-2.1.0 → delos_cli-5.9.16}/delos_cli/ui/style.py +0 -0
  94. {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
- !cvn/whatsapp-bridge/package-lock.json
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 App (Expo/React Native)
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: 2.1.0
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' and platform_machine == 'arm64'
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()
@@ -0,0 +1,5 @@
1
+ {
2
+ "release": "v5.9.16",
3
+ "sha": "6d8943056ab27d475e82f4f8ce09feba751745fb",
4
+ "version": "5.9.16"
5
+ }
@@ -2,6 +2,5 @@
2
2
 
3
3
  from .base import App, Renderer
4
4
  from .chat import ChatApp
5
- from .scribe import ScribeApp
6
5
 
7
- __all__ = ["App", "ChatApp", "Renderer", "ScribeApp"]
6
+ __all__ = ["App", "ChatApp", "Renderer"]
@@ -1,6 +1,6 @@
1
1
  """App + Renderer protocols.
2
2
 
3
- Each app (chat, scribe, code, …) implements ``App``. The loop is
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"``, ``"scribe"``, …).
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 fetch_models
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.available_models = await fetch_models(ctx.http)
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`` → defer to the outer loop: set
9
- ``state.model_pick_requested`` and exit the REPL
10
- Application. ``main._run_repl`` then runs
11
- :func:`pick_model` (a standalone Application) and
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``. Stays inside the REPL.
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
- # Defer to the outer loop: it owns the picker (no nested Application
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
- sink.print(Text(f"model → {arg}", style="dim"))
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 plan permission-mode and stage the execute prompt."""
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 = "default"
122
- _sink(ctx).print(Text("permission mode → default", style="dim"))
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: ``close()`` renders the turn total.
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
- def _render_usage(self) -> None:
253
- """One dim line with the turn's prompt tokens and how many were cached."""
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._output.append_block(
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; scribe reads the ``messages`` jsonb column
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 = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.ewogICJyb2xlIjogImFub24iLAogICJpc3MiOiAic3VwYWJhc2UiLAogICJpYXQiOiAxNzEyNTI3MjAwLAogICJleHAiOiAxODcwMjkzNjAwCn0.gO1qyjuG2g4yAZ3u820P9q5CUTQmDiDrHSurZjIePbA"
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 = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.ewogICJyb2xlIjogImFub24iLAogICJpc3MiOiAic3VwYWJhc2UiLAogICJpYXQiOiAxNzEyNTI3MjAwLAogICJleHAiOiAxODcwMjkzNjAwCn0.GDHgmBuu4XivLHq18P3A-sHblw99If9uqW-HChepSb8"
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
- POSIX only (uses :mod:`fcntl`). The lockfile is created on first use and
356
- left in place; only the lock state matters.
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
- fcntl.flock(f.fileno(), fcntl.LOCK_EX)
389
+ _lock(f)
362
390
  yield
363
391
  finally:
364
392
  with contextlib.suppress(OSError):
365
- fcntl.flock(f.fileno(), fcntl.LOCK_UN)
393
+ _unlock(f)
366
394
  f.close()
367
395
 
368
396