delos-cli 2.0.2__tar.gz → 5.9.15__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 (80) hide show
  1. {delos_cli-2.0.2 → delos_cli-5.9.15}/.gitignore +32 -10
  2. {delos_cli-2.0.2 → delos_cli-5.9.15}/PKG-INFO +6 -3
  3. delos_cli-5.9.15/delos_cli/__main__.py +10 -0
  4. delos_cli-5.9.15/delos_cli/_build.json +5 -0
  5. {delos_cli-2.0.2 → delos_cli-5.9.15}/delos_cli/agent/session.py +13 -3
  6. {delos_cli-2.0.2 → delos_cli-5.9.15}/delos_cli/apps/chat/render.py +22 -0
  7. {delos_cli-2.0.2 → delos_cli-5.9.15}/delos_cli/auth/config.py +49 -9
  8. {delos_cli-2.0.2 → delos_cli-5.9.15}/delos_cli/commands/prompts.py +2 -2
  9. delos_cli-5.9.15/delos_cli/public_key.py +116 -0
  10. delos_cli-5.9.15/delos_cli/sentry_identity.py +31 -0
  11. {delos_cli-2.0.2 → delos_cli-5.9.15}/delos_cli/serve/confirm.py +3 -0
  12. {delos_cli-2.0.2 → delos_cli-5.9.15}/delos_cli/serve/rpc.py +42 -15
  13. {delos_cli-2.0.2 → delos_cli-5.9.15}/delos_cli/serve/server.py +7 -0
  14. delos_cli-5.9.15/delos_cli/telemetry.py +79 -0
  15. {delos_cli-2.0.2 → delos_cli-5.9.15}/delos_cli/tools/glob_tool.py +15 -3
  16. {delos_cli-2.0.2 → delos_cli-5.9.15}/delos_cli/tools/grep.py +21 -3
  17. delos_cli-5.9.15/delos_cli/transport/models.py +50 -0
  18. {delos_cli-2.0.2 → delos_cli-5.9.15}/pyproject.toml +20 -11
  19. delos_cli-2.0.2/delos_cli/transport/models.py +0 -19
  20. {delos_cli-2.0.2 → delos_cli-5.9.15}/README.md +0 -0
  21. {delos_cli-2.0.2 → delos_cli-5.9.15}/delos_cli/__init__.py +0 -0
  22. {delos_cli-2.0.2 → delos_cli-5.9.15}/delos_cli/agent/__init__.py +0 -0
  23. {delos_cli-2.0.2 → delos_cli-5.9.15}/delos_cli/agent/tools.py +0 -0
  24. {delos_cli-2.0.2 → delos_cli-5.9.15}/delos_cli/agent/transport.py +0 -0
  25. {delos_cli-2.0.2 → delos_cli-5.9.15}/delos_cli/agent/turn.py +0 -0
  26. {delos_cli-2.0.2 → delos_cli-5.9.15}/delos_cli/apps/__init__.py +0 -0
  27. {delos_cli-2.0.2 → delos_cli-5.9.15}/delos_cli/apps/base.py +0 -0
  28. {delos_cli-2.0.2 → delos_cli-5.9.15}/delos_cli/apps/chat/__init__.py +0 -0
  29. {delos_cli-2.0.2 → delos_cli-5.9.15}/delos_cli/apps/chat/app.py +0 -0
  30. {delos_cli-2.0.2 → delos_cli-5.9.15}/delos_cli/apps/chat/commands.py +0 -0
  31. {delos_cli-2.0.2 → delos_cli-5.9.15}/delos_cli/apps/replay.py +0 -0
  32. {delos_cli-2.0.2 → delos_cli-5.9.15}/delos_cli/apps/scribe/__init__.py +0 -0
  33. {delos_cli-2.0.2 → delos_cli-5.9.15}/delos_cli/apps/scribe/app.py +0 -0
  34. {delos_cli-2.0.2 → delos_cli-5.9.15}/delos_cli/apps/scribe/commands.py +0 -0
  35. {delos_cli-2.0.2 → delos_cli-5.9.15}/delos_cli/apps/scribe/tools.py +0 -0
  36. {delos_cli-2.0.2 → delos_cli-5.9.15}/delos_cli/auth/__init__.py +0 -0
  37. {delos_cli-2.0.2 → delos_cli-5.9.15}/delos_cli/auth/mfa.py +0 -0
  38. {delos_cli-2.0.2 → delos_cli-5.9.15}/delos_cli/auth/oauth.py +0 -0
  39. {delos_cli-2.0.2 → delos_cli-5.9.15}/delos_cli/auth/token_manager.py +0 -0
  40. {delos_cli-2.0.2 → delos_cli-5.9.15}/delos_cli/commands/__init__.py +0 -0
  41. {delos_cli-2.0.2 → delos_cli-5.9.15}/delos_cli/commands/base.py +0 -0
  42. {delos_cli-2.0.2 → delos_cli-5.9.15}/delos_cli/commands/builtin.py +0 -0
  43. {delos_cli-2.0.2 → delos_cli-5.9.15}/delos_cli/context_report.py +0 -0
  44. {delos_cli-2.0.2 → delos_cli-5.9.15}/delos_cli/ctx.py +0 -0
  45. {delos_cli-2.0.2 → delos_cli-5.9.15}/delos_cli/git_context.py +0 -0
  46. {delos_cli-2.0.2 → delos_cli-5.9.15}/delos_cli/loop.py +0 -0
  47. {delos_cli-2.0.2 → delos_cli-5.9.15}/delos_cli/main.py +0 -0
  48. {delos_cli-2.0.2 → delos_cli-5.9.15}/delos_cli/project_context.py +0 -0
  49. {delos_cli-2.0.2 → delos_cli-5.9.15}/delos_cli/serve/__init__.py +0 -0
  50. {delos_cli-2.0.2 → delos_cli-5.9.15}/delos_cli/serve/app.py +0 -0
  51. {delos_cli-2.0.2 → delos_cli-5.9.15}/delos_cli/serve/protocol.py +0 -0
  52. {delos_cli-2.0.2 → delos_cli-5.9.15}/delos_cli/state.py +0 -0
  53. {delos_cli-2.0.2 → delos_cli-5.9.15}/delos_cli/tools/__init__.py +0 -0
  54. {delos_cli-2.0.2 → delos_cli-5.9.15}/delos_cli/tools/ask_user_input.py +0 -0
  55. {delos_cli-2.0.2 → delos_cli-5.9.15}/delos_cli/tools/edit_content.py +0 -0
  56. {delos_cli-2.0.2 → delos_cli-5.9.15}/delos_cli/tools/explore.py +0 -0
  57. {delos_cli-2.0.2 → delos_cli-5.9.15}/delos_cli/tools/read.py +0 -0
  58. {delos_cli-2.0.2 → delos_cli-5.9.15}/delos_cli/tools/run_shell.py +0 -0
  59. {delos_cli-2.0.2 → delos_cli-5.9.15}/delos_cli/tools/task.py +0 -0
  60. {delos_cli-2.0.2 → delos_cli-5.9.15}/delos_cli/tools/todo_write.py +0 -0
  61. {delos_cli-2.0.2 → delos_cli-5.9.15}/delos_cli/tools/verify.py +0 -0
  62. {delos_cli-2.0.2 → delos_cli-5.9.15}/delos_cli/tools/write_content.py +0 -0
  63. {delos_cli-2.0.2 → delos_cli-5.9.15}/delos_cli/transport/__init__.py +0 -0
  64. {delos_cli-2.0.2 → delos_cli-5.9.15}/delos_cli/transport/chats.py +0 -0
  65. {delos_cli-2.0.2 → delos_cli-5.9.15}/delos_cli/transport/client.py +0 -0
  66. {delos_cli-2.0.2 → delos_cli-5.9.15}/delos_cli/transport/documents.py +0 -0
  67. {delos_cli-2.0.2 → delos_cli-5.9.15}/delos_cli/transport/integrations.py +0 -0
  68. {delos_cli-2.0.2 → delos_cli-5.9.15}/delos_cli/ui/__init__.py +0 -0
  69. {delos_cli-2.0.2 → delos_cli-5.9.15}/delos_cli/ui/banner.py +0 -0
  70. {delos_cli-2.0.2 → delos_cli-5.9.15}/delos_cli/ui/chat_picker.py +0 -0
  71. {delos_cli-2.0.2 → delos_cli-5.9.15}/delos_cli/ui/collapse.py +0 -0
  72. {delos_cli-2.0.2 → delos_cli-5.9.15}/delos_cli/ui/completer.py +0 -0
  73. {delos_cli-2.0.2 → delos_cli-5.9.15}/delos_cli/ui/document_picker.py +0 -0
  74. {delos_cli-2.0.2 → delos_cli-5.9.15}/delos_cli/ui/integrations_picker.py +0 -0
  75. {delos_cli-2.0.2 → delos_cli-5.9.15}/delos_cli/ui/lexer.py +0 -0
  76. {delos_cli-2.0.2 → delos_cli-5.9.15}/delos_cli/ui/model_picker.py +0 -0
  77. {delos_cli-2.0.2 → delos_cli-5.9.15}/delos_cli/ui/output.py +0 -0
  78. {delos_cli-2.0.2 → delos_cli-5.9.15}/delos_cli/ui/repl.py +0 -0
  79. {delos_cli-2.0.2 → delos_cli-5.9.15}/delos_cli/ui/style.py +0 -0
  80. {delos_cli-2.0.2 → delos_cli-5.9.15}/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.0.2
