janito 2.3.0__py3-none-any.whl → 2.3.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. janito/__init__.py +6 -6
  2. janito/cli/chat_mode/shell/autocomplete.py +21 -21
  3. janito/cli/chat_mode/shell/commands/clear.py +12 -12
  4. janito/cli/chat_mode/shell/commands/multi.py +51 -51
  5. janito/cli/chat_mode/shell/input_history.py +62 -62
  6. janito/cli/cli_commands/list_models.py +35 -35
  7. janito/cli/cli_commands/list_providers.py +9 -9
  8. janito/cli/cli_commands/list_tools.py +53 -53
  9. janito/cli/cli_commands/model_selection.py +50 -50
  10. janito/cli/cli_commands/model_utils.py +95 -95
  11. janito/cli/cli_commands/set_api_key.py +19 -19
  12. janito/cli/cli_commands/show_config.py +51 -51
  13. janito/cli/cli_commands/show_system_prompt.py +62 -62
  14. janito/cli/core/__init__.py +4 -4
  15. janito/cli/core/event_logger.py +59 -59
  16. janito/cli/core/getters.py +33 -33
  17. janito/cli/core/unsetters.py +54 -54
  18. janito/cli/single_shot_mode/__init__.py +6 -6
  19. janito/config.py +5 -5
  20. janito/config_manager.py +112 -112
  21. janito/drivers/anthropic/driver.py +113 -113
  22. janito/formatting_token.py +54 -54
  23. janito/i18n/__init__.py +35 -35
  24. janito/i18n/messages.py +23 -23
  25. janito/i18n/pt.py +47 -47
  26. janito/llm/__init__.py +5 -5
  27. janito/llm/agent.py +443 -443
  28. janito/llm/auth.py +63 -63
  29. janito/llm/driver_config_builder.py +34 -34
  30. janito/llm/driver_input.py +12 -12
  31. janito/llm/message_parts.py +60 -60
  32. janito/llm/model.py +38 -38
  33. janito/llm/provider.py +196 -196
  34. janito/provider_registry.py +176 -176
  35. janito/providers/anthropic/model_info.py +22 -22
  36. janito/providers/anthropic/provider.py +2 -0
  37. janito/providers/azure_openai/model_info.py +16 -16
  38. janito/providers/azure_openai/provider.py +3 -0
  39. janito/providers/deepseek/__init__.py +1 -1
  40. janito/providers/deepseek/model_info.py +16 -16
  41. janito/providers/deepseek/provider.py +94 -91
  42. janito/providers/google/provider.py +3 -0
  43. janito/providers/mistralai/provider.py +3 -0
  44. janito/providers/openai/provider.py +4 -0
  45. janito/tools/adapters/__init__.py +1 -1
  46. janito/tools/adapters/local/ask_user.py +102 -102
  47. janito/tools/adapters/local/copy_file.py +84 -84
  48. janito/tools/adapters/local/create_directory.py +69 -69
  49. janito/tools/adapters/local/create_file.py +82 -82
  50. janito/tools/adapters/local/fetch_url.py +97 -97
  51. janito/tools/adapters/local/find_files.py +138 -138
  52. janito/tools/adapters/local/get_file_outline/__init__.py +1 -1
  53. janito/tools/adapters/local/get_file_outline/core.py +117 -117
  54. janito/tools/adapters/local/get_file_outline/java_outline.py +40 -40
  55. janito/tools/adapters/local/get_file_outline/markdown_outline.py +14 -14
  56. janito/tools/adapters/local/get_file_outline/python_outline.py +303 -303
  57. janito/tools/adapters/local/get_file_outline/python_outline_v2.py +156 -156
  58. janito/tools/adapters/local/get_file_outline/search_outline.py +33 -33
  59. janito/tools/adapters/local/python_code_run.py +166 -166
  60. janito/tools/adapters/local/python_command_run.py +164 -164
  61. janito/tools/adapters/local/python_file_run.py +163 -163
  62. janito/tools/adapters/local/run_bash_command.py +176 -176
  63. janito/tools/adapters/local/run_powershell_command.py +219 -219
  64. janito/tools/adapters/local/search_text/__init__.py +1 -1
  65. janito/tools/adapters/local/search_text/core.py +201 -201
  66. janito/tools/adapters/local/search_text/pattern_utils.py +73 -73
  67. janito/tools/adapters/local/search_text/traverse_directory.py +145 -145
  68. janito/tools/adapters/local/validate_file_syntax/__init__.py +1 -1
  69. janito/tools/adapters/local/validate_file_syntax/core.py +106 -106
  70. janito/tools/adapters/local/validate_file_syntax/css_validator.py +35 -35
  71. janito/tools/adapters/local/validate_file_syntax/html_validator.py +93 -93
  72. janito/tools/adapters/local/validate_file_syntax/js_validator.py +27 -27
  73. janito/tools/adapters/local/validate_file_syntax/json_validator.py +6 -6
  74. janito/tools/adapters/local/validate_file_syntax/markdown_validator.py +109 -109
  75. janito/tools/adapters/local/validate_file_syntax/ps1_validator.py +32 -32
  76. janito/tools/adapters/local/validate_file_syntax/python_validator.py +5 -5
  77. janito/tools/adapters/local/validate_file_syntax/xml_validator.py +11 -11
  78. janito/tools/adapters/local/validate_file_syntax/yaml_validator.py +6 -6
  79. janito/tools/adapters/local/view_file.py +167 -167
  80. janito/tools/inspect_registry.py +17 -17
  81. janito/tools/tool_base.py +105 -105
  82. janito/tools/tool_events.py +58 -58
  83. janito/tools/tool_run_exception.py +12 -12
  84. janito/tools/tool_use_tracker.py +81 -81
  85. janito/tools/tool_utils.py +45 -45
  86. janito/tools/tools_schema.py +104 -104
  87. janito/version.py +4 -4
  88. {janito-2.3.0.dist-info → janito-2.3.1.dist-info}/METADATA +390 -388
  89. {janito-2.3.0.dist-info → janito-2.3.1.dist-info}/RECORD +93 -93
  90. {janito-2.3.0.dist-info → janito-2.3.1.dist-info}/WHEEL +0 -0
  91. {janito-2.3.0.dist-info → janito-2.3.1.dist-info}/entry_points.txt +0 -0
  92. {janito-2.3.0.dist-info → janito-2.3.1.dist-info}/licenses/LICENSE +0 -0
  93. {janito-2.3.0.dist-info → janito-2.3.1.dist-info}/top_level.txt +0 -0
