euron-code 0.4.0__tar.gz → 0.4.1__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.
- {euron_code-0.4.0 → euron_code-0.4.1}/CHANGELOG.md +9 -0
- {euron_code-0.4.0 → euron_code-0.4.1}/PKG-INFO +16 -18
- {euron_code-0.4.0 → euron_code-0.4.1}/README.md +15 -17
- {euron_code-0.4.0 → euron_code-0.4.1}/pyproject.toml +1 -1
- {euron_code-0.4.0 → euron_code-0.4.1}/src/euron_code/__init__.py +1 -1
- {euron_code-0.4.0 → euron_code-0.4.1}/src/euron_code/agent.py +3 -1
- {euron_code-0.4.0 → euron_code-0.4.1}/src/euron_code/api.py +3 -2
- {euron_code-0.4.0 → euron_code-0.4.1}/src/euron_code/attachments.py +1 -1
- {euron_code-0.4.0 → euron_code-0.4.1}/src/euron_code/config.py +1 -1
- {euron_code-0.4.0 → euron_code-0.4.1}/src/euron_code/doctor.py +88 -88
- {euron_code-0.4.0 → euron_code-0.4.1}/src/euron_code/repl.py +8 -1
- euron_code-0.4.1/src/euron_code/textsafe.py +41 -0
- {euron_code-0.4.0 → euron_code-0.4.1}/src/euron_code/usage_view.py +1 -1
- {euron_code-0.4.0 → euron_code-0.4.1}/tests/test_api_shell.py +160 -160
- {euron_code-0.4.0 → euron_code-0.4.1}/tests/test_permissions.py +91 -91
- euron_code-0.4.1/tests/test_textsafe.py +45 -0
- {euron_code-0.4.0 → euron_code-0.4.1}/tests/test_tools_fs.py +143 -143
- {euron_code-0.4.0 → euron_code-0.4.1}/tests/test_v4.py +3 -2
- {euron_code-0.4.0 → euron_code-0.4.1}/.gitignore +0 -0
- {euron_code-0.4.0 → euron_code-0.4.1}/LICENSE +0 -0
- {euron_code-0.4.0 → euron_code-0.4.1}/src/euron_code/__main__.py +0 -0
- {euron_code-0.4.0 → euron_code-0.4.1}/src/euron_code/activity.py +0 -0
- {euron_code-0.4.0 → euron_code-0.4.1}/src/euron_code/auth.py +0 -0
- {euron_code-0.4.0 → euron_code-0.4.1}/src/euron_code/builtin_skills/explain-codebase/SKILL.md +0 -0
- {euron_code-0.4.0 → euron_code-0.4.1}/src/euron_code/builtin_skills/review/SKILL.md +0 -0
- {euron_code-0.4.0 → euron_code-0.4.1}/src/euron_code/builtin_skills/test-writer/SKILL.md +0 -0
- {euron_code-0.4.0 → euron_code-0.4.1}/src/euron_code/cache.py +0 -0
- {euron_code-0.4.0 → euron_code-0.4.1}/src/euron_code/checkpoints.py +0 -0
- {euron_code-0.4.0 → euron_code-0.4.1}/src/euron_code/cli.py +0 -0
- {euron_code-0.4.0 → euron_code-0.4.1}/src/euron_code/context.py +0 -0
- {euron_code-0.4.0 → euron_code-0.4.1}/src/euron_code/contextforge/LICENSE +0 -0
- {euron_code-0.4.0 → euron_code-0.4.1}/src/euron_code/contextforge/__init__.py +0 -0
- {euron_code-0.4.0 → euron_code-0.4.1}/src/euron_code/contextforge/text.py +0 -0
- {euron_code-0.4.0 → euron_code-0.4.1}/src/euron_code/contextforge/toolout.py +0 -0
- {euron_code-0.4.0 → euron_code-0.4.1}/src/euron_code/credentials.py +0 -0
- {euron_code-0.4.0 → euron_code-0.4.1}/src/euron_code/documents.py +0 -0
- {euron_code-0.4.0 → euron_code-0.4.1}/src/euron_code/frontmatter.py +0 -0
- {euron_code-0.4.0 → euron_code-0.4.1}/src/euron_code/hooks.py +0 -0
- {euron_code-0.4.0 → euron_code-0.4.1}/src/euron_code/keys.py +0 -0
- {euron_code-0.4.0 → euron_code-0.4.1}/src/euron_code/memory.py +0 -0
- {euron_code-0.4.0 → euron_code-0.4.1}/src/euron_code/permissions.py +0 -0
- {euron_code-0.4.0 → euron_code-0.4.1}/src/euron_code/project_setup.py +0 -0
- {euron_code-0.4.0 → euron_code-0.4.1}/src/euron_code/prompts.py +0 -0
- {euron_code-0.4.0 → euron_code-0.4.1}/src/euron_code/repomap.py +0 -0
- {euron_code-0.4.0 → euron_code-0.4.1}/src/euron_code/rules.py +0 -0
- {euron_code-0.4.0 → euron_code-0.4.1}/src/euron_code/sessions.py +0 -0
- {euron_code-0.4.0 → euron_code-0.4.1}/src/euron_code/skills.py +0 -0
- {euron_code-0.4.0 → euron_code-0.4.1}/src/euron_code/tools/__init__.py +0 -0
- {euron_code-0.4.0 → euron_code-0.4.1}/src/euron_code/tools/background.py +0 -0
- {euron_code-0.4.0 → euron_code-0.4.1}/src/euron_code/tools/base.py +0 -0
- {euron_code-0.4.0 → euron_code-0.4.1}/src/euron_code/tools/db.py +0 -0
- {euron_code-0.4.0 → euron_code-0.4.1}/src/euron_code/tools/document.py +0 -0
- {euron_code-0.4.0 → euron_code-0.4.1}/src/euron_code/tools/fs.py +0 -0
- {euron_code-0.4.0 → euron_code-0.4.1}/src/euron_code/tools/ignore.py +0 -0
- {euron_code-0.4.0 → euron_code-0.4.1}/src/euron_code/tools/reuse.py +0 -0
- {euron_code-0.4.0 → euron_code-0.4.1}/src/euron_code/tools/search.py +0 -0
- {euron_code-0.4.0 → euron_code-0.4.1}/src/euron_code/tools/shell.py +0 -0
- {euron_code-0.4.0 → euron_code-0.4.1}/src/euron_code/tools/skill.py +0 -0
- {euron_code-0.4.0 → euron_code-0.4.1}/src/euron_code/tools/symbols.py +0 -0
- {euron_code-0.4.0 → euron_code-0.4.1}/src/euron_code/tools/task.py +0 -0
- {euron_code-0.4.0 → euron_code-0.4.1}/src/euron_code/tools/todo.py +0 -0
- {euron_code-0.4.0 → euron_code-0.4.1}/src/euron_code/tools/web.py +0 -0
- {euron_code-0.4.0 → euron_code-0.4.1}/src/euron_code/trust.py +0 -0
- {euron_code-0.4.0 → euron_code-0.4.1}/src/euron_code/ui.py +0 -0
- {euron_code-0.4.0 → euron_code-0.4.1}/tests/conftest.py +0 -0
- {euron_code-0.4.0 → euron_code-0.4.1}/tests/helpers.py +0 -0
- {euron_code-0.4.0 → euron_code-0.4.1}/tests/mock_gateway.py +0 -0
- {euron_code-0.4.0 → euron_code-0.4.1}/tests/serve_helper.py +0 -0
- {euron_code-0.4.0 → euron_code-0.4.1}/tests/test_agent_mock.py +0 -0
- {euron_code-0.4.0 → euron_code-0.4.1}/tests/test_cli_e2e.py +0 -0
- {euron_code-0.4.0 → euron_code-0.4.1}/tests/test_context_memory_sessions.py +0 -0
- {euron_code-0.4.0 → euron_code-0.4.1}/tests/test_db.py +0 -0
- {euron_code-0.4.0 → euron_code-0.4.1}/tests/test_powershell_exit_codes.py +0 -0
- {euron_code-0.4.0 → euron_code-0.4.1}/tests/test_repl.py +0 -0
- {euron_code-0.4.0 → euron_code-0.4.1}/tests/test_search.py +0 -0
- {euron_code-0.4.0 → euron_code-0.4.1}/tests/test_v2_rules_skills_hooks.py +0 -0
- {euron_code-0.4.0 → euron_code-0.4.1}/tests/test_v2_tokens_shortcuts.py +0 -0
- {euron_code-0.4.0 → euron_code-0.4.1}/tests/test_v2_usage_headers.py +0 -0
- {euron_code-0.4.0 → euron_code-0.4.1}/tests/test_v31_security.py +0 -0
- {euron_code-0.4.0 → euron_code-0.4.1}/tests/test_v3_accounts_value_setup.py +0 -0
- {euron_code-0.4.0 → euron_code-0.4.1}/tests/test_v3_activity.py +0 -0
- {euron_code-0.4.0 → euron_code-0.4.1}/tests/test_v3_attachments.py +0 -0
- {euron_code-0.4.0 → euron_code-0.4.1}/tests/test_v3_context.py +0 -0
- {euron_code-0.4.0 → euron_code-0.4.1}/tests/test_v3_live_repl.py +0 -0
- {euron_code-0.4.0 → euron_code-0.4.1}/tests/test_v3_single_login.py +0 -0
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.4.1 (2026-11-05)
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
- Pasting text with emoji into a Windows terminal crashed the prompt ("surrogates not allowed") and
|
|
7
|
+
then failed every request. Text is now repaired before it is saved or sent.
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
- Clearer privacy wording; the limits section shows the daily allowance and where to track usage.
|
|
11
|
+
|
|
3
12
|
## 0.4.0 (2026-11-05)
|
|
4
13
|
|
|
5
14
|
### Run the app, not just the tests
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: euron-code
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.1
|
|
4
4
|
Summary: Euron Code: an agentic coding assistant in your terminal for Euron Plus and Super 30 students.
|
|
5
5
|
Project-URL: Homepage, https://euron.one/code-agent
|
|
6
6
|
Author-email: Euron <contact@euron.one>
|
|
@@ -167,23 +167,21 @@ Shift+Tab cycles default -> acceptEdits -> plan, and reaches bypass only if you
|
|
|
167
167
|
|
|
168
168
|
## Where your data goes
|
|
169
169
|
|
|
170
|
-
- Your
|
|
171
|
-
|
|
172
|
-
- Images you attach are sent to Google Gemini to be described in text for the coding model; they are not
|
|
173
|
-
stored. You are told this the first time you attach one.
|
|
170
|
+
- Your code stays on your computer. Euron Code runs on Euron's GPUs.
|
|
171
|
+
- Images you attach are turned into a text description by Euron's image understanding and are not stored.
|
|
174
172
|
- One login at a time: signing in on another computer signs this one out.
|
|
173
|
+
- Don't paste passwords or API keys into prompts.
|
|
175
174
|
|
|
176
175
|
## Limits and value
|
|
177
176
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
Days, weeks (Monday), months and years start at midnight IST. If a limit is reached, Euron Code says
|
|
181
|
-
which one and when it refills; nothing else breaks.
|
|
177
|
+
**10 million AI tokens. Every day.** Build more. Experiment more. Create without constantly worrying
|
|
178
|
+
about tokens. Included with Euron Plus and Super 30.
|
|
182
179
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
180
|
+
<sub>Up to 10 million tokens per day, subject to fair-use policies, model availability, and plan limits.</sub>
|
|
181
|
+
|
|
182
|
+
Track everything with `/usage` (or `euron usage`) and at https://euron.one/code-agent/usage: what you
|
|
183
|
+
used today, this week, this month and this year, what is left and when it refills, in your local time.
|
|
184
|
+
If a limit is reached, Euron Code says which one and when it refills; nothing else breaks.
|
|
187
185
|
|
|
188
186
|
## Run and check your app
|
|
189
187
|
|
|
@@ -393,7 +391,7 @@ Hooks are your own shell commands that run at fixed points. Put them in `setting
|
|
|
393
391
|
| `Stop` | when the model finishes its answer | stderr goes to the model and it continues (at most twice) |
|
|
394
392
|
|
|
395
393
|
Any other non-zero exit is shown as a warning and blocks nothing. `matcher` is a regular expression
|
|
396
|
-
over the tool name;
|
|
394
|
+
over the tool name; common names (`Edit`, `Write`, `MultiEdit`, `Read`, `Bash`, `Grep`, `Glob`,
|
|
397
395
|
`WebFetch`) and Euron Code names (`edit_file`, `bash`, ...) both work. The command runs in the project
|
|
398
396
|
folder with `cmd.exe` on Windows and `sh` elsewhere, and gets one JSON object on stdin: `event`,
|
|
399
397
|
`session_id`, `cwd`, `tool_name`, `tool_input`, `tool_output` (PostToolUse) or `prompt`. Environment:
|
|
@@ -409,7 +407,7 @@ always run.
|
|
|
409
407
|
## Context and tokens (automatic)
|
|
410
408
|
|
|
411
409
|
The default model has a 32k-token context and writes at most about 8k tokens per step. Euron Code
|
|
412
|
-
manages that
|
|
410
|
+
manages that for you, with nothing to set up:
|
|
413
411
|
|
|
414
412
|
- **Room for the answer.** An output budget (about 8k) is always kept free; "100% full" means the
|
|
415
413
|
prompt leaves no more than that. Each request asks for as many output tokens as really fit, and a
|
|
@@ -531,9 +529,9 @@ and the keychain.
|
|
|
531
529
|
|
|
532
530
|
## Privacy and safety
|
|
533
531
|
|
|
534
|
-
- Your
|
|
535
|
-
|
|
536
|
-
|
|
532
|
+
- Your code stays on your computer: only the parts the agent reads are sent, as part of the conversation
|
|
533
|
+
with Euron's model. Attached images are turned into text by Euron's image understanding and are not
|
|
534
|
+
stored.
|
|
537
535
|
- Your key is never printed in full and never logged.
|
|
538
536
|
- Edits and commands ask first. Files outside the project folder always ask, even to read.
|
|
539
537
|
- Each request carries a random session id and task id so your usage page can count sessions and
|
|
@@ -101,23 +101,21 @@ Shift+Tab cycles default -> acceptEdits -> plan, and reaches bypass only if you
|
|
|
101
101
|
|
|
102
102
|
## Where your data goes
|
|
103
103
|
|
|
104
|
-
- Your
|
|
105
|
-
|
|
106
|
-
- Images you attach are sent to Google Gemini to be described in text for the coding model; they are not
|
|
107
|
-
stored. You are told this the first time you attach one.
|
|
104
|
+
- Your code stays on your computer. Euron Code runs on Euron's GPUs.
|
|
105
|
+
- Images you attach are turned into a text description by Euron's image understanding and are not stored.
|
|
108
106
|
- One login at a time: signing in on another computer signs this one out.
|
|
107
|
+
- Don't paste passwords or API keys into prompts.
|
|
109
108
|
|
|
110
109
|
## Limits and value
|
|
111
110
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
Days, weeks (Monday), months and years start at midnight IST. If a limit is reached, Euron Code says
|
|
115
|
-
which one and when it refills; nothing else breaks.
|
|
111
|
+
**10 million AI tokens. Every day.** Build more. Experiment more. Create without constantly worrying
|
|
112
|
+
about tokens. Included with Euron Plus and Super 30.
|
|
116
113
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
114
|
+
<sub>Up to 10 million tokens per day, subject to fair-use policies, model availability, and plan limits.</sub>
|
|
115
|
+
|
|
116
|
+
Track everything with `/usage` (or `euron usage`) and at https://euron.one/code-agent/usage: what you
|
|
117
|
+
used today, this week, this month and this year, what is left and when it refills, in your local time.
|
|
118
|
+
If a limit is reached, Euron Code says which one and when it refills; nothing else breaks.
|
|
121
119
|
|
|
122
120
|
## Run and check your app
|
|
123
121
|
|
|
@@ -327,7 +325,7 @@ Hooks are your own shell commands that run at fixed points. Put them in `setting
|
|
|
327
325
|
| `Stop` | when the model finishes its answer | stderr goes to the model and it continues (at most twice) |
|
|
328
326
|
|
|
329
327
|
Any other non-zero exit is shown as a warning and blocks nothing. `matcher` is a regular expression
|
|
330
|
-
over the tool name;
|
|
328
|
+
over the tool name; common names (`Edit`, `Write`, `MultiEdit`, `Read`, `Bash`, `Grep`, `Glob`,
|
|
331
329
|
`WebFetch`) and Euron Code names (`edit_file`, `bash`, ...) both work. The command runs in the project
|
|
332
330
|
folder with `cmd.exe` on Windows and `sh` elsewhere, and gets one JSON object on stdin: `event`,
|
|
333
331
|
`session_id`, `cwd`, `tool_name`, `tool_input`, `tool_output` (PostToolUse) or `prompt`. Environment:
|
|
@@ -343,7 +341,7 @@ always run.
|
|
|
343
341
|
## Context and tokens (automatic)
|
|
344
342
|
|
|
345
343
|
The default model has a 32k-token context and writes at most about 8k tokens per step. Euron Code
|
|
346
|
-
manages that
|
|
344
|
+
manages that for you, with nothing to set up:
|
|
347
345
|
|
|
348
346
|
- **Room for the answer.** An output budget (about 8k) is always kept free; "100% full" means the
|
|
349
347
|
prompt leaves no more than that. Each request asks for as many output tokens as really fit, and a
|
|
@@ -465,9 +463,9 @@ and the keychain.
|
|
|
465
463
|
|
|
466
464
|
## Privacy and safety
|
|
467
465
|
|
|
468
|
-
- Your
|
|
469
|
-
|
|
470
|
-
|
|
466
|
+
- Your code stays on your computer: only the parts the agent reads are sent, as part of the conversation
|
|
467
|
+
with Euron's model. Attached images are turned into text by Euron's image understanding and are not
|
|
468
|
+
stored.
|
|
471
469
|
- Your key is never printed in full and never logged.
|
|
472
470
|
- Edits and commands ask first. Files outside the project folder always ask, even to read.
|
|
473
471
|
- Each request carries a random session id and task id so your usage page can count sessions and
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "euron-code"
|
|
7
|
-
version = "0.4.
|
|
7
|
+
version = "0.4.1"
|
|
8
8
|
description = "Euron Code: an agentic coding assistant in your terminal for Euron Plus and Super 30 students."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.9"
|
|
@@ -100,7 +100,7 @@ def repair_json(raw: str):
|
|
|
100
100
|
|
|
101
101
|
|
|
102
102
|
def parse_inline_tool_calls(content: str):
|
|
103
|
-
"""Fallback for servers without a tool parser:
|
|
103
|
+
"""Fallback for servers without a tool parser: some models emit <tool_call>{json}</tool_call>.
|
|
104
104
|
Slightly broken JSON is repaired; unparseable calls become an error result, never a crash."""
|
|
105
105
|
calls = []
|
|
106
106
|
for m in TOOL_CALL_TAG.finditer(content):
|
|
@@ -272,6 +272,8 @@ class Agent:
|
|
|
272
272
|
|
|
273
273
|
# ---------------------------------------------------------- public
|
|
274
274
|
def run_turn(self, user_text: str, max_turns: Optional[int] = None) -> TurnResult:
|
|
275
|
+
from euron_code import textsafe
|
|
276
|
+
user_text = textsafe.clean(user_text) # emoji pasted into a Windows console (surrogates)
|
|
275
277
|
if self.is_subagent:
|
|
276
278
|
return self._run_turn(user_text, max_turns)
|
|
277
279
|
self._ui_hook("turn_started")
|
|
@@ -19,6 +19,7 @@ from typing import Any, Callable, Dict, Iterator, List, Optional, Tuple
|
|
|
19
19
|
|
|
20
20
|
import httpx
|
|
21
21
|
|
|
22
|
+
from euron_code import textsafe
|
|
22
23
|
from euron_code import __version__, config
|
|
23
24
|
|
|
24
25
|
NO_PLUS_MESSAGE = (
|
|
@@ -366,7 +367,7 @@ class Client:
|
|
|
366
367
|
def chat(self, payload: Dict[str, Any]) -> Dict[str, Any]:
|
|
367
368
|
"""Non-streaming completion (used for compaction summaries)."""
|
|
368
369
|
with self._lock:
|
|
369
|
-
body = dict(payload)
|
|
370
|
+
body = textsafe.clean_obj(dict(payload))
|
|
370
371
|
body["stream"] = False
|
|
371
372
|
data = self._json("POST", "/v1/chat/completions", json=body, headers=self._chat_headers())
|
|
372
373
|
self.takeover_once = False
|
|
@@ -383,7 +384,7 @@ class Client:
|
|
|
383
384
|
raise EuronError("Internal error: a model request is already running.")
|
|
384
385
|
q: "queue.Queue[Tuple[str, Any]]" = queue.Queue()
|
|
385
386
|
holder: Dict[str, Any] = {}
|
|
386
|
-
body = dict(payload)
|
|
387
|
+
body = textsafe.clean_obj(dict(payload))
|
|
387
388
|
body["stream"] = True
|
|
388
389
|
body.setdefault("stream_options", {"include_usage": True})
|
|
389
390
|
|
|
@@ -130,7 +130,7 @@ def load_image(path: Path) -> Tuple[Dict, str]:
|
|
|
130
130
|
return {"type": "image_url", "image_url": {"url": url}}, note
|
|
131
131
|
|
|
132
132
|
|
|
133
|
-
IMAGE_PRIVACY = ("Images
|
|
133
|
+
IMAGE_PRIVACY = ("Images are turned into a text description by Euron's image understanding and are not stored; "
|
|
134
134
|
"they are not stored. Don't attach images that show secrets.")
|
|
135
135
|
|
|
136
136
|
|
|
@@ -17,7 +17,7 @@ from typing import Any, Dict, List, Optional, Tuple
|
|
|
17
17
|
|
|
18
18
|
DEFAULT_BASE_URL = "https://api.euron.one/api/v1/code-agent"
|
|
19
19
|
|
|
20
|
-
#: Context window and output cap of the default model
|
|
20
|
+
#: Context window and output cap of the default model.
|
|
21
21
|
DEFAULT_CONTEXT_WINDOW = 32768
|
|
22
22
|
DEFAULT_MAX_OUTPUT = 8192
|
|
23
23
|
|
|
@@ -1,88 +1,88 @@
|
|
|
1
|
-
"""`euron doctor`: check the local setup and the connection to Euron Code."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
import os
|
|
6
|
-
import platform
|
|
7
|
-
import sys
|
|
8
|
-
from typing import List, Tuple
|
|
9
|
-
|
|
10
|
-
import httpx
|
|
11
|
-
from rich.markup import escape
|
|
12
|
-
|
|
13
|
-
from euron_code import __version__, api, config, credentials
|
|
14
|
-
from euron_code.tools.search import ripgrep_path
|
|
15
|
-
from euron_code.tools.shell import detect_shell
|
|
16
|
-
|
|
17
|
-
OK, WARN, FAIL = "ok", "warn", "fail"
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
def run_checks() -> List[Tuple[str, str, str]]:
|
|
21
|
-
checks: List[Tuple[str, str, str]] = []
|
|
22
|
-
v = sys.version_info
|
|
23
|
-
checks.append(("Python", OK if v >= (3, 9) else FAIL,
|
|
24
|
-
f"{platform.python_version()} ({sys.executable})"))
|
|
25
|
-
checks.append(("euron-code", OK, __version__))
|
|
26
|
-
checks.append(("Platform", OK, f"{platform.system()} {platform.release()} {platform.machine()}"))
|
|
27
|
-
url = config.base_url()
|
|
28
|
-
src = "EURON_CODE_API" if os.environ.get("EURON_CODE_API") else "default"
|
|
29
|
-
checks.append(("API base URL", OK, f"{url} ({src})"))
|
|
30
|
-
|
|
31
|
-
# connectivity: any HTTP answer means the gateway is reachable
|
|
32
|
-
try:
|
|
33
|
-
r = httpx.get(url + "/v1/models", timeout=10.0, headers={"User-Agent": api.user_agent()})
|
|
34
|
-
checks.append(("Connectivity", OK, f"reachable (HTTP {r.status_code})"))
|
|
35
|
-
reachable = True
|
|
36
|
-
except httpx.HTTPError as e:
|
|
37
|
-
checks.append(("Connectivity", FAIL, f"cannot reach {url}: {type(e).__name__}"))
|
|
38
|
-
reachable = False
|
|
39
|
-
|
|
40
|
-
cred = credentials.load(url)
|
|
41
|
-
if cred is None:
|
|
42
|
-
checks.append(("Login", WARN, "not logged in; run `euron login`"))
|
|
43
|
-
else:
|
|
44
|
-
checks.append(("Login", OK, f"key {credentials.mask_key(cred.api_key)} in {credentials.where(url)}"))
|
|
45
|
-
if reachable:
|
|
46
|
-
c = api.Client(api_key=cred.api_key, base_url=url)
|
|
47
|
-
try:
|
|
48
|
-
me = c.me(use_cache=False)
|
|
49
|
-
access = me.get("access") or {}
|
|
50
|
-
if access.get("entitled"):
|
|
51
|
-
q = me.get("quota") or {}
|
|
52
|
-
checks.append(("Entitlement", OK, f"{api.plan_label(access)}; "
|
|
53
|
-
f"{q.get('remainingToday', '?'):,} tokens left today"
|
|
54
|
-
if isinstance(q.get("remainingToday"), int) else api.plan_label(access)))
|
|
55
|
-
else:
|
|
56
|
-
checks.append(("Entitlement", FAIL, api.NO_PLUS_MESSAGE))
|
|
57
|
-
except api.AuthError:
|
|
58
|
-
checks.append(("Entitlement", FAIL, "the saved key was rejected (401); run `euron login`"))
|
|
59
|
-
except api.EuronError as e:
|
|
60
|
-
checks.append(("Entitlement", WARN, f"could not check: {e.message}"))
|
|
61
|
-
finally:
|
|
62
|
-
c.close()
|
|
63
|
-
|
|
64
|
-
rg = ripgrep_path()
|
|
65
|
-
checks.append(("ripgrep", OK if rg else WARN, rg or "not found; grep uses the slower Python fallback"))
|
|
66
|
-
checks.append(("Keyring", OK if "unavailable" not in credentials.keyring_backend_name() else WARN,
|
|
67
|
-
credentials.keyring_backend_name()))
|
|
68
|
-
kind, exe = detect_shell(config.load_user_settings().get("shell"))
|
|
69
|
-
checks.append(("Shell", OK, f"{kind} ({exe})"))
|
|
70
|
-
term = os.environ.get("TERM_PROGRAM") or os.environ.get("TERM") or ("Windows console" if os.name == "nt" else "?")
|
|
71
|
-
checks.append(("Terminal", OK if sys.stdout.isatty() else WARN,
|
|
72
|
-
f"{term}{'' if sys.stdout.isatty() else ' (output is not a terminal)'}"))
|
|
73
|
-
checks.append(("Config", OK, f"{config.user_settings_path()}"))
|
|
74
|
-
return checks
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
def doctor(ui) -> int:
|
|
78
|
-
checks = run_checks()
|
|
79
|
-
style = {OK: "[green]OK [/green]", WARN: "[yellow]WARN[/yellow]", FAIL: "[red]FAIL[/red]"}
|
|
80
|
-
for name, st, detail in checks:
|
|
81
|
-
ui.info(f"{style[st]} {name:<14} {escape(detail)}")
|
|
82
|
-
fails = [c for c in checks if c[1] == FAIL]
|
|
83
|
-
ui.info("")
|
|
84
|
-
if fails:
|
|
85
|
-
ui.info(f"[red]{len(fails)} problem(s) found.[/red]")
|
|
86
|
-
return 1
|
|
87
|
-
ui.info("[green]All checks passed.[/green]")
|
|
88
|
-
return 0
|
|
1
|
+
"""`euron doctor`: check the local setup and the connection to Euron Code."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import platform
|
|
7
|
+
import sys
|
|
8
|
+
from typing import List, Tuple
|
|
9
|
+
|
|
10
|
+
import httpx
|
|
11
|
+
from rich.markup import escape
|
|
12
|
+
|
|
13
|
+
from euron_code import __version__, api, config, credentials
|
|
14
|
+
from euron_code.tools.search import ripgrep_path
|
|
15
|
+
from euron_code.tools.shell import detect_shell
|
|
16
|
+
|
|
17
|
+
OK, WARN, FAIL = "ok", "warn", "fail"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def run_checks() -> List[Tuple[str, str, str]]:
|
|
21
|
+
checks: List[Tuple[str, str, str]] = []
|
|
22
|
+
v = sys.version_info
|
|
23
|
+
checks.append(("Python", OK if v >= (3, 9) else FAIL,
|
|
24
|
+
f"{platform.python_version()} ({sys.executable})"))
|
|
25
|
+
checks.append(("euron-code", OK, __version__))
|
|
26
|
+
checks.append(("Platform", OK, f"{platform.system()} {platform.release()} {platform.machine()}"))
|
|
27
|
+
url = config.base_url()
|
|
28
|
+
src = "EURON_CODE_API" if os.environ.get("EURON_CODE_API") else "default"
|
|
29
|
+
checks.append(("API base URL", OK, f"{url} ({src})"))
|
|
30
|
+
|
|
31
|
+
# connectivity: any HTTP answer means the gateway is reachable
|
|
32
|
+
try:
|
|
33
|
+
r = httpx.get(url + "/v1/models", timeout=10.0, headers={"User-Agent": api.user_agent()})
|
|
34
|
+
checks.append(("Connectivity", OK, f"reachable (HTTP {r.status_code})"))
|
|
35
|
+
reachable = True
|
|
36
|
+
except httpx.HTTPError as e:
|
|
37
|
+
checks.append(("Connectivity", FAIL, f"cannot reach {url}: {type(e).__name__}"))
|
|
38
|
+
reachable = False
|
|
39
|
+
|
|
40
|
+
cred = credentials.load(url)
|
|
41
|
+
if cred is None:
|
|
42
|
+
checks.append(("Login", WARN, "not logged in; run `euron login`"))
|
|
43
|
+
else:
|
|
44
|
+
checks.append(("Login", OK, f"key {credentials.mask_key(cred.api_key)} in {credentials.where(url)}"))
|
|
45
|
+
if reachable:
|
|
46
|
+
c = api.Client(api_key=cred.api_key, base_url=url)
|
|
47
|
+
try:
|
|
48
|
+
me = c.me(use_cache=False)
|
|
49
|
+
access = me.get("access") or {}
|
|
50
|
+
if access.get("entitled"):
|
|
51
|
+
q = me.get("quota") or {}
|
|
52
|
+
checks.append(("Entitlement", OK, f"{api.plan_label(access)}; "
|
|
53
|
+
f"{q.get('remainingToday', '?'):,} tokens left today"
|
|
54
|
+
if isinstance(q.get("remainingToday"), int) else api.plan_label(access)))
|
|
55
|
+
else:
|
|
56
|
+
checks.append(("Entitlement", FAIL, api.NO_PLUS_MESSAGE))
|
|
57
|
+
except api.AuthError:
|
|
58
|
+
checks.append(("Entitlement", FAIL, "the saved key was rejected (401); run `euron login`"))
|
|
59
|
+
except api.EuronError as e:
|
|
60
|
+
checks.append(("Entitlement", WARN, f"could not check: {e.message}"))
|
|
61
|
+
finally:
|
|
62
|
+
c.close()
|
|
63
|
+
|
|
64
|
+
rg = ripgrep_path()
|
|
65
|
+
checks.append(("ripgrep", OK if rg else WARN, rg or "not found; grep uses the slower Python fallback"))
|
|
66
|
+
checks.append(("Keyring", OK if "unavailable" not in credentials.keyring_backend_name() else WARN,
|
|
67
|
+
credentials.keyring_backend_name()))
|
|
68
|
+
kind, exe = detect_shell(config.load_user_settings().get("shell"))
|
|
69
|
+
checks.append(("Shell", OK, f"{kind} ({exe})"))
|
|
70
|
+
term = os.environ.get("TERM_PROGRAM") or os.environ.get("TERM") or ("Windows console" if os.name == "nt" else "?")
|
|
71
|
+
checks.append(("Terminal", OK if sys.stdout.isatty() else WARN,
|
|
72
|
+
f"{term}{'' if sys.stdout.isatty() else ' (output is not a terminal)'}"))
|
|
73
|
+
checks.append(("Config", OK, f"{config.user_settings_path()}"))
|
|
74
|
+
return checks
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def doctor(ui) -> int:
|
|
78
|
+
checks = run_checks()
|
|
79
|
+
style = {OK: "[green]OK [/green]", WARN: "[yellow]WARN[/yellow]", FAIL: "[red]FAIL[/red]"}
|
|
80
|
+
for name, st, detail in checks:
|
|
81
|
+
ui.info(f"{style[st]} {name:<14} {escape(detail)}")
|
|
82
|
+
fails = [c for c in checks if c[1] == FAIL]
|
|
83
|
+
ui.info("")
|
|
84
|
+
if fails:
|
|
85
|
+
ui.info(f"[red]{len(fails)} problem(s) found.[/red]")
|
|
86
|
+
return 1
|
|
87
|
+
ui.info("[green]All checks passed.[/green]")
|
|
88
|
+
return 0
|
|
@@ -342,6 +342,13 @@ class Repl:
|
|
|
342
342
|
from prompt_toolkit import PromptSession
|
|
343
343
|
from prompt_toolkit.filters import Condition
|
|
344
344
|
from prompt_toolkit.history import FileHistory
|
|
345
|
+
|
|
346
|
+
class _SafeHistory(FileHistory):
|
|
347
|
+
"""History that never fails on emoji pasted into a Windows console."""
|
|
348
|
+
|
|
349
|
+
def store_string(self, string: str) -> None:
|
|
350
|
+
from euron_code import textsafe
|
|
351
|
+
super().store_string(textsafe.clean(string))
|
|
345
352
|
from prompt_toolkit.key_binding import KeyBindings
|
|
346
353
|
from prompt_toolkit.styles import Style
|
|
347
354
|
|
|
@@ -413,7 +420,7 @@ class Repl:
|
|
|
413
420
|
return (t.startswith("/") and " " not in t) or (last.startswith("@") and len(last) > 1)
|
|
414
421
|
|
|
415
422
|
session = PromptSession(
|
|
416
|
-
history=
|
|
423
|
+
history=_SafeHistory(str(hist)),
|
|
417
424
|
completer=_completer(self),
|
|
418
425
|
complete_while_typing=Condition(auto_menu),
|
|
419
426
|
key_bindings=kb,
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""Text that is always valid UTF-8.
|
|
2
|
+
|
|
3
|
+
Windows consoles hand Python emoji and other non-BMP characters as two UTF-16
|
|
4
|
+
surrogate halves (seen live 2026-11-05: pasting text with emoji crashed the
|
|
5
|
+
prompt history and then every request with "surrogates not allowed"). File
|
|
6
|
+
contents decoded with errors="surrogateescape" can carry lone surrogates too.
|
|
7
|
+
`clean` rejoins valid pairs into the real character and replaces anything still
|
|
8
|
+
broken, so nothing downstream (history file, session file, JSON body) can fail.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def clean(s: str) -> str:
|
|
17
|
+
if not isinstance(s, str) or s.isascii():
|
|
18
|
+
return s
|
|
19
|
+
try:
|
|
20
|
+
s.encode("utf-8")
|
|
21
|
+
return s # already valid
|
|
22
|
+
except UnicodeEncodeError:
|
|
23
|
+
pass
|
|
24
|
+
try:
|
|
25
|
+
s = s.encode("utf-16", "surrogatepass").decode("utf-16") # rejoin pairs
|
|
26
|
+
except UnicodeError:
|
|
27
|
+
pass
|
|
28
|
+
return s.encode("utf-8", "replace").decode("utf-8") # lone halves become "?"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def clean_obj(o: Any) -> Any:
|
|
32
|
+
"""Recursively clean every string in a JSON-like value (dicts, lists, strings)."""
|
|
33
|
+
if isinstance(o, str):
|
|
34
|
+
return clean(o)
|
|
35
|
+
if isinstance(o, list):
|
|
36
|
+
return [clean_obj(x) for x in o]
|
|
37
|
+
if isinstance(o, tuple):
|
|
38
|
+
return tuple(clean_obj(x) for x in o)
|
|
39
|
+
if isinstance(o, dict):
|
|
40
|
+
return {clean(k) if isinstance(k, str) else k: clean_obj(v) for k, v in o.items()}
|
|
41
|
+
return o
|
|
@@ -160,7 +160,7 @@ def money(v: Optional[Dict[str, Any]], unicode: bool = True) -> str:
|
|
|
160
160
|
|
|
161
161
|
def reference_label(value: Optional[Dict[str, Any]]) -> str:
|
|
162
162
|
ref = (value or {}).get("reference") or {}
|
|
163
|
-
label = ref.get("label") or "
|
|
163
|
+
label = ref.get("label") or "reference market API prices"
|
|
164
164
|
return "at " + (label if label.endswith("s") else label + "s")
|
|
165
165
|
|
|
166
166
|
|