3
+ Version: 5.9.15
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,8 +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')
30
+ Requires-Dist: sentry-sdk<3,>=2.44.0
31
+ Requires-Dist: truststore>=0.10.0
29
32
  Requires-Dist: typer>=0.12.5
30
33
  Description-Content-Type: text/markdown
31
34
 
@@ -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.15",
3
+ "sha": "d90e1f13f19f4dd81ac163706bab5fe35e75761e",
4
+ "version": "5.9.15"
5
+ }
@@ -56,8 +56,9 @@ class AgentSession:
56
56
  terminates with unresolved tool calls, treats them as client-side,
57
57
  invokes the registered handler, posts the result via
58
58
  ``/tool-result``, and re-streams a continuation request with
59
- ``tool_continuation=true`` and ``messages=[]``. Loops until no
60
- pending calls remain or the user explicitly stopped.
59
+ ``tool_continuation=true``, ``messages=[]`` and no ``git_context``
60
+ (the backend replays the snapshot it froze on the user message).
61
+ Loops until no pending calls remain or the user explicitly stopped.
61
62
 
62
63
  The backend never emits ``tool-output-available`` for client-side
63
64
  tools — with ``emit_tool_output=True``, a synthetic one is yielded
@@ -128,7 +129,16 @@ class AgentSession:
128
129
  "output": result,
129
130
  }
130
131
 
131
- current_body = {**body, "messages": [], "tool_continuation": True}
132
+ # ``git_context`` is dropped: the backend froze the turn's snapshot
133
+ # onto the user message on the first request and replays it from
134
+ # there, so re-sending a fresh one would be ignored at best.
135
+ # ``custom_instructions`` stays — the system prompt is re-rendered
136
+ # on every request and reads it.
137
+ current_body = {
138
+ **{k: v for k, v in body.items() if k != "git_context"},
139
+ "messages": [],
140
+ "tool_continuation": True,
141
+ }
132
142
 
133
143
  async def stop(self) -> None:
134
144
  """Set the backend stop signal. Idempotent."""
@@ -109,6 +109,12 @@ class V6Renderer:
109
109
  # inline between literal tag deltas. Reasoning is swallowed (the
110
110
  # spinner keeps running) so it never lands in scrollback.
111
111
  self._in_think = False
112
+ # Prompt-token totals across every LLM call of this user turn (one
113
+ # renderer per turn, N HTTP requests inside it). Summed rather than
114
+ # rendered per call: the interesting number is what the whole turn
115
+ # paid, and a line per continuation would drown the transcript.
116
+ self._input_tokens = 0
117
+ self._cached_tokens = 0
112
118
 
113
119
  def apply(self, event: dict[str, Any]) -> None:
114
120
  """Apply a single v6 event to the output buffer."""
@@ -194,6 +200,12 @@ class V6Renderer:
194
200
  ),
195
201
  )
196
202
 