@@ -1,145 +1,145 @@
1
- import os
2
- from janito.gitignore_utils import GitignoreFilter
3
- from .match_lines import match_line, should_limit, read_file_lines
4
-
5
-
6
- def walk_directory(search_path, max_depth):
7
- if max_depth == 1:
8
- walk_result = next(os.walk(search_path), None)
9
- if walk_result is None:
10
- return [(search_path, [], [])]
11
- else:
12
- return [walk_result]
13
- else:
14
- return os.walk(search_path)
15
-
16
-
17
- def filter_dirs(dirs, root, gitignore_filter):
18
- # Always exclude directories named .git, regardless of gitignore
19
- return [
20
- d
21
- for d in dirs
22
- if d != ".git" and not gitignore_filter.is_ignored(os.path.join(root, d))
23
- ]
24
-
25
-
26
- def process_file_count_only(
27
- file_path,
28
- per_file_counts,
29
- pattern,
30
- regex,
31
- use_regex,
32
- case_sensitive,
33
- max_results,
34
- total_results,
35
- ):
36
- match_count, file_limit_reached, _ = read_file_lines(
37
- file_path,
38
- pattern,
39
- regex,
40
- use_regex,
41
- case_sensitive,
42
- True,
43
- max_results,
44
- total_results + sum(count for _, count in per_file_counts),
45
- )
46
- if match_count > 0:
47
- per_file_counts.append((file_path, match_count))
48
- return file_limit_reached
49
-
50
-
51
- def process_file_collect(
52
- file_path,
53
- dir_output,
54
- per_file_counts,
55
- pattern,
56
- regex,
57
- use_regex,
58
- case_sensitive,
59
- max_results,
60
- total_results,
61
- ):
62
- actual_match_count, file_limit_reached, file_lines_output = read_file_lines(
63
- file_path,
64
- pattern,
65
- regex,
66
- use_regex,
67
- case_sensitive,
68
- False,
69
- max_results,
70
- total_results + len(dir_output),
71
- )
72
- dir_output.extend(file_lines_output)
73
- if actual_match_count > 0:
74
- per_file_counts.append((file_path, actual_match_count))
75
- return file_limit_reached
76
-
77
-
78
- def should_limit_depth(root, search_path, max_depth, dirs):
79
- if max_depth > 0:
80
- rel_root = os.path.relpath(root, search_path)
81
- if rel_root != ".":
82
- depth = rel_root.count(os.sep) + 1
83
- if depth >= max_depth:
84
- del dirs[:]
85
-
86
-
87
- def traverse_directory(
88
- search_path,
89
- pattern,
90
- regex,
91
- use_regex,
92
- case_sensitive,
93
- max_depth,
94
- max_results,
95
- total_results,
96
- count_only,
97
- ):
98
- dir_output = []
99
- dir_limit_reached = False
100
- per_file_counts = []
101
- walker = walk_directory(search_path, max_depth)
102
- gitignore_filter = GitignoreFilter(search_path)
103
-
104
- for root, dirs, files in walker:
105
- dirs[:] = filter_dirs(dirs, root, gitignore_filter)
106
- for file in files:
107
- file_path = os.path.join(root, file)
108
- if gitignore_filter.is_ignored(file_path):
109
- continue
110
- if count_only:
111
- file_limit_reached = process_file_count_only(
112
- file_path,
113
- per_file_counts,
114
- pattern,
115
- regex,
116
- use_regex,
117
- case_sensitive,
118
- max_results,
119
- total_results,
120
- )
121
- if file_limit_reached:
122
- dir_limit_reached = True
123
- break
124
- else:
125
- file_limit_reached = process_file_collect(
126
- file_path,
127
- dir_output,
128
- per_file_counts,
129
- pattern,
130
- regex,
131
- use_regex,
132
- case_sensitive,
133
- max_results,
134
- total_results,
135
- )
136
- if file_limit_reached:
137
- dir_limit_reached = True
138
- break
139
- if dir_limit_reached:
140
- break
141
- should_limit_depth(root, search_path, max_depth, dirs)
142
- if count_only:
143
- return per_file_counts, dir_limit_reached, []
144
- else:
145
- return dir_output, dir_limit_reached, per_file_counts
1
+ import os
2
+ from janito.gitignore_utils import GitignoreFilter
3
+ from .match_lines import match_line, should_limit, read_file_lines
4
+
5
+
6
+ def walk_directory(search_path, max_depth):
7
+ if max_depth == 1:
8
+ walk_result = next(os.walk(search_path), None)
9
+ if walk_result is None:
10
+ return [(search_path, [], [])]
11
+ else:
12
+ return [walk_result]
13
+ else:
14
+ return os.walk(search_path)
15
+
16
+
17
+ def filter_dirs(dirs, root, gitignore_filter):
18
+ # Always exclude directories named .git, regardless of gitignore
19
+ return [
20
+ d
21
+ for d in dirs
22
+ if d != ".git" and not gitignore_filter.is_ignored(os.path.join(root, d))
23
+ ]
24
+
25
+
26
+ def process_file_count_only(
27
+ file_path,
28
+ per_file_counts,
29
+ pattern,
30
+ regex,
31
+ use_regex,
32
+ case_sensitive,
33
+ max_results,
34
+ total_results,
35
+ ):
36
+ match_count, file_limit_reached, _ = read_file_lines(
37
+ file_path,
38
+ pattern,
39
+ regex,
40
+ use_regex,
41
+ case_sensitive,
42
+ True,
43
+ max_results,
44
+ total_results + sum(count for _, count in per_file_counts),
45
+ )
46
+ if match_count > 0:
47
+ per_file_counts.append((file_path, match_count))
48
+ return file_limit_reached
49
+
50
+
51
+ def process_file_collect(
52
+ file_path,
53
+ dir_output,
54
+ per_file_counts,
55
+ pattern,
56
+ regex,
57
+ use_regex,
58
+ case_sensitive,
59
+ max_results,
60
+ total_results,
61
+ ):
62
+ actual_match_count, file_limit_reached, file_lines_output = read_file_lines(
63
+ file_path,
64
+ pattern,
65
+ regex,
66
+ use_regex,
67
+ case_sensitive,
68
+ False,
69
+ max_results,
70
+ total_results + len(dir_output),
71
+ )
72
+ dir_output.extend(file_lines_output)
73
+ if actual_match_count > 0:
74
+ per_file_counts.append((file_path, actual_match_count))
75
+ return file_limit_reached
76
+
77
+
78
+ def should_limit_depth(root, search_path, max_depth, dirs):
79
+ if max_depth > 0:
80
+ rel_root = os.path.relpath(root, search_path)
81
+ if rel_root != ".":
82
+ depth = rel_root.count(os.sep) + 1
83
+ if depth >= max_depth:
84
+ del dirs[:]
85
+
86
+
87
+ def traverse_directory(
88
+ search_path,
89
+ pattern,
90
+ regex,
91
+ use_regex,
92
+ case_sensitive,
93
+ max_depth,
94
+ max_results,
95
+ total_results,
96
+ count_only,
97
+ ):
98
+ dir_output = []
99
+ dir_limit_reached = False
100
+ per_file_counts = []
101
+ walker = walk_directory(search_path, max_depth)
102
+ gitignore_filter = GitignoreFilter(search_path)
103
+
104
+ for root, dirs, files in walker:
105
+ dirs[:] = filter_dirs(dirs, root, gitignore_filter)
106
+ for file in files:
107
+ file_path = os.path.join(root, file)
108
+ if gitignore_filter.is_ignored(file_path):
109
+ continue
110
+ if count_only:
111
+ file_limit_reached = process_file_count_only(
112
+ file_path,
113
+ per_file_counts,
114
+ pattern,
115
+ regex,
116
+ use_regex,
117
+ case_sensitive,
118
+ max_results,
119
+ total_results,
120
+ )
121
+ if file_limit_reached:
122
+ dir_limit_reached = True
123
+ break
124
+ else:
125
+ file_limit_reached = process_file_collect(
126
+ file_path,
127
+ dir_output,
128
+ per_file_counts,
129
+ pattern,
130
+ regex,
131
+ use_regex,
132
+ case_sensitive,
133
+ max_results,
134
+ total_results,
135
+ )
136
+ if file_limit_reached:
137
+ dir_limit_reached = True
138
+ break
139
+ if dir_limit_reached:
140
+ break
141
+ should_limit_depth(root, search_path, max_depth, dirs)
142
+ if count_only:
143
+ return per_file_counts, dir_limit_reached, []
144
+ else:
145
+ return dir_output, dir_limit_reached, per_file_counts
@@ -1 +1 @@
1
- # Validation syntax package
1
+ # Validation syntax package
@@ -1,106 +1,106 @@
1
- import os
2
- from janito.i18n import tr
3
- from janito.tools.tool_base import ToolBase
4
- from janito.report_events import ReportAction
5
- from janito.tools.adapters.local.adapter import register_local_tool
6
- from janito.tools.tool_utils import display_path
7
- from janito.tools.adapters.local.adapter import register_local_tool as register_tool
8
-
9
- from .python_validator import validate_python
10
- from .json_validator import validate_json
11
- from .yaml_validator import validate_yaml
12
- from .ps1_validator import validate_ps1
13
- from .xml_validator import validate_xml
14
- from .html_validator import validate_html
15
- from .markdown_validator import validate_markdown
16
- from .js_validator import validate_js
17
- from .css_validator import validate_css
18
-
19
-
20
- def _get_validator(ext):
21
- """Return the appropriate validator function for the file extension."""
22
- mapping = {
23
- ".py": validate_python,
24
- ".pyw": validate_python,
25
- ".json": validate_json,
26
- ".yml": validate_yaml,
27
- ".yaml": validate_yaml,
28
- ".ps1": validate_ps1,
29
- ".xml": validate_xml,
30
- ".html": validate_html,
31
- ".htm": validate_html,
32
- ".md": validate_markdown,
33
- ".js": validate_js,
34
- ".css": validate_css,
35
- }
36
- return mapping.get(ext)
37
-
38
-
39
- def _handle_validation_error(e, report_warning):
40
- msg = tr("⚠️ Warning: Syntax error: {error}", error=e)
41
- if report_warning:
42
- report_warning(msg)
43
- return msg
44
-
45
-
46
- def validate_file_syntax(
47
- file_path: str, report_info=None, report_warning=None, report_success=None
48
- ) -> str:
49
- ext = os.path.splitext(file_path)[1].lower()
50
- validator = _get_validator(ext)
51
- try:
52
- if validator:
53
- return validator(file_path)
54
- else:
55
- msg = tr("⚠️ Warning: Unsupported file extension: {ext}", ext=ext)
56
- if report_warning:
57
- report_warning(msg)
58
- return msg
59
- except Exception as e:
60
- return _handle_validation_error(e, report_warning)
61
-
62
-
63
- class ValidateFileSyntaxTool(ToolBase):
64
- """
65
- Validate a file for syntax issues.
66
-
67
- Supported types:
68
- - Python (.py, .pyw)
69
- - JSON (.json)
70
- - YAML (.yml, .yaml)
71
- - PowerShell (.ps1)
72
- - XML (.xml)
73
- - HTML (.html, .htm) [lxml]
74
- - Markdown (.md)
75
- - JavaScript (.js)
76
-
77
- Args:
78
- file_path (str): Path to the file to validate.
79
- Returns:
80
- str: Validation status message. Example:
81
- - "✅ Syntax OK"
82
- - "⚠️ Warning: Syntax error: <error message>"
83
- - "⚠️ Warning: Unsupported file extension: <ext>"
84
- """
85
-
86
- tool_name = "validate_file_syntax"
87
-
88
- def run(self, file_path: str) -> str:
89
- disp_path = display_path(file_path)
90
- self.report_action(
91
- tr(
92
- "🔎 Validate syntax for file '{disp_path}' ...",
93
- disp_path=disp_path,
94
- ),
95
- ReportAction.READ,
96
- )
97
- result = validate_file_syntax(
98
- file_path,
99
- report_info=self.report_info,
100
- report_warning=self.report_warning,
101
- report_success=self.report_success,
102
- )
103
- if result.startswith("✅"):
104
- self.report_success(result, ReportAction.READ)
105
-
106
- return result
1
+ import os
2
+ from janito.i18n import tr
3
+ from janito.tools.tool_base import ToolBase
4
+ from janito.report_events import ReportAction
5
+ from janito.tools.adapters.local.adapter import register_local_tool
6
+ from janito.tools.tool_utils import display_path
7
+ from janito.tools.adapters.local.adapter import register_local_tool as register_tool
8
+
9
+ from .python_validator import validate_python
10
+ from .json_validator import validate_json
11
+ from .yaml_validator import validate_yaml
12
+ from .ps1_validator import validate_ps1
13
+ from .xml_validator import validate_xml
14
+ from .html_validator import validate_html
15
+ from .markdown_validator import validate_markdown
16
+ from .js_validator import validate_js
17
+ from .css_validator import validate_css
18
+
19
+
20
+ def _get_validator(ext):
21
+ """Return the appropriate validator function for the file extension."""
22
+ mapping = {
23
+ ".py": validate_python,
24
+ ".pyw": validate_python,
25
+ ".json": validate_json,
26
+ ".yml": validate_yaml,
27
+ ".yaml": validate_yaml,
28
+ ".ps1": validate_ps1,
29
+ ".xml": validate_xml,
30
+ ".html": validate_html,
31
+ ".htm": validate_html,
32
+ ".md": validate_markdown,
33
+ ".js": validate_js,
34
+ ".css": validate_css,
35
+ }
36
+ return mapping.get(ext)
37
+
38
+
39
+ def _handle_validation_error(e, report_warning):
40
+ msg = tr("⚠️ Warning: Syntax error: {error}", error=e)
41
+ if report_warning:
42
+ report_warning(msg)
43
+ return msg
44
+
45
+
46
+ def validate_file_syntax(
47
+ file_path: str, report_info=None, report_warning=None, report_success=None
48
+ ) -> str:
49
+ ext = os.path.splitext(file_path)[1].lower()
50
+ validator = _get_validator(ext)
51
+ try:
52
+ if validator:
53
+ return validator(file_path)
54
+ else:
55
+ msg = tr("⚠️ Warning: Unsupported file extension: {ext}", ext=ext)
56
+ if report_warning:
57
+ report_warning(msg)
58
+ return msg
59
+ except Exception as e:
60
+ return _handle_validation_error(e, report_warning)
61
+
62
+
63
+ class ValidateFileSyntaxTool(ToolBase):
64
+ """
65
+ Validate a file for syntax issues.
66
+
67
+ Supported types:
68
+ - Python (.py, .pyw)
69
+ - JSON (.json)
70
+ - YAML (.yml, .yaml)
71
+ - PowerShell (.ps1)
72
+ - XML (.xml)
73
+ - HTML (.html, .htm) [lxml]
74
+ - Markdown (.md)
75
+ - JavaScript (.js)
76
+
77
+ Args:
78
+ file_path (str): Path to the file to validate.
79
+ Returns:
80
+ str: Validation status message. Example:
81
+ - "✅ Syntax OK"
82
+ - "⚠️ Warning: Syntax error: <error message>"
83
+ - "⚠️ Warning: Unsupported file extension: <ext>"
84
+ """
85
+
86
+ tool_name = "validate_file_syntax"
87
+
88
+ def run(self, file_path: str) -> str:
89
+ disp_path = display_path(file_path)
90
+ self.report_action(
91
+ tr(
92
+ "🔎 Validate syntax for file '{disp_path}' ...",
93
+ disp_path=disp_path,
94
+ ),
95
+ ReportAction.READ,
96
+ )
97
+ result = validate_file_syntax(
98
+ file_path,
99
+ report_info=self.report_info,
100
+ report_warning=self.report_warning,
101
+ report_success=self.report_success,
102
+ )
103
+ if result.startswith("✅"):
104
+ self.report_success(result, ReportAction.READ)
105
+
106
+ return result
@@ -1,35 +1,35 @@
1
- from janito.i18n import tr
2
- import re
3
-
4
-
5
- def validate_css(file_path: str) -> str:
6
- with open(file_path, "r", encoding="utf-8") as f:
7
- content = f.read()
8
- errors = []
9
- # Check for unmatched curly braces
10
- if content.count("{") != content.count("}"):
11
- errors.append("Unmatched curly braces { }")
12
- # Check for unclosed comments
13
- if content.count("/*") != content.count("*/"):
14
- errors.append("Unclosed comment (/* ... */)")
15
- # Check for invalid property declarations (very basic)
16
- for i, line in enumerate(content.splitlines(), 1):
17
- # Ignore empty lines and comments
18
- if not line.strip() or line.strip().startswith("/*"):
19
- continue
20
- # Match property: value; (allow whitespace)
21
- if ":" in line and not re.search(r":.*;", line):
22
- errors.append(
23
- f"Line {i}: Missing semicolon after property value | {line.strip()}"
24
- )
25
- # Match lines with property but missing colon
26
- if ";" in line and ":" not in line:
27
- errors.append(
28
- f"Line {i}: Missing colon in property declaration | {line.strip()}"
29
- )
30
- if errors:
31
- msg = tr(
32
- "⚠️ Warning: CSS syntax issues found:\n{errors}", errors="\n".join(errors)
33
- )
34
- return msg
35
- return "✅ OK"
1
+ from janito.i18n import tr
2
+ import re
3
+
4
+
5
+ def validate_css(file_path: str) -> str:
6
+ with open(file_path, "r", encoding="utf-8") as f:
7
+ content = f.read()
8
+ errors = []
9
+ # Check for unmatched curly braces
10
+ if content.count("{") != content.count("}"):
11
+ errors.append("Unmatched curly braces { }")
12
+ # Check for unclosed comments
13
+ if content.count("/*") != content.count("*/"):
14
+ errors.append("Unclosed comment (/* ... */)")
15
+ # Check for invalid property declarations (very basic)
16
+ for i, line in enumerate(content.splitlines(), 1):
17
+ # Ignore empty lines and comments
18
+ if not line.strip() or line.strip().startswith("/*"):
19
+ continue
20
+ # Match property: value; (allow whitespace)
21
+ if ":" in line and not re.search(r":.*;", line):
22
+ errors.append(
23
+ f"Line {i}: Missing semicolon after property value | {line.strip()}"
24
+ )
25
+ # Match lines with property but missing colon
26
+ if ";" in line and ":" not in line:
27
+ errors.append(
28
+ f"Line {i}: Missing colon in property declaration | {line.strip()}"
29
+ )
30
+ if errors:
31
+ msg = tr(
32
+ "⚠️ Warning: CSS syntax issues found:\n{errors}", errors="\n".join(errors)
33
+ )
34
+ return msg
35
+ return "✅ OK"