delos-cli 2.0.2__tar.gz → 2.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- delos_cli-2.1.0/.claude/settings.local.json +8 -0
- {delos_cli-2.0.2 → delos_cli-2.1.0}/PKG-INFO +2 -1
- delos_cli-2.1.0/delos_cli/_build.json +1 -0
- {delos_cli-2.0.2 → delos_cli-2.1.0}/delos_cli/agent/session.py +13 -3
- {delos_cli-2.0.2 → delos_cli-2.1.0}/delos_cli/apps/chat/render.py +22 -0
- {delos_cli-2.0.2 → delos_cli-2.1.0}/delos_cli/auth/config.py +14 -2
- delos_cli-2.1.0/delos_cli/public_key.py +116 -0
- delos_cli-2.1.0/delos_cli/sentry_identity.py +31 -0
- {delos_cli-2.0.2 → delos_cli-2.1.0}/delos_cli/serve/server.py +7 -0
- delos_cli-2.1.0/delos_cli/telemetry.py +75 -0
- {delos_cli-2.0.2 → delos_cli-2.1.0}/delos_cli/tools/glob_tool.py +24 -3
- {delos_cli-2.0.2 → delos_cli-2.1.0}/delos_cli/tools/grep.py +20 -2
- {delos_cli-2.0.2 → delos_cli-2.1.0}/pyproject.toml +9 -8
- {delos_cli-2.0.2 → delos_cli-2.1.0}/.gitignore +0 -0
- {delos_cli-2.0.2 → delos_cli-2.1.0}/README.md +0 -0
- {delos_cli-2.0.2 → delos_cli-2.1.0}/delos_cli/__init__.py +0 -0
- {delos_cli-2.0.2 → delos_cli-2.1.0}/delos_cli/agent/__init__.py +0 -0
- {delos_cli-2.0.2 → delos_cli-2.1.0}/delos_cli/agent/tools.py +0 -0
- {delos_cli-2.0.2 → delos_cli-2.1.0}/delos_cli/agent/transport.py +0 -0
- {delos_cli-2.0.2 → delos_cli-2.1.0}/delos_cli/agent/turn.py +0 -0
- {delos_cli-2.0.2 → delos_cli-2.1.0}/delos_cli/apps/__init__.py +0 -0
- {delos_cli-2.0.2 → delos_cli-2.1.0}/delos_cli/apps/base.py +0 -0
- {delos_cli-2.0.2 → delos_cli-2.1.0}/delos_cli/apps/chat/__init__.py +0 -0
- {delos_cli-2.0.2 → delos_cli-2.1.0}/delos_cli/apps/chat/app.py +0 -0
- {delos_cli-2.0.2 → delos_cli-2.1.0}/delos_cli/apps/chat/commands.py +0 -0
- {delos_cli-2.0.2 → delos_cli-2.1.0}/delos_cli/apps/replay.py +0 -0
- {delos_cli-2.0.2 → delos_cli-2.1.0}/delos_cli/apps/scribe/__init__.py +0 -0
- {delos_cli-2.0.2 → delos_cli-2.1.0}/delos_cli/apps/scribe/app.py +0 -0
- {delos_cli-2.0.2 → delos_cli-2.1.0}/delos_cli/apps/scribe/commands.py +0 -0
- {delos_cli-2.0.2 → delos_cli-2.1.0}/delos_cli/apps/scribe/tools.py +0 -0
- {delos_cli-2.0.2 → delos_cli-2.1.0}/delos_cli/auth/__init__.py +0 -0
- {delos_cli-2.0.2 → delos_cli-2.1.0}/delos_cli/auth/mfa.py +0 -0
- {delos_cli-2.0.2 → delos_cli-2.1.0}/delos_cli/auth/oauth.py +0 -0
- {delos_cli-2.0.2 → delos_cli-2.1.0}/delos_cli/auth/token_manager.py +0 -0
- {delos_cli-2.0.2 → delos_cli-2.1.0}/delos_cli/commands/__init__.py +0 -0
- {delos_cli-2.0.2 → delos_cli-2.1.0}/delos_cli/commands/base.py +0 -0
- {delos_cli-2.0.2 → delos_cli-2.1.0}/delos_cli/commands/builtin.py +0 -0
- {delos_cli-2.0.2 → delos_cli-2.1.0}/delos_cli/commands/prompts.py +0 -0
- {delos_cli-2.0.2 → delos_cli-2.1.0}/delos_cli/context_report.py +0 -0
- {delos_cli-2.0.2 → delos_cli-2.1.0}/delos_cli/ctx.py +0 -0
- {delos_cli-2.0.2 → delos_cli-2.1.0}/delos_cli/git_context.py +0 -0
- {delos_cli-2.0.2 → delos_cli-2.1.0}/delos_cli/loop.py +0 -0
- {delos_cli-2.0.2 → delos_cli-2.1.0}/delos_cli/main.py +0 -0
- {delos_cli-2.0.2 → delos_cli-2.1.0}/delos_cli/project_context.py +0 -0
- {delos_cli-2.0.2 → delos_cli-2.1.0}/delos_cli/serve/__init__.py +0 -0
- {delos_cli-2.0.2 → delos_cli-2.1.0}/delos_cli/serve/app.py +0 -0
- {delos_cli-2.0.2 → delos_cli-2.1.0}/delos_cli/serve/confirm.py +0 -0
- {delos_cli-2.0.2 → delos_cli-2.1.0}/delos_cli/serve/protocol.py +0 -0
- {delos_cli-2.0.2 → delos_cli-2.1.0}/delos_cli/serve/rpc.py +0 -0
- {delos_cli-2.0.2 → delos_cli-2.1.0}/delos_cli/state.py +0 -0
- {delos_cli-2.0.2 → delos_cli-2.1.0}/delos_cli/tools/__init__.py +0 -0
- {delos_cli-2.0.2 → delos_cli-2.1.0}/delos_cli/tools/ask_user_input.py +0 -0
- {delos_cli-2.0.2 → delos_cli-2.1.0}/delos_cli/tools/edit_content.py +0 -0
- {delos_cli-2.0.2 → delos_cli-2.1.0}/delos_cli/tools/explore.py +0 -0
- {delos_cli-2.0.2 → delos_cli-2.1.0}/delos_cli/tools/read.py +0 -0
- {delos_cli-2.0.2 → delos_cli-2.1.0}/delos_cli/tools/run_shell.py +0 -0
- {delos_cli-2.0.2 → delos_cli-2.1.0}/delos_cli/tools/task.py +0 -0
- {delos_cli-2.0.2 → delos_cli-2.1.0}/delos_cli/tools/todo_write.py +0 -0
- {delos_cli-2.0.2 → delos_cli-2.1.0}/delos_cli/tools/verify.py +0 -0
- {delos_cli-2.0.2 → delos_cli-2.1.0}/delos_cli/tools/write_content.py +0 -0
- {delos_cli-2.0.2 → delos_cli-2.1.0}/delos_cli/transport/__init__.py +0 -0
- {delos_cli-2.0.2 → delos_cli-2.1.0}/delos_cli/transport/chats.py +0 -0
- {delos_cli-2.0.2 → delos_cli-2.1.0}/delos_cli/transport/client.py +0 -0
- {delos_cli-2.0.2 → delos_cli-2.1.0}/delos_cli/transport/documents.py +0 -0
- {delos_cli-2.0.2 → delos_cli-2.1.0}/delos_cli/transport/integrations.py +0 -0
- {delos_cli-2.0.2 → delos_cli-2.1.0}/delos_cli/transport/models.py +0 -0
- {delos_cli-2.0.2 → delos_cli-2.1.0}/delos_cli/ui/__init__.py +0 -0
- {delos_cli-2.0.2 → delos_cli-2.1.0}/delos_cli/ui/banner.py +0 -0
- {delos_cli-2.0.2 → delos_cli-2.1.0}/delos_cli/ui/chat_picker.py +0 -0
- {delos_cli-2.0.2 → delos_cli-2.1.0}/delos_cli/ui/collapse.py +0 -0
- {delos_cli-2.0.2 → delos_cli-2.1.0}/delos_cli/ui/completer.py +0 -0
- {delos_cli-2.0.2 → delos_cli-2.1.0}/delos_cli/ui/document_picker.py +0 -0
- {delos_cli-2.0.2 → delos_cli-2.1.0}/delos_cli/ui/integrations_picker.py +0 -0
- {delos_cli-2.0.2 → delos_cli-2.1.0}/delos_cli/ui/lexer.py +0 -0
- {delos_cli-2.0.2 → delos_cli-2.1.0}/delos_cli/ui/model_picker.py +0 -0
- {delos_cli-2.0.2 → delos_cli-2.1.0}/delos_cli/ui/output.py +0 -0
- {delos_cli-2.0.2 → delos_cli-2.1.0}/delos_cli/ui/repl.py +0 -0
- {delos_cli-2.0.2 → delos_cli-2.1.0}/delos_cli/ui/style.py +0 -0
- {delos_cli-2.0.2 → delos_cli-2.1.0}/delos_cli/usage_report.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: delos-cli
|
|
3
|
-
Version: 2.0
|
|
3
|
+
Version: 2.1.0
|
|
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
|
|
@@ -26,6 +26,7 @@ Requires-Dist: questionary>=2.0.1
|
|
|
26
26
|
Requires-Dist: rich>=13.8.0
|
|
27
27
|
Requires-Dist: ripgrep<15,>=14; sys_platform == 'darwin' and platform_machine == 'arm64'
|
|
28
28
|
Requires-Dist: ripgrep<15,>=14; sys_platform == 'linux' and platform_machine == 'x86_64'
|
|
29
|
+
Requires-Dist: sentry-sdk<3,>=2.44.0
|
|
29
30
|
Requires-Dist: typer>=0.12.5
|
|
30
31
|
Description-Content-Type: text/markdown
|
|
31
32
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{}
|
|
@@ -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:
|
|
@@ -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
|
|
@@ -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
|
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""Public key discovery; keep in sync with cvn/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"
|
|
@@ -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,75 @@
|
|
|
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
|
+
|
|
12
|
+
from delos_cli import __version__, _editable_source_dir
|
|
13
|
+
from delos_cli.public_key import public_key_tags
|
|
14
|
+
from delos_cli.sentry_identity import endpoint_identity
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from sentry_sdk.client import BaseClient
|
|
18
|
+
|
|
19
|
+
from delos_cli.auth.config import Config
|
|
20
|
+
|
|
21
|
+
_cli_client: BaseClient | None = None
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def build_identity() -> dict[str, str]:
|
|
25
|
+
"""Read metadata shipped with the wheel, never the caller shell environment."""
|
|
26
|
+
metadata = json.loads(Path(__file__).with_name("_build.json").read_text())
|
|
27
|
+
if metadata.get("version") == __version__ and _editable_source_dir() is None:
|
|
28
|
+
return metadata
|
|
29
|
+
return {"release": f"cosmos-cli@{__version__}"}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def applied_config(cfg: Config) -> None:
|
|
33
|
+
"""Reads only the endpoints the active config object will use."""
|
|
34
|
+
if _cli_client is None or sentry_sdk.get_client() is not _cli_client:
|
|
35
|
+
return
|
|
36
|
+
sentry_sdk.set_tags(public_key_tags(cfg.supabase_anon_key))
|
|
37
|
+
sentry_sdk.set_tag("backend.deployment", endpoint_identity(cfg.api_url))
|
|
38
|
+
sentry_sdk.set_tag("database.deployment", endpoint_identity(cfg.supabase_url))
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def main() -> None:
|
|
42
|
+
"""Keep CLI exit codes and exceptions intact; never collect CLI arguments."""
|
|
43
|
+
global _cli_client # noqa: PLW0603 -- identify our client in a shared process
|
|
44
|
+
build = build_identity()
|
|
45
|
+
editable = _editable_source_dir() is not None
|
|
46
|
+
disabled = editable or os.environ.get("DELOS_TELEMETRY_DISABLED") == "1"
|
|
47
|
+
sentry_sdk.init(
|
|
48
|
+
dsn=""
|
|
49
|
+
if disabled
|
|
50
|
+
else "https://03a98437d8f510283186ce47f55866d7@o4507940398825472.ingest.de.sentry.io/4512067093463120",
|
|
51
|
+
release=build["release"],
|
|
52
|
+
environment="development" if editable else "production",
|
|
53
|
+
default_integrations=False,
|
|
54
|
+
auto_enabling_integrations=False,
|
|
55
|
+
send_default_pii=False,
|
|
56
|
+
include_local_variables=False,
|
|
57
|
+
max_request_body_size="never",
|
|
58
|
+
)
|
|
59
|
+
_cli_client = sentry_sdk.get_client()
|
|
60
|
+
sentry_sdk.set_tag("service", "cli")
|
|
61
|
+
sentry_sdk.set_tag("delos.release", build["release"])
|
|
62
|
+
sentry_sdk.set_tag("app.version", __version__)
|
|
63
|
+
if sha := build.get("sha"):
|
|
64
|
+
sentry_sdk.set_tag("build.sha", sha)
|
|
65
|
+
sentry_sdk.start_session(session_mode="application")
|
|
66
|
+
try:
|
|
67
|
+
from delos_cli.main import app # noqa: PLC0415
|
|
68
|
+
|
|
69
|
+
app()
|
|
70
|
+
except Exception as exc:
|
|
71
|
+
sentry_sdk.capture_exception(exc)
|
|
72
|
+
raise
|
|
73
|
+
finally:
|
|
74
|
+
sentry_sdk.end_session()
|
|
75
|
+
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,29 @@ 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 = (
|
|
67
|
+
base.glob(pattern, recurse_symlinks=True)
|
|
68
|
+
if include_ignored
|
|
69
|
+
else base.glob(pattern)
|
|
70
|
+
)
|
|
71
|
+
candidates = [
|
|
72
|
+
p
|
|
73
|
+
for p in matches
|
|
74
|
+
if (include_ignored or not _IGNORED_DIR_SEGMENTS.intersection(p.parts)) and p.is_file()
|
|
75
|
+
]
|
|
66
76
|
except (OSError, ValueError) as e:
|
|
67
77
|
return f"Error walking {base} with pattern {pattern!r}: {e}"
|
|
68
78
|
|
|
69
79
|
if not candidates:
|
|
70
|
-
|
|
80
|
+
suffix = (
|
|
81
|
+
""
|
|
82
|
+
if include_ignored
|
|
83
|
+
else (
|
|
84
|
+
" (dependency/build trees like node_modules are excluded — "
|
|
85
|
+
"pass include_ignored=true to search them)"
|
|
86
|
+
)
|
|
87
|
+
)
|
|
88
|
+
return f"No files matching {pattern!r} in {base}{suffix}."
|
|
71
89
|
|
|
72
90
|
# Sort by mtime desc; fall back to path on stat errors so a single
|
|
73
91
|
# vanished file doesn't poison the whole result.
|
|
@@ -101,4 +119,7 @@ def render_glob(event: dict[str, Any]) -> RenderableType | None:
|
|
|
101
119
|
if not pattern:
|
|
102
120
|
return None
|
|
103
121
|
path = inputs.get("path") or "."
|
|
104
|
-
|
|
122
|
+
suffix = f"({path})"
|
|
123
|
+
if inputs.get("include_ignored"):
|
|
124
|
+
suffix += " incl. ignored"
|
|
125
|
+
return tool_line("→", "List files", f'"{pattern}"', color="cyan", suffix=suffix)
|
|
@@ -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:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "delos-cli"
|
|
3
|
-
version = "2.0
|
|
3
|
+
version = "2.1.0"
|
|
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,6 +21,7 @@ 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",
|
|
@@ -39,7 +40,7 @@ dependencies = [
|
|
|
39
40
|
]
|
|
40
41
|
|
|
41
42
|
[project.scripts]
|
|
42
|
-
delos = "delos_cli.main
|
|
43
|
+
delos = "delos_cli.telemetry:main"
|
|
43
44
|
|
|
44
45
|
[project.urls]
|
|
45
46
|
Homepage = "https://delos.so"
|
|
@@ -50,11 +51,11 @@ Issues = "https://github.com/Delos-Intelligence/cosmos-saas/issues"
|
|
|
50
51
|
requires = ["hatchling"]
|
|
51
52
|
build-backend = "hatchling.build"
|
|
52
53
|
|
|
54
|
+
[tool.hatch.build]
|
|
55
|
+
exclude = [
|
|
56
|
+
"/bench/.toolchain",
|
|
57
|
+
"/bench/fixture/.ruff_cache",
|
|
58
|
+
]
|
|
59
|
+
|
|
53
60
|
[tool.hatch.build.targets.wheel]
|
|
54
61
|
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"]
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|