203
+ elif etype == "data-usage":
204
+ # Accumulated, not drawn: ``close()`` renders the turn total.
205
+ payload = event.get("data") or {}
206
+ self._input_tokens += int(payload.get("inputTokens") or 0)
207
+ self._cached_tokens += int(payload.get("cachedTokens") or 0)
208
+
197
209
  elif etype == "data-compaction-skipped":
198
210
  # Backend tried to compact but couldn't (LLM failure, etc.).
199
211
  # Surface it so the user understands why the agent may still
@@ -235,6 +247,16 @@ class V6Renderer:
235
247
  self._output.commit_live()
236
248
  else:
237
249
  self._output.discard_live()
250
+ self._render_usage()
251
+
252
+ def _render_usage(self) -> None:
253
+ """One dim line with the turn's prompt tokens and how many were cached."""
254
+ if self._input_tokens <= 0:
255
+ return
256
+ 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
+ )
238
260
 
239
261
  @property
240
262
  def assistant_text(self) -> str:
@@ -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
@@ -25,6 +25,8 @@ from pathlib import Path
25
25
  from typing import IO, TYPE_CHECKING, Any, TypedDict
26
26
 
27
27
  from delos_cli import _editable_source_dir
28
+ from delos_cli.public_key import is_public_key, resolve_public_key
29
+ from delos_cli.telemetry import applied_config
28
30
 
29
31
  if TYPE_CHECKING:
30
32
  from collections.abc import Iterator
@@ -47,10 +49,10 @@ class RegionUrls(TypedDict):
47
49
 
48
50
  # Supabase anon keys are public; same values live in apps/web/.env.production.*
49
51
  # and are embedded in the web/extension bundles. Safe to ship here.
50
- _EU_ANON = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.ewogICJyb2xlIjogImFub24iLAogICJpc3MiOiAic3VwYWJhc2UiLAogICJpYXQiOiAxNzEyNTI3MjAwLAogICJleHAiOiAxODcwMjkzNjAwCn0.gO1qyjuG2g4yAZ3u820P9q5CUTQmDiDrHSurZjIePbA"
52
+ _EU_ANON = "sb_publishable_k0h1VrcR5Wlo7uHypgzp6EPXBM6AmCJYaty1HdlwHkc"
51
53
  _US_ANON = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzU5MjY5NjAwLCJleHAiOjE5MTcwMzYwMDB9.VYuiRrB3q9GaR3qsZauOm1oIH_bZ3iuOzmNQfDcqo40"
52
54
  _AE_ANON = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzY1MTQ4NDAwLCJleHAiOjE5MjI5MTQ4MDB9.TlD6gF-Q5K1drdnh-XPQgXGs410h5NcHJBRgV6aNXrY"
53
- _STAGING_ANON = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.ewogICJyb2xlIjogImFub24iLAogICJpc3MiOiAic3VwYWJhc2UiLAogICJpYXQiOiAxNzEyNTI3MjAwLAogICJleHAiOiAxODcwMjkzNjAwCn0.GDHgmBuu4XivLHq18P3A-sHblw99If9uqW-HChepSb8"
55
+ _STAGING_ANON = "sb_publishable_FXOYjigrAP8eOtVoJsxJs-X6X6oEL0UfEIi3n-3r_4o"
54
56
  _DEV_ANON = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0"
55
57
 
56
58
 
@@ -187,8 +189,16 @@ class Config:
187
189
 
188
190
  @property
189
191
  def supabase_anon_key(self) -> str:
190
- """Public Supabase anon key for this region."""
191
- return (PUBLIC_REGIONS | INTERNAL_REGIONS)[self.region]["supabase_anon_key"]
192
+ """Public key for this database; discovery never changes session tokens."""
193
+ if override := os.environ.get("DELOS_SUPABASE_PUBLISHABLE_KEY"):
194
+ if not is_public_key(override):
195
+ message = "DELOS_SUPABASE_PUBLISHABLE_KEY must be a public Supabase key"
196
+ raise ValueError(message)
197
+ return override
198
+ bundled = (PUBLIC_REGIONS | INTERNAL_REGIONS)[self.region]["supabase_anon_key"]
199
+ if self.region == "dev" and self.supabase_url == INTERNAL_REGIONS["dev"]["supabase_url"]:
200
+ return bundled
201
+ return resolve_public_key(self.web_url, self.supabase_url, bundled, CONFIG_DIR / "public-keys")
192
202
 
193
203
  @property
194
204
  def is_signed_in(self) -> bool:
@@ -232,6 +242,7 @@ def set_region(cfg: Config, region: str) -> None:
232
242
  cfg.user_id = adopted.user_id
233
243
  cfg.client_id = adopted.client_id
234
244
  cfg.tokens = adopted.tokens
245
+ applied_config(cfg)
235
246
 
236
247
 
237
248
  def load() -> Config:
@@ -268,6 +279,7 @@ def load() -> Config:
268
279
  if cfg.region not in available_regions():
269
280
  set_region(cfg, DEFAULT_REGION)
270
281
 
282
+ applied_config(cfg)
271
283
  return cfg
272
284
 
273
285
 
@@ -331,6 +343,33 @@ def save_tokens(cfg: Config) -> Path:
331
343
  return save(disk)
332
344
 
333
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
+
334
373
  @contextmanager
335
374
  def config_file_lock() -> Iterator[None]:
336
375
  """Advisory cross-process exclusive lock on ``~/.config/delos/.config.lock``.
@@ -340,17 +379,18 @@ def config_file_lock() -> Iterator[None]:
340
379
  against the same stale refresh_token (whichever loses the race would
341
380
  otherwise be kicked out with ``invalid_grant``).
342
381
 
343
- POSIX only (uses :mod:`fcntl`). The lockfile is created on first use and
344
- 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.
345
385
  """
346
386
  CONFIG_DIR.mkdir(parents=True, exist_ok=True)
347
387
  f: IO[str] = LOCK_PATH.open("a+")
348
388
  try:
349
- fcntl.flock(f.fileno(), fcntl.LOCK_EX)
389
+ _lock(f)
350
390
  yield
351
391
  finally:
352
392
  with contextlib.suppress(OSError):
353
- fcntl.flock(f.fileno(), fcntl.LOCK_UN)
393
+ _unlock(f)
354
394
  f.close()
355
395
 
356
396
 
