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.
- {delos_cli-2.0.2 → delos_cli-5.9.15}/.gitignore +32 -10
- {delos_cli-2.0.2 → delos_cli-5.9.15}/PKG-INFO +6 -3
- delos_cli-5.9.15/delos_cli/__main__.py +10 -0
- delos_cli-5.9.15/delos_cli/_build.json +5 -0
- {delos_cli-2.0.2 → delos_cli-5.9.15}/delos_cli/agent/session.py +13 -3
- {delos_cli-2.0.2 → delos_cli-5.9.15}/delos_cli/apps/chat/render.py +22 -0
- {delos_cli-2.0.2 → delos_cli-5.9.15}/delos_cli/auth/config.py +49 -9
- {delos_cli-2.0.2 → delos_cli-5.9.15}/delos_cli/commands/prompts.py +2 -2
- delos_cli-5.9.15/delos_cli/public_key.py +116 -0
- delos_cli-5.9.15/delos_cli/sentry_identity.py +31 -0
- {delos_cli-2.0.2 → delos_cli-5.9.15}/delos_cli/serve/confirm.py +3 -0
- {delos_cli-2.0.2 → delos_cli-5.9.15}/delos_cli/serve/rpc.py +42 -15
- {delos_cli-2.0.2 → delos_cli-5.9.15}/delos_cli/serve/server.py +7 -0
- delos_cli-5.9.15/delos_cli/telemetry.py +79 -0
- {delos_cli-2.0.2 → delos_cli-5.9.15}/delos_cli/tools/glob_tool.py +15 -3
- {delos_cli-2.0.2 → delos_cli-5.9.15}/delos_cli/tools/grep.py +21 -3
- delos_cli-5.9.15/delos_cli/transport/models.py +50 -0
- {delos_cli-2.0.2 → delos_cli-5.9.15}/pyproject.toml +20 -11
- delos_cli-2.0.2/delos_cli/transport/models.py +0 -19
- {delos_cli-2.0.2 → delos_cli-5.9.15}/README.md +0 -0
- {delos_cli-2.0.2 → delos_cli-5.9.15}/delos_cli/__init__.py +0 -0
- {delos_cli-2.0.2 → delos_cli-5.9.15}/delos_cli/agent/__init__.py +0 -0
- {delos_cli-2.0.2 → delos_cli-5.9.15}/delos_cli/agent/tools.py +0 -0
- {delos_cli-2.0.2 → delos_cli-5.9.15}/delos_cli/agent/transport.py +0 -0
- {delos_cli-2.0.2 → delos_cli-5.9.15}/delos_cli/agent/turn.py +0 -0
- {delos_cli-2.0.2 → delos_cli-5.9.15}/delos_cli/apps/__init__.py +0 -0
- {delos_cli-2.0.2 → delos_cli-5.9.15}/delos_cli/apps/base.py +0 -0
- {delos_cli-2.0.2 → delos_cli-5.9.15}/delos_cli/apps/chat/__init__.py +0 -0
- {delos_cli-2.0.2 → delos_cli-5.9.15}/delos_cli/apps/chat/app.py +0 -0
- {delos_cli-2.0.2 → delos_cli-5.9.15}/delos_cli/apps/chat/commands.py +0 -0
- {delos_cli-2.0.2 → delos_cli-5.9.15}/delos_cli/apps/replay.py +0 -0
- {delos_cli-2.0.2 → delos_cli-5.9.15}/delos_cli/apps/scribe/__init__.py +0 -0
- {delos_cli-2.0.2 → delos_cli-5.9.15}/delos_cli/apps/scribe/app.py +0 -0
- {delos_cli-2.0.2 → delos_cli-5.9.15}/delos_cli/apps/scribe/commands.py +0 -0
- {delos_cli-2.0.2 → delos_cli-5.9.15}/delos_cli/apps/scribe/tools.py +0 -0
- {delos_cli-2.0.2 → delos_cli-5.9.15}/delos_cli/auth/__init__.py +0 -0
- {delos_cli-2.0.2 → delos_cli-5.9.15}/delos_cli/auth/mfa.py +0 -0
- {delos_cli-2.0.2 → delos_cli-5.9.15}/delos_cli/auth/oauth.py +0 -0
- {delos_cli-2.0.2 → delos_cli-5.9.15}/delos_cli/auth/token_manager.py +0 -0
- {delos_cli-2.0.2 → delos_cli-5.9.15}/delos_cli/commands/__init__.py +0 -0
- {delos_cli-2.0.2 → delos_cli-5.9.15}/delos_cli/commands/base.py +0 -0
- {delos_cli-2.0.2 → delos_cli-5.9.15}/delos_cli/commands/builtin.py +0 -0
- {delos_cli-2.0.2 → delos_cli-5.9.15}/delos_cli/context_report.py +0 -0
- {delos_cli-2.0.2 → delos_cli-5.9.15}/delos_cli/ctx.py +0 -0
- {delos_cli-2.0.2 → delos_cli-5.9.15}/delos_cli/git_context.py +0 -0
- {delos_cli-2.0.2 → delos_cli-5.9.15}/delos_cli/loop.py +0 -0
- {delos_cli-2.0.2 → delos_cli-5.9.15}/delos_cli/main.py +0 -0
- {delos_cli-2.0.2 → delos_cli-5.9.15}/delos_cli/project_context.py +0 -0
- {delos_cli-2.0.2 → delos_cli-5.9.15}/delos_cli/serve/__init__.py +0 -0
- {delos_cli-2.0.2 → delos_cli-5.9.15}/delos_cli/serve/app.py +0 -0
- {delos_cli-2.0.2 → delos_cli-5.9.15}/delos_cli/serve/protocol.py +0 -0
- {delos_cli-2.0.2 → delos_cli-5.9.15}/delos_cli/state.py +0 -0
- {delos_cli-2.0.2 → delos_cli-5.9.15}/delos_cli/tools/__init__.py +0 -0
- {delos_cli-2.0.2 → delos_cli-5.9.15}/delos_cli/tools/ask_user_input.py +0 -0
- {delos_cli-2.0.2 → delos_cli-5.9.15}/delos_cli/tools/edit_content.py +0 -0
- {delos_cli-2.0.2 → delos_cli-5.9.15}/delos_cli/tools/explore.py +0 -0
- {delos_cli-2.0.2 → delos_cli-5.9.15}/delos_cli/tools/read.py +0 -0
- {delos_cli-2.0.2 → delos_cli-5.9.15}/delos_cli/tools/run_shell.py +0 -0
- {delos_cli-2.0.2 → delos_cli-5.9.15}/delos_cli/tools/task.py +0 -0
- {delos_cli-2.0.2 → delos_cli-5.9.15}/delos_cli/tools/todo_write.py +0 -0
- {delos_cli-2.0.2 → delos_cli-5.9.15}/delos_cli/tools/verify.py +0 -0
- {delos_cli-2.0.2 → delos_cli-5.9.15}/delos_cli/tools/write_content.py +0 -0
- {delos_cli-2.0.2 → delos_cli-5.9.15}/delos_cli/transport/__init__.py +0 -0
- {delos_cli-2.0.2 → delos_cli-5.9.15}/delos_cli/transport/chats.py +0 -0
- {delos_cli-2.0.2 → delos_cli-5.9.15}/delos_cli/transport/client.py +0 -0
- {delos_cli-2.0.2 → delos_cli-5.9.15}/delos_cli/transport/documents.py +0 -0
- {delos_cli-2.0.2 → delos_cli-5.9.15}/delos_cli/transport/integrations.py +0 -0
- {delos_cli-2.0.2 → delos_cli-5.9.15}/delos_cli/ui/__init__.py +0 -0
- {delos_cli-2.0.2 → delos_cli-5.9.15}/delos_cli/ui/banner.py +0 -0
- {delos_cli-2.0.2 → delos_cli-5.9.15}/delos_cli/ui/chat_picker.py +0 -0
- {delos_cli-2.0.2 → delos_cli-5.9.15}/delos_cli/ui/collapse.py +0 -0
- {delos_cli-2.0.2 → delos_cli-5.9.15}/delos_cli/ui/completer.py +0 -0
- {delos_cli-2.0.2 → delos_cli-5.9.15}/delos_cli/ui/document_picker.py +0 -0
- {delos_cli-2.0.2 → delos_cli-5.9.15}/delos_cli/ui/integrations_picker.py +0 -0
- {delos_cli-2.0.2 → delos_cli-5.9.15}/delos_cli/ui/lexer.py +0 -0
- {delos_cli-2.0.2 → delos_cli-5.9.15}/delos_cli/ui/model_picker.py +0 -0
- {delos_cli-2.0.2 → delos_cli-5.9.15}/delos_cli/ui/output.py +0 -0
- {delos_cli-2.0.2 → delos_cli-5.9.15}/delos_cli/ui/repl.py +0 -0
- {delos_cli-2.0.2 → delos_cli-5.9.15}/delos_cli/ui/style.py +0 -0
- {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
|
-
|
|
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.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'
|
|
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()
|
|
@@ -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
|
|
60
|
-
|
|
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
|
-
|
|
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 = "
|
|
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 = "
|
|
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
|
|
191
|
-
|
|
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
|
-
|
|
344
|
-
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.
|
|
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
|
-
|
|
389
|
+
_lock(f)
|
|
350
390
|
yield
|
|
351
391
|
finally:
|
|
352
392
|
with contextlib.suppress(OSError):
|
|
353
|
-
|
|
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
|
-
"""
|
|
95
|
+
"""File + folder lookup, Cmd+P style: ``rg --files`` + ranked matching.
|
|
96
96
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
|
|
112
|
-
if
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
"""
|
|
153
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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] = [
|
|
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
|
-
|
|
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 = "
|
|
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
|
-
|
|
38
|
-
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|