jac-coder 0.1.0__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.
- jac_coder/__init__.jac +0 -0
- jac_coder/api.jac +82 -0
- jac_coder/cli_entry.py +25 -0
- jac_coder/config.jac +36 -0
- jac_coder/context.jac +17 -0
- jac_coder/data/examples/ai_agent.md +90 -0
- jac_coder/data/examples/blog_app.md +386 -0
- jac_coder/data/examples/core_patterns.md +321 -0
- jac_coder/data/examples/todo_app.md +321 -0
- jac_coder/data/reference/ai.md +131 -0
- jac_coder/data/reference/backend.md +215 -0
- jac_coder/data/reference/frontend.md +271 -0
- jac_coder/data/reference/osp.md +229 -0
- jac_coder/data/reference/pitfalls.md +141 -0
- jac_coder/data/reference/syntax.md +159 -0
- jac_coder/data/rules/core_jac.md +559 -0
- jac_coder/data/rules/fullstack.md +362 -0
- jac_coder/data/rules/workflow.md +88 -0
- jac_coder/events.jac +110 -0
- jac_coder/impl/api.impl.jac +399 -0
- jac_coder/impl/config.impl.jac +163 -0
- jac_coder/impl/context.impl.jac +117 -0
- jac_coder/impl/mcp_manager.impl.jac +380 -0
- jac_coder/impl/memory.impl.jac +247 -0
- jac_coder/impl/nodes.impl.jac +259 -0
- jac_coder/impl/permission.impl.jac +62 -0
- jac_coder/impl/walkers.impl.jac +298 -0
- jac_coder/mcp_manager.jac +35 -0
- jac_coder/memory.jac +15 -0
- jac_coder/nodes.jac +306 -0
- jac_coder/permission.jac +19 -0
- jac_coder/serve_entry.jac +30 -0
- jac_coder/server.jac +324 -0
- jac_coder/tool/__init__.jac +17 -0
- jac_coder/tool/checked.jac +10 -0
- jac_coder/tool/delegation.jac +23 -0
- jac_coder/tool/filesystem.jac +25 -0
- jac_coder/tool/git.jac +18 -0
- jac_coder/tool/guarded.jac +23 -0
- jac_coder/tool/impl/checked.impl.jac +38 -0
- jac_coder/tool/impl/delegation.impl.jac +157 -0
- jac_coder/tool/impl/filesystem.impl.jac +781 -0
- jac_coder/tool/impl/git.impl.jac +115 -0
- jac_coder/tool/impl/guarded.impl.jac +72 -0
- jac_coder/tool/impl/jac_analyzer.impl.jac +593 -0
- jac_coder/tool/impl/jac_docs.impl.jac +136 -0
- jac_coder/tool/impl/jac_tools.impl.jac +79 -0
- jac_coder/tool/impl/mcp.impl.jac +32 -0
- jac_coder/tool/impl/preview.impl.jac +233 -0
- jac_coder/tool/impl/question.impl.jac +29 -0
- jac_coder/tool/impl/scaffold.impl.jac +231 -0
- jac_coder/tool/impl/search.impl.jac +85 -0
- jac_coder/tool/impl/shell.impl.jac +89 -0
- jac_coder/tool/impl/task.impl.jac +12 -0
- jac_coder/tool/impl/think.impl.jac +4 -0
- jac_coder/tool/impl/todo.impl.jac +58 -0
- jac_coder/tool/impl/validate.impl.jac +236 -0
- jac_coder/tool/impl/web.impl.jac +91 -0
- jac_coder/tool/jac_analyzer.jac +21 -0
- jac_coder/tool/jac_docs.jac +9 -0
- jac_coder/tool/jac_tools.jac +11 -0
- jac_coder/tool/mcp.jac +17 -0
- jac_coder/tool/preview.jac +31 -0
- jac_coder/tool/question.jac +7 -0
- jac_coder/tool/scaffold.jac +10 -0
- jac_coder/tool/search.jac +14 -0
- jac_coder/tool/shell.jac +12 -0
- jac_coder/tool/task.jac +9 -0
- jac_coder/tool/think.jac +5 -0
- jac_coder/tool/todo.jac +12 -0
- jac_coder/tool/validate.jac +11 -0
- jac_coder/tool/vision.jac +17 -0
- jac_coder/tool/web.jac +10 -0
- jac_coder/util/__init__.jac +18 -0
- jac_coder/util/colors.jac +20 -0
- jac_coder/util/impl/sandbox.impl.jac +38 -0
- jac_coder/util/impl/tool_output.impl.jac +208 -0
- jac_coder/util/sandbox.jac +8 -0
- jac_coder/util/tool_output.jac +29 -0
- jac_coder/walkers.jac +67 -0
- jac_coder-0.1.0.dist-info/METADATA +9 -0
- jac_coder-0.1.0.dist-info/RECORD +85 -0
- jac_coder-0.1.0.dist-info/WHEEL +5 -0
- jac_coder-0.1.0.dist-info/entry_points.txt +3 -0
- jac_coder-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,781 @@
|
|
|
1
|
+
# ===========================================================================
|
|
2
|
+
# JS-to-Jac Sanitization Pipeline
|
|
3
|
+
# ===========================================================================
|
|
4
|
+
|
|
5
|
+
# JS built-in classes that need Reflect.construct() to get an object instance.
|
|
6
|
+
# Date() -> returns a string (no methods)
|
|
7
|
+
# new Date() -> returns Date object (has .getFullYear(), .toISOString(), etc.)
|
|
8
|
+
# Jac has no `new` keyword, so: Reflect.construct(Date, [])
|
|
9
|
+
glob _JS_CONSTRUCTORS: list = [
|
|
10
|
+
"Date",
|
|
11
|
+
"WebSocket",
|
|
12
|
+
"TextDecoder",
|
|
13
|
+
"TextEncoder",
|
|
14
|
+
"Uint8Array",
|
|
15
|
+
"Int8Array",
|
|
16
|
+
"Uint16Array",
|
|
17
|
+
"Float32Array",
|
|
18
|
+
"Float64Array",
|
|
19
|
+
"ArrayBuffer",
|
|
20
|
+
"DataView",
|
|
21
|
+
"Map",
|
|
22
|
+
"Set",
|
|
23
|
+
"WeakMap",
|
|
24
|
+
"WeakSet",
|
|
25
|
+
"Error",
|
|
26
|
+
"TypeError",
|
|
27
|
+
"RangeError",
|
|
28
|
+
"SyntaxError",
|
|
29
|
+
"ReferenceError",
|
|
30
|
+
"URL",
|
|
31
|
+
"URLSearchParams",
|
|
32
|
+
"FormData",
|
|
33
|
+
"Blob",
|
|
34
|
+
"File",
|
|
35
|
+
"FileReader",
|
|
36
|
+
"Headers",
|
|
37
|
+
"Request",
|
|
38
|
+
"Response",
|
|
39
|
+
"RegExp",
|
|
40
|
+
"Promise",
|
|
41
|
+
"AbortController",
|
|
42
|
+
"MutationObserver",
|
|
43
|
+
"ResizeObserver",
|
|
44
|
+
"IntersectionObserver",
|
|
45
|
+
"BroadcastChannel",
|
|
46
|
+
"MessageChannel",
|
|
47
|
+
"Worker"
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
# Browser globals that should not be used as function names inside components.
|
|
51
|
+
glob _BROWSER_GLOBALS: list = [
|
|
52
|
+
"open",
|
|
53
|
+
"close",
|
|
54
|
+
"print",
|
|
55
|
+
"focus",
|
|
56
|
+
"blur",
|
|
57
|
+
"scroll",
|
|
58
|
+
"fetch",
|
|
59
|
+
"stop",
|
|
60
|
+
"find",
|
|
61
|
+
"alert",
|
|
62
|
+
"confirm",
|
|
63
|
+
"prompt"
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _fix_template_literal(m: Any) -> str {
|
|
68
|
+
inner = m.group(1);
|
|
69
|
+
inner = inner.replace("${", "{");
|
|
70
|
+
return "f\"" + inner + "\"";
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _sanitize_jac(content: str, file_path: str) -> tuple {
|
|
75
|
+
fixes: list = [];
|
|
76
|
+
is_client = file_path.endswith(".cl.jac");
|
|
77
|
+
|
|
78
|
+
# --- const / let / var declarations -> bare assignment ---
|
|
79
|
+
before = content;
|
|
80
|
+
content = re.sub(r'(?m)^(\s*)(const|let|var)\s+', r'\1', content);
|
|
81
|
+
if content != before {
|
|
82
|
+
n = len(re.findall(r'(?m)^(\s*)(const|let|var)\s+', before));
|
|
83
|
+
fixes.append(
|
|
84
|
+
"Removed " + str(n) + " JS declaration keyword(s) (const/let/var)"
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
# --- def: pub / def: priv -> def:pub / def:priv (no space) ---
|
|
89
|
+
before = content;
|
|
90
|
+
content = re.sub(r'\bdef:\s+pub\b', 'def:pub', content);
|
|
91
|
+
content = re.sub(r'\bdef:\s+priv\b', 'def:priv', content);
|
|
92
|
+
if content != before {
|
|
93
|
+
fixes.append("Fixed 'def: pub' to 'def:pub' (no space after colon)");
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
# --- for p: Type in ... -> for p in ... (no type on loop var) ---
|
|
97
|
+
before = content;
|
|
98
|
+
content = re.sub(r'\bfor\s+(\w+)\s*:\s*\w+\s+in\b', r'for \1 in', content);
|
|
99
|
+
if content != before {
|
|
100
|
+
fixes.append("Fixed typed loop variable: 'for p: Type in ...' to 'for p in ...' (Jac doesn't support type annotation on loop var)");
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
# --- Tuple unpacking: for i, k in ... -> for (i, k) in ... ---
|
|
104
|
+
before = content;
|
|
105
|
+
content = re.sub(r'\bfor\s+(\w+)\s*,\s*(\w+)\s+in\b', r'for (\1, \2) in', content);
|
|
106
|
+
if content != before {
|
|
107
|
+
fixes.append("Fixed tuple unpacking: added parentheses (Jac requires `for (a, b) in ...`)");
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
# --- Strict equality: === -> ==, !== -> != ---
|
|
111
|
+
before = content;
|
|
112
|
+
content = content.replace("!==", "!=");
|
|
113
|
+
content = content.replace("===", "==");
|
|
114
|
+
if content != before {
|
|
115
|
+
fixes.append("Fixed strict equality (===/!==) to Jac equality (==/!=)");
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
# --- Arrow functions: (params) => { -> lambda params: any -> any { ---
|
|
119
|
+
before = content;
|
|
120
|
+
content = re.sub(r'\((\w+)\)\s*=>\s*\{', r'lambda \1: any -> any {', content);
|
|
121
|
+
content = re.sub(r'(?<![.\w])(\w+)\s*=>\s*\{', r'lambda \1: any -> any {', content);
|
|
122
|
+
content = re.sub(
|
|
123
|
+
r'\((\w+),\s*(\w+)\)\s*=>\s*\{', r'lambda \1: any, \2: any -> any {', content
|
|
124
|
+
);
|
|
125
|
+
if content != before {
|
|
126
|
+
fixes.append("Converted JS arrow function(s) to Jac lambda syntax");
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
# --- Template literals: `...${x}...` -> f"...{x}..." ---
|
|
130
|
+
before = content;
|
|
131
|
+
content = re.sub(r'`([^`]*)`', _fix_template_literal, content);
|
|
132
|
+
if content != before {
|
|
133
|
+
fixes.append("Converted JS template literal(s) to Jac f-string(s)");
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
# --- new ClassName(args) -> Reflect.construct(ClassName, [args]) ---
|
|
137
|
+
# Only match PascalCase class names (start with uppercase)
|
|
138
|
+
before = content;
|
|
139
|
+
content = re.sub(
|
|
140
|
+
r'\bnew\s+([A-Z]\w*)\(([^)]*)\)', r'Reflect.construct(\1, [\2])', content
|
|
141
|
+
);
|
|
142
|
+
if content != before {
|
|
143
|
+
fixes.append("Converted new ClassName() to Reflect.construct()");
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
# --- export default / export -> remove ---
|
|
147
|
+
before = content;
|
|
148
|
+
content = re.sub(r'\bexport\s+default\s+', '', content);
|
|
149
|
+
content = re.sub(r'\bexport\s+(?=function|class|const|let|var|def)', '', content);
|
|
150
|
+
if content != before {
|
|
151
|
+
fixes.append("Removed JS export keyword(s)");
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
# --- Import path slashes -> dots (CRITICAL: Jac uses dots, not slashes) ---
|
|
155
|
+
# import from ".ui/card" { Card } -> import from ".ui.card" { Card }
|
|
156
|
+
# import from "..components/ui/button" -> import from "..components.ui.button"
|
|
157
|
+
before = content;
|
|
158
|
+
content = re.sub(
|
|
159
|
+
r'(import\s+from\s+"[.]*)["]',
|
|
160
|
+
lambda m: m.group(0), # no-op anchor
|
|
161
|
+
content
|
|
162
|
+
);
|
|
163
|
+
# Fix slashes inside quoted import paths: "...anything/with/slashes"
|
|
164
|
+
def _fix_import_slashes(m: Any) -> str {
|
|
165
|
+
prefix = m.group(1);
|
|
166
|
+
path = m.group(2);
|
|
167
|
+
fixed_path = path.replace("/", ".");
|
|
168
|
+
return prefix + fixed_path + '"';
|
|
169
|
+
}
|
|
170
|
+
content = re.sub(
|
|
171
|
+
r'(import\s+from\s+")((?:[.]*)[^"]*)"',
|
|
172
|
+
_fix_import_slashes,
|
|
173
|
+
content
|
|
174
|
+
);
|
|
175
|
+
if content != before {
|
|
176
|
+
fixes.append("Fixed import path slashes to dots (Jac uses dots, not slashes)");
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
# --- dict.get() not valid in .cl.jac — warn agent ---
|
|
180
|
+
if is_client {
|
|
181
|
+
get_calls = re.findall(r'\w+\.get\s*\(\s*"[^"]*"', content);
|
|
182
|
+
if get_calls {
|
|
183
|
+
fixes.append(
|
|
184
|
+
"WARNING: .get('key') is not valid in .cl.jac — use d[\"key\"] with guard: "
|
|
185
|
+
+ "value = d[\"key\"] if \"key\" in d else default;"
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
# --- type(x) == dict not valid in .cl.jac ---
|
|
191
|
+
if is_client {
|
|
192
|
+
type_checks = re.findall(r'type\s*\(\s*\w+\s*\)\s*==\s*(?:dict|list|str|int|float|bool)', content);
|
|
193
|
+
if type_checks {
|
|
194
|
+
fixes.append(
|
|
195
|
+
"WARNING: type(x) == dict is not valid in .cl.jac (Python types don't exist in JS runtime). "
|
|
196
|
+
+ "Use 'key' in x to guard dict access, or typeof for JS type checks."
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
# --- Reflect.globalThis → globalThis (Reflect doesn't have globalThis) ---
|
|
202
|
+
if is_client {
|
|
203
|
+
before = content;
|
|
204
|
+
content = content.replace("Reflect.globalThis.", "globalThis.");
|
|
205
|
+
if content != before {
|
|
206
|
+
fixes.append("Fixed Reflect.globalThis to globalThis (Reflect is only for construct())");
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
# --- Inline def/lambda in JSX attributes (CRITICAL .cl.jac error) ---
|
|
211
|
+
if is_client {
|
|
212
|
+
inline_defs = re.findall(r'=\{(?:def|lambda)\s*\(', content);
|
|
213
|
+
inline_lambdas = re.findall(r'=\{lambda\s', content);
|
|
214
|
+
if inline_defs or inline_lambdas {
|
|
215
|
+
fixes.append(
|
|
216
|
+
"WARNING: Found inline def(...) or lambda inside JSX attributes. "
|
|
217
|
+
+ "Extract to a named def handler ABOVE the return statement. "
|
|
218
|
+
+ "Example: def handle_click() -> None { ... } then onClick={handle_click}"
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
# --- ! operator → not (in .cl.jac, Jac uses 'not' not '!') ---
|
|
224
|
+
if is_client {
|
|
225
|
+
before = content;
|
|
226
|
+
# Replace !variable but not !== (already handled) and not !important (CSS)
|
|
227
|
+
content = re.sub(r'(?<!=)!(?!=)(?!important)(\w)', r'not \1', content);
|
|
228
|
+
if content != before {
|
|
229
|
+
fixes.append("Fixed ! operator to 'not' (Jac uses 'not', not '!')");
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
# --- Missing def:pub on .cl.jac component functions ---
|
|
234
|
+
if is_client {
|
|
235
|
+
# Find def FunctionName() -> JsxElement without :pub
|
|
236
|
+
missing_pub = re.findall(r'^def\s+[A-Z]\w*\s*\(.*\)\s*->\s*JsxElement', content, re.MULTILINE);
|
|
237
|
+
if missing_pub {
|
|
238
|
+
fixes.append(
|
|
239
|
+
"WARNING: Component function missing def:pub — .cl.jac components must use def:pub to be importable. "
|
|
240
|
+
+ "Example: def:pub Layout() -> JsxElement { ... }"
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
# --- JS constructor().method() pattern for known built-ins ---
|
|
246
|
+
# e.g. Date().getFullYear() -> Reflect.construct(Date, []).getFullYear()
|
|
247
|
+
# Only when NOT already wrapped in Reflect.construct
|
|
248
|
+
if is_client {
|
|
249
|
+
for cls in _JS_CONSTRUCTORS {
|
|
250
|
+
pattern = r'(?<!construct\()(?<!\w)' + cls + r'\(([^)]*)\)\.';
|
|
251
|
+
if re.search(pattern, content) {
|
|
252
|
+
replacement = "Reflect.construct(" + cls + r", [\1]).";
|
|
253
|
+
content = re.sub(pattern, replacement, content);
|
|
254
|
+
fixes.append(
|
|
255
|
+
"Fixed " + cls + "().method() to Reflect.construct(" + cls + ", [...]).method()"
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
# --- Browser global name conflicts ---
|
|
262
|
+
if is_client {
|
|
263
|
+
for global_name in _BROWSER_GLOBALS {
|
|
264
|
+
if re.search(r'\bdef\s+' + global_name + r'\s*\(', content) {
|
|
265
|
+
cap_name = global_name[0].upper() + global_name[1:];
|
|
266
|
+
fixes.append(
|
|
267
|
+
"WARNING: def " + global_name + "() shadows browser global window." + global_name + " - rename to handle" + cap_name + "()"
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return (content, fixes);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
# ===========================================================================
|
|
278
|
+
# Quick Parse Check — syntax only, no type checking (fast, in-process)
|
|
279
|
+
# ===========================================================================
|
|
280
|
+
def _quick_parse_check(file_path: str, result_msg: str) -> str {
|
|
281
|
+
try {
|
|
282
|
+
import from jaclang.jac0core.program { JacProgram }
|
|
283
|
+
prog = JacProgram();
|
|
284
|
+
module = prog.compile(file_path=file_path, type_check=False, no_cgen=True);
|
|
285
|
+
if module is None and prog.errors_had {
|
|
286
|
+
error_lines: list = [];
|
|
287
|
+
for e in prog.errors_had[:10] {
|
|
288
|
+
error_lines.append(str(e));
|
|
289
|
+
}
|
|
290
|
+
errors = "\n".join(error_lines);
|
|
291
|
+
hints = _get_fix_hints(errors);
|
|
292
|
+
result_msg = result_msg + "\n\nSYNTAX ERRORS:\n" + errors;
|
|
293
|
+
if hints {
|
|
294
|
+
result_msg = result_msg + "\n\nFIX HINTS:\n" + hints;
|
|
295
|
+
}
|
|
296
|
+
result_msg = result_msg + "\nFix syntax errors before continuing.";
|
|
297
|
+
}
|
|
298
|
+
} except Exception { }
|
|
299
|
+
return result_msg;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def _get_fix_hints(errors: str) -> str {
|
|
304
|
+
hints: list = [];
|
|
305
|
+
err_lower = errors.lower();
|
|
306
|
+
|
|
307
|
+
if "?" in errors and ("unexpected" in err_lower or "ternary" in err_lower) {
|
|
308
|
+
hints.append("- Replace `cond ? a : b` with `(a if cond else b)`");
|
|
309
|
+
}
|
|
310
|
+
if "=>" in errors {
|
|
311
|
+
hints.append("- Replace `(x) => {` with `lambda x: any -> any {`");
|
|
312
|
+
}
|
|
313
|
+
if "const " in errors or "let " in errors or "var " in errors {
|
|
314
|
+
hints.append("- Remove const/let/var keywords. Just use: x = value;");
|
|
315
|
+
}
|
|
316
|
+
if "new " in errors {
|
|
317
|
+
hints.append("- Replace `new X(args)` with `Reflect.construct(X, [args])`");
|
|
318
|
+
}
|
|
319
|
+
if "semicolon" in err_lower or "expected `;`" in err_lower {
|
|
320
|
+
hints.append("- Every statement must end with `;`");
|
|
321
|
+
}
|
|
322
|
+
if "import" in err_lower and ("error" in err_lower or "not found" in err_lower) {
|
|
323
|
+
hints.append("- Never include .cl or .jac extensions in import paths");
|
|
324
|
+
hints.append("- Use: import from .components.Name { Name }");
|
|
325
|
+
}
|
|
326
|
+
if "component" in err_lower and "unexpected" in err_lower {
|
|
327
|
+
hints.append(
|
|
328
|
+
"- No `component` keyword. Use: def:pub Name(props: dict) -> JsxElement { }"
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
if "export" in err_lower {
|
|
332
|
+
hints.append(
|
|
333
|
+
"- Remove `export`/`export default`. Use def:pub for public functions"
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
if "return type" in err_lower or "type" in err_lower and "expected" in err_lower {
|
|
337
|
+
hints.append(
|
|
338
|
+
"- Components return JsxElement, hooks return dict, handlers return None"
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
return "\n".join(hints) if hints else "";
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
# ===========================================================================
|
|
347
|
+
# Runtime Error Detection (Preview Log)
|
|
348
|
+
# ===========================================================================
|
|
349
|
+
def _get_preview_log_pos(file_path: str) -> tuple {
|
|
350
|
+
if not file_path.endswith(".cl.jac") {
|
|
351
|
+
return ("", 0);
|
|
352
|
+
}
|
|
353
|
+
if not os.environ.get("JACCODER_WEB_MODE", "") {
|
|
354
|
+
return ("", 0);
|
|
355
|
+
}
|
|
356
|
+
search_dir = os.path.dirname(file_path);
|
|
357
|
+
for _ in range(5) {
|
|
358
|
+
candidate = os.path.join(search_dir, ".jac-preview.log");
|
|
359
|
+
if os.path.isfile(candidate) {
|
|
360
|
+
return (candidate, os.path.getsize(candidate));
|
|
361
|
+
}
|
|
362
|
+
parent = os.path.dirname(search_dir);
|
|
363
|
+
if parent == search_dir {
|
|
364
|
+
break;
|
|
365
|
+
}
|
|
366
|
+
search_dir = parent;
|
|
367
|
+
}
|
|
368
|
+
return ("", 0);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def _check_preview_log(
|
|
373
|
+
file_path: str, result_msg: str, log_path: str = "", log_pos: int = 0
|
|
374
|
+
) -> str {
|
|
375
|
+
if not file_path.endswith(".cl.jac") {
|
|
376
|
+
return result_msg;
|
|
377
|
+
}
|
|
378
|
+
if not log_path {
|
|
379
|
+
return result_msg;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
# Give Vite HMR time to process the file change
|
|
383
|
+
time.sleep(1.5);
|
|
384
|
+
|
|
385
|
+
if not os.path.isfile(log_path) {
|
|
386
|
+
return result_msg;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
try {
|
|
390
|
+
# Only read NEW log lines that appeared after the write
|
|
391
|
+
with open(log_path, "r") as f {
|
|
392
|
+
f.seek(log_pos);
|
|
393
|
+
new_content = f.read();
|
|
394
|
+
}
|
|
395
|
+
if not new_content.strip() {
|
|
396
|
+
return result_msg;
|
|
397
|
+
}
|
|
398
|
+
recent = new_content.strip().splitlines();
|
|
399
|
+
|
|
400
|
+
# Filter "Failed to resolve import" only if the source .cl.jac file exists
|
|
401
|
+
# (transient HMR timing issue). If the source file is missing, it's a REAL error.
|
|
402
|
+
filtered: list = [];
|
|
403
|
+
project_dir = os.path.dirname(file_path);
|
|
404
|
+
# Walk up to find project root (where jac.toml lives)
|
|
405
|
+
search_dir = project_dir;
|
|
406
|
+
for _ in range(5) {
|
|
407
|
+
if os.path.isfile(os.path.join(search_dir, "jac.toml")) {
|
|
408
|
+
project_dir = search_dir;
|
|
409
|
+
break;
|
|
410
|
+
}
|
|
411
|
+
parent = os.path.dirname(search_dir);
|
|
412
|
+
if parent == search_dir {
|
|
413
|
+
break;
|
|
414
|
+
}
|
|
415
|
+
search_dir = parent;
|
|
416
|
+
}
|
|
417
|
+
for line in recent {
|
|
418
|
+
if "Failed to resolve import" in line {
|
|
419
|
+
# Extract the import path from the error message
|
|
420
|
+
# Format: Failed to resolve import "../components/ui/badge.js" from "..."
|
|
421
|
+
import_match = re.search(r'Failed to resolve import "([^"]+)"', line);
|
|
422
|
+
if import_match {
|
|
423
|
+
import_path = import_match.group(1);
|
|
424
|
+
# Convert .js import to .cl.jac source path
|
|
425
|
+
source_path = import_path.replace(".js", ".cl.jac");
|
|
426
|
+
# Resolve relative to the file being written
|
|
427
|
+
abs_source = os.path.normpath(
|
|
428
|
+
os.path.join(os.path.dirname(file_path), source_path)
|
|
429
|
+
);
|
|
430
|
+
if os.path.isfile(abs_source) {
|
|
431
|
+
# Source exists — transient HMR timing, skip it
|
|
432
|
+
continue;
|
|
433
|
+
}
|
|
434
|
+
# Also check from project root (compiled/ paths)
|
|
435
|
+
stripped = source_path.lstrip("./");
|
|
436
|
+
if os.path.isfile(os.path.join(project_dir, stripped)) {
|
|
437
|
+
continue;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
# Source doesn't exist — real error, keep it
|
|
441
|
+
filtered.append(
|
|
442
|
+
f"MISSING IMPORT: {line.strip()} — Create the missing .cl.jac file FIRST."
|
|
443
|
+
);
|
|
444
|
+
} else {
|
|
445
|
+
filtered.append(line);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
recent = filtered;
|
|
449
|
+
|
|
450
|
+
error_patterns = [
|
|
451
|
+
"is not a function",
|
|
452
|
+
"is not defined",
|
|
453
|
+
"is not a constructor",
|
|
454
|
+
"Cannot read properties of",
|
|
455
|
+
"Cannot read property",
|
|
456
|
+
"Unexpected token",
|
|
457
|
+
"TypeError:",
|
|
458
|
+
"ReferenceError:",
|
|
459
|
+
"SyntaxError:",
|
|
460
|
+
"Uncaught Error"
|
|
461
|
+
];
|
|
462
|
+
|
|
463
|
+
found_errors: list = [];
|
|
464
|
+
for line in recent {
|
|
465
|
+
for pattern in error_patterns {
|
|
466
|
+
if pattern in line and line.strip() not in found_errors {
|
|
467
|
+
found_errors.append(line.strip());
|
|
468
|
+
break;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
if found_errors {
|
|
474
|
+
result_msg = result_msg + "\n\nRUNTIME ERRORS (from live preview):";
|
|
475
|
+
for err in found_errors[:5] {
|
|
476
|
+
result_msg = result_msg + "\n " + err;
|
|
477
|
+
}
|
|
478
|
+
if len(found_errors) > 5 {
|
|
479
|
+
result_msg = result_msg + "\n ... and " + str(len(found_errors) - 5) + " more";
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
# Add targeted fix hints based on error content
|
|
483
|
+
hint_set: set = set();
|
|
484
|
+
for err in found_errors {
|
|
485
|
+
if "is not a function" in err {
|
|
486
|
+
hint_set.add(
|
|
487
|
+
"- Method on constructor result? Use Reflect.construct(ClassName, [args]).method()"
|
|
488
|
+
);
|
|
489
|
+
}
|
|
490
|
+
if "is not defined" in err {
|
|
491
|
+
hint_set.add("- Check imports: missing import or wrong path?");
|
|
492
|
+
}
|
|
493
|
+
if "is not a constructor" in err {
|
|
494
|
+
hint_set.add(
|
|
495
|
+
"- Use Reflect.construct(Class, [args]) instead of Class(args)"
|
|
496
|
+
);
|
|
497
|
+
}
|
|
498
|
+
if "Cannot read properties of undefined" in err
|
|
499
|
+
or "Cannot read property" in err {
|
|
500
|
+
hint_set.add(
|
|
501
|
+
"- Accessing property on undefined. Check variable initialization and async timing"
|
|
502
|
+
);
|
|
503
|
+
}
|
|
504
|
+
if "Unexpected token" in err {
|
|
505
|
+
hint_set.add(
|
|
506
|
+
"- Syntax error in compiled output. Check for JS-only syntax (ternary, arrow fn, spread)"
|
|
507
|
+
);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
if hint_set {
|
|
511
|
+
result_msg = result_msg + "\n\nRUNTIME FIX HINTS:";
|
|
512
|
+
for hint in hint_set {
|
|
513
|
+
result_msg = result_msg + "\n" + hint;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
result_msg = result_msg + "\nCall jac_docs(query) to look up correct syntax BEFORE fixing. Do NOT guess.";
|
|
517
|
+
}
|
|
518
|
+
} except Exception { }
|
|
519
|
+
|
|
520
|
+
return result_msg;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
|
|
524
|
+
# ===========================================================================
|
|
525
|
+
# Tool Implementations
|
|
526
|
+
# ===========================================================================
|
|
527
|
+
impl read_file(file_path: str, offset: int = 0, limit: int = 2000) -> str {
|
|
528
|
+
(file_path, err) = sandbox_path(file_path);
|
|
529
|
+
if err {
|
|
530
|
+
return err;
|
|
531
|
+
}
|
|
532
|
+
tool_status("read", file_path);
|
|
533
|
+
p = Path(file_path);
|
|
534
|
+
|
|
535
|
+
if not p.exists() {
|
|
536
|
+
return f"Error: File not found: {file_path}";
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
if p.is_dir() {
|
|
540
|
+
entries = sorted(os.listdir(str(p)));
|
|
541
|
+
listing = [];
|
|
542
|
+
for entry in entries {
|
|
543
|
+
full = os.path.join(str(p), entry);
|
|
544
|
+
if os.path.isdir(full) {
|
|
545
|
+
listing.append(f" {entry}/");
|
|
546
|
+
} else {
|
|
547
|
+
size = os.path.getsize(full);
|
|
548
|
+
listing.append(f" {entry} ({size} bytes)");
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
return f"Directory: {file_path}\n" + "\n".join(listing);
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
(mime, _) = mimetypes.guess_type(str(p));
|
|
555
|
+
if mime and not mime.startswith("text/") and mime != "application/json" {
|
|
556
|
+
size = os.path.getsize(str(p));
|
|
557
|
+
return f"Binary file: {file_path} ({size} bytes, type: {mime})";
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
try {
|
|
561
|
+
content = p.read_text();
|
|
562
|
+
} except Exception as e {
|
|
563
|
+
return f"Error reading {file_path}: {str(e)}";
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
lines = content.splitlines();
|
|
567
|
+
total = len(lines);
|
|
568
|
+
|
|
569
|
+
if offset > 0 {
|
|
570
|
+
lines = lines[offset:offset + limit];
|
|
571
|
+
start_num = offset + 1;
|
|
572
|
+
} else {
|
|
573
|
+
lines = lines[:limit];
|
|
574
|
+
start_num = 1;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
numbered = [f"{i + start_num}: {line}" for (i, line) in enumerate(lines)];
|
|
578
|
+
result = "\n".join(numbered);
|
|
579
|
+
|
|
580
|
+
(result, was_truncated) = truncate_output(result);
|
|
581
|
+
|
|
582
|
+
if was_truncated {
|
|
583
|
+
result = result + "\n\n⚠ Output truncated. Use offset/limit to read remaining lines.";
|
|
584
|
+
}
|
|
585
|
+
if total > len(lines) {
|
|
586
|
+
result = result + f"\n\n({total} total lines)";
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
tool_end(f"-> {total} lines");
|
|
590
|
+
return result;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
|
|
594
|
+
impl write_file(file_path: str, content: str) -> str {
|
|
595
|
+
(file_path, err) = sandbox_path(file_path);
|
|
596
|
+
if err {
|
|
597
|
+
return err;
|
|
598
|
+
}
|
|
599
|
+
tool_status("write", file_path);
|
|
600
|
+
p = Path(file_path);
|
|
601
|
+
try {
|
|
602
|
+
p.parent.mkdir(parents=True, exist_ok=True);
|
|
603
|
+
|
|
604
|
+
# Snapshot preview log position BEFORE writing
|
|
605
|
+
(log_path, log_pos) = _get_preview_log_pos(file_path);
|
|
606
|
+
|
|
607
|
+
# Stage 1: Pre-write sanitization for .jac files
|
|
608
|
+
auto_fixes: list = [];
|
|
609
|
+
if file_path.endswith(".jac") {
|
|
610
|
+
(content, auto_fixes) = _sanitize_jac(content, file_path);
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
p.write_text(content);
|
|
614
|
+
line_count = len(content.splitlines());
|
|
615
|
+
emit_event("file_written", {"path": file_path, "lines": line_count});
|
|
616
|
+
if os.environ.get("JACCODER_WEB_MODE", "") {
|
|
617
|
+
time.sleep(0.3);
|
|
618
|
+
}
|
|
619
|
+
result_msg = f"Written {line_count} lines to {file_path}";
|
|
620
|
+
|
|
621
|
+
# Report auto-fixes so the LLM learns to avoid these patterns
|
|
622
|
+
if auto_fixes {
|
|
623
|
+
result_msg = result_msg + "\n\nAUTO-FIXED JS syntax:";
|
|
624
|
+
for fix in auto_fixes {
|
|
625
|
+
result_msg = result_msg + "\n - " + fix;
|
|
626
|
+
}
|
|
627
|
+
result_msg = result_msg + "\nAvoid these JS patterns in future writes.";
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
# Write succeeded — close the Write step as success
|
|
631
|
+
tool_end(f"-> {line_count} lines");
|
|
632
|
+
|
|
633
|
+
# Stage 2: Compiler validation (separate step so errors show on "Check", not "Write")
|
|
634
|
+
if file_path.endswith(".jac") {
|
|
635
|
+
tool_status("jac_check", file_path);
|
|
636
|
+
result_msg = _quick_parse_check(file_path, result_msg);
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
# Stage 3: Runtime error detection (only NEW log lines since write)
|
|
640
|
+
result_msg = _check_preview_log(file_path, result_msg, log_path, log_pos);
|
|
641
|
+
|
|
642
|
+
has_errors = "VALIDATION ERRORS" in result_msg or "RUNTIME ERRORS" in result_msg;
|
|
643
|
+
if file_path.endswith(".jac") {
|
|
644
|
+
tool_end(
|
|
645
|
+
"-> " + ("errors found" if has_errors else "ok"), error=has_errors
|
|
646
|
+
);
|
|
647
|
+
}
|
|
648
|
+
return result_msg;
|
|
649
|
+
} except Exception as e {
|
|
650
|
+
tool_end("-> error", error=True);
|
|
651
|
+
return f"Error writing {file_path}: {str(e)}";
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
|
|
656
|
+
impl edit_file(
|
|
657
|
+
file_path: str, old_string: str, new_string: str, replace_all: bool = False
|
|
658
|
+
) -> str {
|
|
659
|
+
(file_path, err) = sandbox_path(file_path);
|
|
660
|
+
if err {
|
|
661
|
+
return err;
|
|
662
|
+
}
|
|
663
|
+
tool_status("edit", file_path);
|
|
664
|
+
p = Path(file_path);
|
|
665
|
+
|
|
666
|
+
if not p.exists() {
|
|
667
|
+
return f"Error: File not found: {file_path}";
|
|
668
|
+
}
|
|
669
|
+
if not old_string {
|
|
670
|
+
return "Error: old_string is required";
|
|
671
|
+
}
|
|
672
|
+
if old_string == new_string {
|
|
673
|
+
return "Error: old_string and new_string must be different";
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
try {
|
|
677
|
+
content = p.read_text();
|
|
678
|
+
} except Exception as e {
|
|
679
|
+
return f"Error reading {file_path}: {str(e)}";
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
content = content.replace("\r\n", "\n");
|
|
683
|
+
old_string = old_string.replace("\r\n", "\n");
|
|
684
|
+
new_string = new_string.replace("\r\n", "\n");
|
|
685
|
+
|
|
686
|
+
# Stage 1: Sanitize the new_string before replacement
|
|
687
|
+
auto_fixes: list = [];
|
|
688
|
+
if file_path.endswith(".jac") {
|
|
689
|
+
(new_string, auto_fixes) = _sanitize_jac(new_string, file_path);
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
count = content.count(old_string);
|
|
693
|
+
|
|
694
|
+
if count == 0 {
|
|
695
|
+
return "Error: old_string not found in file. Make sure it matches exactly (including whitespace/indentation).";
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
if count > 1 and not replace_all {
|
|
699
|
+
return f"Error: old_string found {count} times. Use replace_all=true or provide more context to make the match unique.";
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
# Snapshot preview log position BEFORE writing
|
|
703
|
+
(log_path, log_pos) = _get_preview_log_pos(file_path);
|
|
704
|
+
|
|
705
|
+
if replace_all {
|
|
706
|
+
new_content = content.replace(old_string, new_string);
|
|
707
|
+
} else {
|
|
708
|
+
new_content = content.replace(old_string, new_string, 1);
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
try {
|
|
712
|
+
p.write_text(new_content);
|
|
713
|
+
} except Exception as e {
|
|
714
|
+
return f"Error writing {file_path}: {str(e)}";
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
replacements = count if replace_all else 1;
|
|
718
|
+
emit_event("file_edited", {"path": file_path, "replacements": replacements});
|
|
719
|
+
result_msg = f"Replaced {replacements} occurrence(s) in {file_path}";
|
|
720
|
+
|
|
721
|
+
# Report auto-fixes
|
|
722
|
+
if auto_fixes {
|
|
723
|
+
result_msg = result_msg + "\n\nAUTO-FIXED JS syntax in new_string:";
|
|
724
|
+
for fix in auto_fixes {
|
|
725
|
+
result_msg = result_msg + "\n - " + fix;
|
|
726
|
+
}
|
|
727
|
+
result_msg = result_msg + "\nAvoid these JS patterns in future edits.";
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
# Edit succeeded — close the Edit step as success
|
|
731
|
+
tool_end(f"-> {replacements} edit(s)");
|
|
732
|
+
|
|
733
|
+
# Stage 2: Compiler validation (separate step)
|
|
734
|
+
if file_path.endswith(".jac") {
|
|
735
|
+
tool_status("jac_check", file_path);
|
|
736
|
+
result_msg = _quick_parse_check(file_path, result_msg);
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
# Stage 3: Runtime error detection (only NEW log lines since edit)
|
|
740
|
+
result_msg = _check_preview_log(file_path, result_msg, log_path, log_pos);
|
|
741
|
+
|
|
742
|
+
has_errors = "VALIDATION ERRORS" in result_msg or "RUNTIME ERRORS" in result_msg;
|
|
743
|
+
if file_path.endswith(".jac") {
|
|
744
|
+
tool_end("-> " + ("errors found" if has_errors else "ok"), error=has_errors);
|
|
745
|
+
}
|
|
746
|
+
return result_msg;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
|
|
750
|
+
impl list_files(directory: str = ".", pattern: str = "**/*") -> str {
|
|
751
|
+
(directory, err) = sandbox_path(directory);
|
|
752
|
+
if err {
|
|
753
|
+
return err;
|
|
754
|
+
}
|
|
755
|
+
tool_status("list", f"{directory} ({pattern})");
|
|
756
|
+
p = Path(directory);
|
|
757
|
+
if not p.exists() {
|
|
758
|
+
return f"Error: Directory not found: {directory}";
|
|
759
|
+
}
|
|
760
|
+
try {
|
|
761
|
+
files = sorted(
|
|
762
|
+
[
|
|
763
|
+
str(f)
|
|
764
|
+
for f in p.glob(pattern)
|
|
765
|
+
if f.is_file() and ".git" not in f.parts
|
|
766
|
+
]
|
|
767
|
+
);
|
|
768
|
+
} except Exception as e {
|
|
769
|
+
return f"Error: {str(e)}";
|
|
770
|
+
}
|
|
771
|
+
if not files {
|
|
772
|
+
tool_end("-> 0 files");
|
|
773
|
+
return f"No files matching '{pattern}' in {directory}";
|
|
774
|
+
}
|
|
775
|
+
total = len(files);
|
|
776
|
+
tool_end(f"-> {total} files");
|
|
777
|
+
if total > 200 {
|
|
778
|
+
return "\n".join(files[:200]) + f"\n\n... and {total - 200} more files ({total} total)";
|
|
779
|
+
}
|
|
780
|
+
return "\n".join(files);
|
|
781
|
+
}
|