@@ -73,7 +73,7 @@ def expand_slash_command(
73
73
  Returns ``(content, mode)`` — unchanged when the message isn't a known
74
74
  command (unknown ``/xyz`` passes through as plain text; the agent can
75
75
  answer helpfully). ``/execute`` forces build mode: executing a plan is
76
- pointless with read-only tools.
76
+ pointless with read-only tools (``auto`` is kept: it already executes).
77
77
  """
78
78
  head, _, rest = content.strip().partition(" ")
79
79
  rest = rest.strip()
@@ -86,5 +86,5 @@ def expand_slash_command(
86
86
  prompt = EXECUTE_PROMPT
87
87
  if rest:
88
88
  prompt += f"\n\nAdditional user instructions: {rest}"
89
- return prompt, "build"
89
+ return prompt, "auto" if mode == "auto" else "build"
90
90
  return content, mode
@@ -0,0 +1,116 @@
1
+ """Public key discovery; keep in sync with desktop/backend/cvn/public_key.py.
2
+
3
+ Only public API keys are cached. Database URLs, user JWTs and refresh tokens
4
+ are never changed. A process uses one snapshot until it restarts.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import base64
10
+ import contextlib
11
+ import hashlib
12
+ import json
13
+ import re
14
+ import tempfile
15
+ from functools import lru_cache
16
+ from pathlib import Path
17
+ from urllib.parse import urlsplit
18
+
19
+ import httpx
20
+
21
+ MAX_PUBLIC_KEY_LENGTH = 8192
22
+ JWT_PARTS = 3
23
+
24
+
25
+ def is_public_key(value: object) -> bool:
26
+ """A disclosure guard, not JWT signature verification."""
27
+ if not isinstance(value, str) or len(value) > MAX_PUBLIC_KEY_LENGTH:
28
+ return False
29
+ if re.fullmatch(r"sb_publishable_[A-Za-z0-9_-]+", value):
30
+ return True
31
+ parts = value.split(".")
32
+ if len(parts) != JWT_PARTS or any(not re.fullmatch(r"[A-Za-z0-9_-]+", part) for part in parts):
33
+ return False
34
+ try:
35
+ payload = json.loads(base64.urlsafe_b64decode(parts[1] + "=" * (-len(parts[1]) % 4)))
36
+ return isinstance(payload, dict) and payload.get("role") == "anon"
37
+ except ValueError, UnicodeError:
38
+ return False
39
+
40
+
41
+ def public_url(value: str) -> str:
42
+ """Reject credentials, fragments and non-HTTPS remote discovery."""
43
+ url = urlsplit(value)
44
+ local = url.hostname in {"localhost", "127.0.0.1", "::1"}
45
+ if (url.scheme != "https" and not (local and url.scheme == "http")) or not url.hostname:
46
+ message = "Invalid public configuration URL"
47
+ raise ValueError(message)
48
+ if url.username or url.password or url.query or url.fragment:
49
+ message = "Invalid public configuration URL"
50
+ raise ValueError(message)
51
+ return url.geturl().rstrip("/")
52
+
53
+
54
+ @lru_cache(maxsize=32)
55
+ def resolve_public_key(web_url: str, supabase_url: str, bundled_key: str, cache_dir: Path) -> str:
56
+ """Resolve once per configured target/process; fall back on discovery failure.
57
+
58
+ No request contains a Supabase API key or a user/session credential.
59
+ The response must name the exact configured database before its key is used.
60
+ """
61
+ key = bundled_key if is_public_key(bundled_key) else ""
62
+ try:
63
+ web = urlsplit(public_url(web_url))
64
+ origin = f"{web.scheme}://{web.netloc}"
65
+ database = public_url(supabase_url)
66
+ except ValueError:
67
+ return key
68
+ cache_id = hashlib.sha256(f"{origin}|{database}".encode()).hexdigest()
69
+ cache_path = cache_dir / f"{cache_id}.json"
70
+ try:
71
+ cached = json.loads(cache_path.read_text())
72
+ if is_public_key(cached):
73
+ key = cached
74
+ except OSError, ValueError:
75
+ pass
76
+ try:
77
+ # No redirects: the configured origin owns key discovery.
78
+ response = httpx.get(f"{origin}/api/addin-config", timeout=3.0, follow_redirects=False)
79
+ response.raise_for_status()
80
+ data = response.json()
81
+ if not isinstance(data, dict) or not isinstance(data.get("supabaseUrl"), str):
82
+ return key
83
+ candidate = data.get("anonKey")
84
+ if public_url(data["supabaseUrl"]) != database or not is_public_key(candidate):
85
+ return key
86
+ key = str(candidate)
87
+ except httpx.HTTPError, ValueError:
88
+ return key
89
+ # Atomic persistence is best-effort and separate from applied memory.
90
+ temporary: Path | None = None
91
+ try:
92
+ cache_dir.mkdir(parents=True, exist_ok=True)
93
+ with tempfile.NamedTemporaryFile(mode="w", encoding="utf-8", dir=cache_dir, delete=False) as output:
94
+ temporary = Path(output.name)
95
+ json.dump(key, output)
96
+ temporary.replace(cache_path)
97
+ except OSError:
98
+ pass
99
+ finally:
100
+ if temporary:
101
+ with contextlib.suppress(OSError):
102
+ temporary.unlink(missing_ok=True)
103
+ return key
104
+
105
+
106
+ def public_key_tags(key: str) -> dict[str, str]:
107
+ """Match @kit/supabase/public-key; no user JWT or secret is fingerprinted."""
108
+ if not is_public_key(key):
109
+ return {"supabase.key_kind": "unknown", "supabase.key_revision": "unknown"}
110
+ fingerprint = 5381
111
+ for char in key:
112
+ fingerprint = ((fingerprint * 33) ^ ord(char)) & 0xFFFFFFFF
113
+ return {
114
+ "supabase.key_kind": "publishable" if key.startswith("sb_publishable_") else "legacy",
115
+ "supabase.key_revision": f"djb2:{fingerprint:x}",
116
+ }
@@ -0,0 +1,31 @@
1
+ """Public deployment identity for a separately distributed client."""
2
+
3
+ from urllib.parse import urlsplit
4
+
5
+ # Keep aligned with packages/monitoring/core/src/identity.ts (parity tested).
6
+ _HOSTS = {
7
+ "app.delos.so": "eu-prod",
8
+ "db.app.delos.so": "eu-prod",
9
+ "us.delos.so": "us-prod",
10
+ "db.app.us.delos.so": "us-prod",
11
+ "ae.delos.so": "uae-prod",
12
+ "db.ae.delos.so": "uae-prod",
13
+ "bouygues.delos.so": "bouygues-prod",
14
+ "db.bouygues.delos.so": "bouygues-prod",
15
+ "beta.delos.so": "eu-staging",
16
+ "db.beta.delos.so": "eu-staging",
17
+ "localhost": "local",
18
+ "127.0.0.1": "local",
19
+ "::1": "local",
20
+ }
21
+
22
+
23
+ def endpoint_identity(endpoint: str) -> str:
24
+ """Never transmit custom URLs, credentials or configuration payloads."""
25
+ try:
26
+ parsed = urlsplit(endpoint)
27
+ if parsed.scheme in {"http", "https"} and parsed.hostname:
28
+ return _HOSTS.get(parsed.hostname, "custom")
29
+ except ValueError:
30
+ pass
31
+ return "unknown"
@@ -8,6 +8,7 @@ a later ``permission_reply`` resolves.
8
8
  Policy (mirrors the extension's previous ``needsPermission`` gate plus the
9
9
  REPL's permission modes):
10
10
 
11
+ * ``auto`` mode (any tool, incl. ``run_shell``) → accept silently
11
12
  * tool previously approved with "always" → accept silently
12
13
  * ``build`` mode + edit/write tool → accept silently
13
14
  * ``plan`` mode + edit/write tool → deny with an explanatory reason
@@ -53,6 +54,8 @@ def make_confirm(server: StdioServer, turn: TurnState) -> ConfirmFn:
53
54
 
54
55
  async def _confirm(body: str, tool: str | None = None) -> ConfirmOutcome:
55
56
  name = tool or ""
57
+ if turn.mode == "auto":
58
+ return ConfirmOutcome(accepted=True)
56
59
  if name in server.always_allowed:
57
60
  return ConfirmOutcome(accepted=True)
58
61
  if name in EDIT_TOOLS:
@@ -22,7 +22,7 @@ from delos_cli.transport.integrations import (
22
22
  fetch_manage_url,
23
23
  set_integration_active,
24
24
  )
25
- from delos_cli.transport.models import fetch_models
25
+ from delos_cli.transport.models import fetch_model_infos, fetch_models
26
26
 
27
27
  if TYPE_CHECKING:
28
28
  from .server import StdioServer
@@ -92,11 +92,13 @@ async def fork_chat(server: StdioServer, params: dict[str, Any]) -> dict[str, An
92
92
 
93
93
 
94
94
  async def find_files(server: StdioServer, params: dict[str, Any]) -> list[str]:
95
- """Fuzzy-ish file lookup: ``rg --files`` + case-insensitive substring filter.
95
+ """File + folder lookup, Cmd+P style: ``rg --files`` + ranked matching.
96
96
 
97
- Replaces the ripgrep spawn the extension used to own; same semantics
98
- (workspace-relative paths, .git/node_modules excluded by rg defaults
99
- plus explicit globs). Falls back to an os.walk scan when rg is absent.
97
+ Workspace-relative paths, .git/node_modules excluded. Folders are
98
+ derived from the file paths and returned with a trailing ``/``.
99
+ Ranking: folders containing the query, folders matching it as a
100
+ subsequence (a, then r, then e), then files likewise. Falls back to an
101
+ os.walk scan when rg is absent.
100
102
  """
101
103
  query = str(params.get("query") or "").strip().lower()
102
104
  max_results = int(params.get("maxResults") or _FIND_FILES_DEFAULT_MAX)
@@ -104,14 +106,37 @@ async def find_files(server: StdioServer, params: dict[str, Any]) -> list[str]:
104
106
  paths = await _rg_files(server.workspace)
105
107
  except FileNotFoundError:
106
108
  paths = await asyncio.to_thread(_walk_files, server.workspace)
107
- matches: list[str] = []
108
- for path in paths:
109
- if query and query not in path.lower():
109
+ return _rank_paths(paths, query, max_results)
110
+
111
+
112
+ def _is_subsequence(query: str, text: str) -> bool:
113
+ it = iter(text)
114
+ return all(ch in it for ch in query)
115
+
116
+
117
+ def _rank_paths(paths: list[str], query: str, max_results: int) -> list[str]:
118
+ folders = sorted({parent.as_posix() for p in paths for parent in Path(p).parents if parent.as_posix() != "."})
119
+ entries = [(f + "/", True) for f in folders] + [(p, False) for p in paths]
120
+
121
+ if not query:
122
+ ordered = [e for e in entries if e[1]] + [e for e in entries if not e[1]]
123
+ return [path for path, _ in ordered[:max_results]]
124
+
125
+ ranked: list[tuple[tuple[int, int, int, int], str]] = []
126
+ for path, is_dir in entries:
127
+ full = path.rstrip("/").lower()
128
+ base = full.rsplit("/", 1)[-1]
129
+ if query in full:
130
+ tier = 0
131
+ elif _is_subsequence(query, full):
132
+ tier = 1
133
+ else:
110
134
  continue
111
- matches.append(path)
112
- if len(matches) >= max_results:
113
- break
114
- return matches
135
+ # Within a tier: basename hits beat directory-part hits, then shorter paths.
136
+ base_hit = 0 if (query in base if tier == 0 else _is_subsequence(query, base)) else 1
137
+ ranked.append(((0 if is_dir else 1, tier, base_hit, len(full)), path))
138
+ ranked.sort()
139
+ return [path for _, path in ranked[:max_results]]
115
140
 
116
141
 
117
142
  async def _rg_files(workspace: Path) -> list[str]:
@@ -148,9 +173,11 @@ def _walk_files(workspace: Path) -> list[str]:
148
173
  return found
149
174
 
150
175
 
151
- async def list_models(server: StdioServer, params: dict[str, Any]) -> list[str]:
152
- """Model ids available to the signed-in user/org (mirrors the REPL picker)."""
153
- _ = params
176
+ async def list_models(server: StdioServer, params: dict[str, Any]) -> list[str] | list[dict[str, Any]]:
177
+ """Models available to the signed-in user/org: ids, or with Gate's copy when ``locale`` is given."""
178
+ locale = params.get("locale")
179
+ if isinstance(locale, str) and locale:
180
+ return await fetch_model_infos(server.require_http(), locale)
154
181
  return await fetch_models(server.require_http())
155
182
 
156
183
 
@@ -201,6 +201,7 @@ class StdioServer:
201
201
  has_project_instructions=bool(self.custom_instructions),
202
202
  )
203
203
  model = message.get("model")
204
+ reasoning_effort = message.get("reasoningEffort")
204
205
  # Uploaded-document refs (Import from Computer / Drive) attached by the
205
206
  # frontend; forwarded to the backend so the agent can read_document them.
206
207
  raw_files = message.get("messageFiles")
@@ -244,6 +245,7 @@ class StdioServer:
244
245
  mode,
245
246
  model if isinstance(model, str) else None,
246
247
  message_files,
248
+ reasoning_effort=reasoning_effort if isinstance(reasoning_effort, str) and reasoning_effort else None,
247
249
  interrupted_content=interrupted_content,
248
250
  edit_turn=edit_turn,
249
251
  ),
@@ -258,6 +260,7 @@ class StdioServer:
258
260
  mode: str,
259
261
  model: str | None,
260
262
  message_files: list[dict[str, Any]],
263
+ reasoning_effort: str | None = None,
261
264
  interrupted_content: str | None = None,
262
265
  edit_turn: int | None = None,
263
266
  ) -> None:
@@ -305,8 +308,12 @@ class StdioServer:
305
308
  # the agent doesn't burn its first tool call on `git status`.
306
309
  if (git_ctx := await collect_git_context(self.workspace)) is not None:
307
310
  body["git_context"] = git_ctx
311
+ if self.custom_instructions:
312
+ body["custom_instructions"] = self.custom_instructions
308
313
  if model:
309
314
  body["options"] = {"model": model}
315
+ if reasoning_effort:
316
+ body.setdefault("options", {})["reasoning_effort"] = reasoning_effort
310
317
  if edit_turn is not None:
311
318
  body["edit_turn"] = edit_turn
312
319
  if message_files:
@@ -0,0 +1,79 @@
1
+ """Telemetry entrypoint; captures unexpected failures and flushes before exit."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ from pathlib import Path
8
+ from typing import TYPE_CHECKING
9
+
10
+ import sentry_sdk
11
+ import truststore
12
+
13
+ from delos_cli import __version__, _editable_source_dir
14
+ from delos_cli.public_key import public_key_tags
15
+ from delos_cli.sentry_identity import endpoint_identity
16
+
17
+ if TYPE_CHECKING:
18
+ from sentry_sdk.client import BaseClient
19
+
20
+ from delos_cli.auth.config import Config
21
+
22
+ _cli_client: BaseClient | None = None
23
+
24
+
25
+ def build_identity() -> dict[str, str]:
26
+ """Read metadata shipped with the wheel, never the caller shell environment."""
27
+ metadata = json.loads(Path(__file__).with_name("_build.json").read_text())
28
+ if metadata.get("version") == __version__ and _editable_source_dir() is None:
29
+ return metadata
30
+ return {"release": f"cosmos-cli@{__version__}"}
31
+
32
+
33
+ def applied_config(cfg: Config) -> None:
34
+ """Reads only the endpoints the active config object will use."""
35
+ if _cli_client is None or sentry_sdk.get_client() is not _cli_client:
36
+ return
37
+ sentry_sdk.set_tags(public_key_tags(cfg.supabase_anon_key))
38
+ sentry_sdk.set_tag("backend.deployment", endpoint_identity(cfg.api_url))
39
+ sentry_sdk.set_tag("database.deployment", endpoint_identity(cfg.supabase_url))
40
+
41
+
42
+ def main() -> None:
43
+ """Keep CLI exit codes and exceptions intact; never collect CLI arguments."""
44
+ global _cli_client # noqa: PLW0603 -- identify our client in a shared process
45
+ # Before any SSLContext exists: every httpx client (and Sentry) then trusts
46
+ # the OS store, so a TLS-inspection proxy's corporate CA is accepted.
47
+ truststore.inject_into_ssl()
48
+ build = build_identity()
49
+ editable = _editable_source_dir() is not None
50
+ disabled = editable or os.environ.get("DELOS_TELEMETRY_DISABLED") == "1"
51
+ sentry_sdk.init(
52
+ dsn=""
53
+ if disabled
54
+ else "https://03a98437d8f510283186ce47f55866d7@o4507940398825472.ingest.de.sentry.io/4512067093463120",
55
+ release=build["release"],
56
+ environment="development" if editable else "production",
57
+ default_integrations=False,
58
+ auto_enabling_integrations=False,
59
+ send_default_pii=False,
60
+ include_local_variables=False,
61
+ max_request_body_size="never",
62
+ )
63
+ _cli_client = sentry_sdk.get_client()
64
+ sentry_sdk.set_tag("service", "cli")
65
+ sentry_sdk.set_tag("delos.release", build["release"])
66
+ sentry_sdk.set_tag("app.version", __version__)
67
+ if sha := build.get("sha"):
68
+ sentry_sdk.set_tag("build.sha", sha)
69
+ sentry_sdk.start_session(session_mode="application")
70
+ try:
71
+ from delos_cli.main import app # noqa: PLC0415
72
+
73
+ app()
74
+ except Exception as exc:
75
+ sentry_sdk.capture_exception(exc)
76
+ raise
77
+ finally:
78
+ sentry_sdk.end_session()
79
+ sentry_sdk.flush(timeout=2)
@@ -51,6 +51,7 @@ async def handle_glob(tool_input: dict[str, Any], ctx: Ctx) -> str: # noqa: RUF
51
51
  return "Error: 'pattern' is required and must be a non-empty string."
52
52
 
53
53
  raw_path = (tool_input.get("path") or ".").strip() or "."
54
+ include_ignored = bool(tool_input.get("include_ignored"))
54
55
  base = Path(raw_path).expanduser() # noqa: ASYNC240 — one-shot stat is fine
55
56
  if not base.is_absolute():
56
57
  base = Path.cwd() / base
@@ -62,12 +63,20 @@ async def handle_glob(tool_input: dict[str, Any], ctx: Ctx) -> str: # noqa: RUF
62
63
 
63
64
  # ``Path.glob`` handles ``**`` recursive segments natively.
64
65
  try:
65
- candidates = [p for p in base.glob(pattern) if not _IGNORED_DIR_SEGMENTS.intersection(p.parts) and p.is_file()]
66
+ matches = base.glob(pattern, recurse_symlinks=True) if include_ignored else base.glob(pattern)
67
+ candidates = [
68
+ p for p in matches if (include_ignored or not _IGNORED_DIR_SEGMENTS.intersection(p.parts)) and p.is_file()
69
+ ]
66
70
  except (OSError, ValueError) as e:
67
71
  return f"Error walking {base} with pattern {pattern!r}: {e}"
68
72
 
69
73
  if not candidates:
70
- return f"No files matching {pattern!r} in {base} (dependency/build trees like node_modules are excluded)."
74
+ suffix = (
75
+ ""
76
+ if include_ignored
77
+ else (" (dependency/build trees like node_modules are excluded — pass include_ignored=true to search them)")
78
+ )
79
+ return f"No files matching {pattern!r} in {base}{suffix}."
71
80
 
72
81
  # Sort by mtime desc; fall back to path on stat errors so a single
73
82
  # vanished file doesn't poison the whole result.
@@ -101,4 +110,7 @@ def render_glob(event: dict[str, Any]) -> RenderableType | None:
101
110
  if not pattern:
102
111
  return None
103
112
  path = inputs.get("path") or "."
104
- return tool_line("→", "List files", f'"{pattern}"', color="cyan", suffix=f"({path})")
113
+ suffix = f"({path})"
114
+ if inputs.get("include_ignored"):
115
+ suffix += " incl. ignored"
116
+ return tool_line("→", "List files", f'"{pattern}"', color="cyan", suffix=suffix)
@@ -69,7 +69,7 @@ def _find_rg() -> str | None:
69
69
  that's the most reliable location — PATH is whatever the spawning app
70
70
  (VSCode, launchd) happened to have.
71
71
  """
72
- bundled = Path(sys.executable).parent / "rg"
72
+ bundled = Path(sys.executable).parent / ("rg.exe" if sys.platform == "win32" else "rg")
73
73
  if os.access(bundled, os.X_OK):
74
74
  return str(bundled)
75
75
  found = shutil.which("rg")
@@ -93,6 +93,7 @@ async def handle_grep(tool_input: dict[str, Any], ctx: Ctx) -> str:
93
93
  type_ = tool_input.get("type")
94
94
  output_mode = (tool_input.get("output_mode") or "content").strip()
95
95
  case_insensitive = bool(tool_input.get("case_insensitive"))
96
+ include_ignored = bool(tool_input.get("include_ignored"))
96
97
  head_limit = _coerce_int(
97
98
  tool_input.get("head_limit"),
98
99
  default=_DEFAULT_HEAD_LIMIT,
@@ -114,6 +115,7 @@ async def handle_grep(tool_input: dict[str, Any], ctx: Ctx) -> str:
114
115
  output_mode=output_mode,
115
116
  case_insensitive=case_insensitive,
116
117
  context_lines=context_lines,
118
+ include_ignored=include_ignored,
117
119
  )
118
120
  else:
119
121
  argv = _build_grep_argv(
@@ -123,6 +125,7 @@ async def handle_grep(tool_input: dict[str, Any], ctx: Ctx) -> str:
123
125
  output_mode=output_mode,
124
126
  case_insensitive=case_insensitive,
125
127
  context_lines=context_lines,
128
+ include_ignored=include_ignored,
126
129
  )
127
130
 
128
131
  try:
@@ -176,6 +179,8 @@ def render_grep(event: dict[str, Any]) -> RenderableType | None:
176
179
  path = inputs.get("path") or "."
177
180
  glob = inputs.get("glob")
178
181
  suffix = f"({path})"
182
+ if inputs.get("include_ignored"):
183
+ suffix += " incl. ignored"
179
184
  if glob:
180
185
  suffix += f" glob: {glob}"
181
186
  return tool_line("→", "Search in files", f'"{pattern}"', color="cyan", suffix=suffix)
@@ -191,9 +196,20 @@ def _build_rg_argv(
191
196
  output_mode: str,
192
197
  case_insensitive: bool,
193
198
  context_lines: int,
199
+ include_ignored: bool,
194
200
  ) -> list[str]:
195
201
  """Compose a ripgrep argv reflecting the tool inputs."""
196
- argv: list[str] = [rg, "--color=never", "--no-heading", "--line-number"]
202
+ argv: list[str] = [
203
+ rg,
204
+ "--color=never",
205
+ "--no-heading",
206
+ "--line-number",
207
+ "--max-columns",
208
+ "500",
209
+ "--max-columns-preview",
210
+ ]
211
+ if include_ignored:
212
+ argv.append("--no-ignore")
197
213
  if case_insensitive:
198
214
  argv.append("-i")
199
215
  if isinstance(glob, str) and glob:
@@ -220,10 +236,12 @@ def _build_grep_argv(
220
236
  output_mode: str,
221
237
  case_insensitive: bool,
222
238
  context_lines: int,
239
+ include_ignored: bool,
223
240
  ) -> list[str]:
224
241
  """Compose a POSIX-grep argv as a fallback when ripgrep isn't installed."""
225
242
  argv: list[str] = ["grep", "-r", "-n", "-I"]
226
- argv.extend(f"--exclude-dir={d}" for d in _EXCLUDED_DIRS)
243
+ excluded_dirs = (".git",) if include_ignored else _EXCLUDED_DIRS
244
+ argv.extend(f"--exclude-dir={d}" for d in excluded_dirs)
227
245
  if case_insensitive:
228
246
  argv.append("-i")
229
247
  if isinstance(glob, str) and glob:
@@ -0,0 +1,50 @@
1
+ """Thin wrapper around ``GET /client/picker/{org_uuid}`` — the plan's ``cli`` picker, in admin order."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING, Any
6
+ from urllib.parse import quote
7
+
8
+ from .client import TransportError
9
+
10
+ if TYPE_CHECKING:
11
+ from .client import AuthedClient
12
+
13
+
14
+ async def fetch_models(client: AuthedClient) -> list[str]:
15
+ """Return model ids available to the signed-in user/org."""
16
+ path = f"/client/picker/{client.cfg.org_uuid}?app=cli"
17
+ payload = await client.json_get(path)
18
+ return [m["id"] for m in payload.get("models", [])]
19
+
20
+
21
+ async def fetch_model_infos(client: AuthedClient, locale: str) -> list[dict[str, Any]]:
22
+ """Return the same models with Gate's title and description in ``locale``, and the provider's logo."""
23
+ path = f"/client/picker/{client.cfg.org_uuid}?app=cli&locale={quote(locale)}"
24
+ payload = await client.json_get(path)
25
+ logos = await _fetch_provider_logos(client)
26
+ return [
27
+ {
28
+ "id": m["id"],
29
+ "title": m.get("title"),
30
+ "description": m.get("description"),
31
+ "icon": logos.get((m.get("provider") or "").lower()),
32
+ }
33
+ for m in payload.get("models", [])
34
+ ]
35
+
36
+
37
+ async def _fetch_provider_logos(client: AuthedClient) -> dict[str, str]:
38
+ """Gate developer id → logo as an image URL; empty when Gate is unreachable (the picker then shows no marks)."""
39
+ try:
40
+ payload = await client.json_get("/client/developers")
41
+ except TransportError:
42
+ return {}
43
+ logos: dict[str, str] = {}
44
+ for developer in payload.get("developers", []):
45
+ logo = (developer.get("logo") or "").strip()
46
+ if not logo:
47
+ continue
48
+ # Gate stores either an inline SVG or a data URI (same rule as the add-ins' `providerIcon`).
49
+ logos[developer["id"].lower()] = f"data:image/svg+xml,{quote(logo)}" if logo.startswith("<") else logo
50
+ return logos
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "delos-cli"
3
- version = "2.0.2"
3
+ version = "5.9.15"
4
4
  description = "Terminal REPL for the Delos agent — the Claude-Code-style CLI for Delos."
5
5
  readme = "README.md"
6
6
  license = { text = "MIT" }
@@ -21,10 +21,15 @@ classifiers = [
21
21
  "Topic :: Utilities",
22
22
  ]
23
23
  dependencies = [
24
+ "sentry-sdk>=2.44.0,<3",
24
25
  "typer>=0.12.5",
25
26
  "rich>=13.8.0",
26
27
  "prompt_toolkit>=3.0.47",
27
28
  "httpx>=0.27.0",
29
+ # Verify TLS against the OS trust store (Windows store, macOS Keychain)
30
+ # instead of certifi alone, so corporate TLS-inspection proxies whose root
31
+ # CA is installed system-wide (Zscaler & co.) do not fail every request.
32
+ "truststore>=0.10.0",
28
33
  "questionary>=2.0.1",
29
34
  # Real ripgrep binary shipped as a wheel — the grep/glob tools use it for
30
35
  # fast, .gitignore-aware search. Markers limit it to the platforms that
@@ -33,13 +38,17 @@ dependencies = [
33
38
  # manylinux_2_39 (glibc >= 2.39), so on older distros (Ubuntu 22.04,
34
39
  # Debian 12, ...) resolvers fall back to the sdist, which fails to build
35
40
  # because its Cargo.toml needs the edition2024 feature. 14.x still ships
36
- # a manylinux2014 (glibc >= 2.17) wheel that works everywhere.
37
- "ripgrep>=14,<15 ; sys_platform == 'darwin' and platform_machine == 'arm64'",
38
- "ripgrep>=14,<15 ; sys_platform == 'linux' and platform_machine == 'x86_64'",
41
+ # a manylinux2014 (glibc >= 2.17) wheel that works everywhere. Windows has
42
+ # no POSIX grep to fall back on, so its wheel (x64 only) is what makes the
43
+ # tools work there at all. Windows x64 reports AMD64; uv's cross-platform
44
+ # resolution (the extension's bundled runtime) reports x86_64.
45
+ "ripgrep>=14,<15 ; sys_platform == 'darwin'",
46
+ "ripgrep>=14,<15 ; sys_platform == 'linux' and (platform_machine == 'x86_64' or platform_machine == 'aarch64')",
47
+ "ripgrep>=14,<15 ; sys_platform == 'win32' and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
39
48
  ]
40
49
 
41
50
  [project.scripts]
42
- delos = "delos_cli.main:app"
51
+ delos = "delos_cli.telemetry:main"
43
52
 
44
53
  [project.urls]
45
54
  Homepage = "https://delos.so"
@@ -50,11 +59,11 @@ Issues = "https://github.com/Delos-Intelligence/cosmos-saas/issues"
50
59
  requires = ["hatchling"]
51
60
  build-backend = "hatchling.build"
52
61
 
62
+ [tool.hatch.build]
63
+ exclude = [
64
+ "/bench/.toolchain",
65
+ "/bench/fixture/.ruff_cache",
66
+ ]
67
+
53
68
  [tool.hatch.build.targets.wheel]
54
69
  packages = ["delos_cli"]
55
-
56
- [tool.hatch.build.targets.sdist]
57
- # hatchling only reads the .gitignore next to this pyproject, not the repo
58
- # root's — so bench/.toolchain (a venv with absolute symlinks) would end up in
59
- # the sdist and break `uv build`. List what ships instead.
60
- include = ["delos_cli", "README.md", "pyproject.toml"]
@@ -1,19 +0,0 @@
1
- """Thin wrapper around ``GET /client/models/{org_uuid}``.
2
-
3
- Mirrors the data the web frontend uses for its chat model picker —
4
- filtered server-side by sigma flag, deployment plan, and config fallbacks.
5
- """
6
-
7
- from __future__ import annotations
8
-
9
- from typing import TYPE_CHECKING
10
-
11
- if TYPE_CHECKING:
12
- from .client import AuthedClient
13
-
14
-
15
- async def fetch_models(client: AuthedClient) -> list[str]:
16
- """Return model ids available to the signed-in user/org."""
17
- path = f"/client/models/{client.cfg.org_uuid}?capability=cli"
18
- payload = await client.json_get(path)
19
- return [m["id"] for m in payload.get("models", [])]
File without changes
File without changes
File without changes
File without changes
File without changes