localcode 0.3.8__py3-none-any.whl → 0.3.9__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.
localcode/__init__.py CHANGED
@@ -1,3 +1,3 @@
1
1
  __all__ = ["__version__"]
2
2
 
3
- __version__ = "0.3.8"
3
+ __version__ = "0.3.9"
@@ -1,23 +1,22 @@
1
- """One-shot syntax validation for files the agent writes.
2
-
3
- Poor-man's LSP. The big agents (Claude Code, opencode) run a persistent
4
- language server and feed its diagnostics back after each edit; codex uses
5
- verified structured patches. A persistent LSP is too heavy for localcode's
6
- target a 16 GB Mac already running a large local model. So instead we run a
7
- FAST, ONE-SHOT syntax check after each write and, if it fails, append the exact
8
- error to the tool result.
9
-
10
- Why this matters for a LOCAL model specifically: a small quantized model drifts
11
- into structural typos (`useState(0]`, an unterminated string, a missing bracket,
12
- `getCardsForCard`). With no verification the model has to NOTICE its own typo —
13
- which weak models are bad at so it re-reads the broken file, the broken code
14
- enters its context, and it reproduces the error (self-conditioning loop). A
15
- deterministic check gives it GROUND TRUTH ("line 37: unexpected token") so it
16
- fixes the exact line instead of guessing.
17
-
18
- Best-effort and non-blocking: the write ALWAYS succeeds; this only appends a
19
- warning. Any checker failure/absence returns None (no false alarms). Disable
20
- with LOCALCODE_SYNTAX_CHECK=0.
1
+ """One-shot syntax validation for files the agent writes — in-process, offline.
2
+
3
+ Tier-1 verification. The big agents use a persistent language server (LSP);
4
+ opencode's own docs say that's often worse than running diagnostics directly
5
+ (LSPs "get out of sync, use significant memory, slow down agent workflows") —
6
+ decisive for a 16 GB Mac already running a large local model.
7
+
8
+ Crucially this must work with NO tools the user has to install and NO network:
9
+ it's bundled. We parse with tree-sitter (grammars compiled into the wheel — one
10
+ ~4.5 MB native extension covering 100+ languages) and flag any syntax error
11
+ in-process. So when a small quantized model drifts into a structural typo
12
+ (`useState(0]`, an unterminated string, a missing bracket), the write tool
13
+ appends the exact error and line, and the model fixes THAT line instead of
14
+ re-reading the broken file and reproducing the error (the self-conditioning
15
+ loop). Python/JSON keep their native checkers for sharper messages.
16
+
17
+ Semantic/type errors (wrong names, missing imports) are caught separately by
18
+ project_check at completion (tsc/ruff/go/cargo). Disable with
19
+ LOCALCODE_SYNTAX_CHECK=0.
21
20
  """
22
21
  from __future__ import annotations
23
22
 
@@ -27,56 +26,39 @@ import subprocess
27
26
 
28
27
  _TIMEOUT = 5.0
29
28
 
29
+ # File extension → tree-sitter language name (all bundled in the language pack).
30
+ _TS_LANG = {
31
+ ".js": "javascript", ".mjs": "javascript", ".cjs": "javascript",
32
+ ".jsx": "tsx", ".ts": "typescript", ".mts": "typescript", ".cts": "typescript",
33
+ ".tsx": "tsx", ".go": "go", ".rs": "rust", ".rb": "ruby", ".php": "php",
34
+ ".sh": "bash", ".bash": "bash", ".zsh": "bash", ".lua": "lua",
35
+ ".c": "c", ".h": "c", ".cpp": "cpp", ".cc": "cpp", ".cxx": "cpp",
36
+ ".hpp": "cpp", ".hh": "cpp", ".java": "java", ".kt": "kotlin",
37
+ ".swift": "swift", ".cs": "csharp", ".scala": "scala",
38
+ ".css": "css", ".scss": "scss", ".html": "html", ".xml": "xml",
39
+ ".yaml": "yaml", ".yml": "yaml", ".toml": "toml", ".sql": "sql",
40
+ }
30
41
 
31
- def check_syntax(path: str, content: str) -> str | None:
32
- """Return a one-line syntax-error string, or None if OK / not checkable.
33
42
 
34
- In-process for JSON/Python (instant, zero deps); a fast one-shot subprocess
35
- for JS (node --check) and TS/JSX (esbuild, when the project has it).
36
- """
43
+ def check_syntax(path: str, content: str) -> str | None:
44
+ """Return a one-line syntax-error string, or None if OK / not checkable."""
37
45
  if os.environ.get("LOCALCODE_SYNTAX_CHECK") == "0":
38
46
  return None
39
47
  ext = os.path.splitext(path)[1].lower()
40
48
  try:
49
+ # Native checkers give the sharpest messages; use them where free.
41
50
  if ext == ".json":
42
51
  return _check_json(content)
43
52
  if ext in (".py", ".pyi"):
44
53
  return _check_python(content, path)
45
- if ext in (".js", ".mjs", ".cjs"):
46
- return _check_node(path)
47
- if ext in (".ts", ".tsx", ".jsx", ".mts", ".cts"):
48
- return _check_esbuild(path)
49
- # Per-file syntax linters for other languages (best-effort — only if the
50
- # interpreter/tool is on PATH). Whole-project TYPE checks (go/rust/tsc)
51
- # run separately at completion in project_check.
52
- if ext == ".rb":
53
- return _check_cli(["ruby", "-wc", path], "ruby")
54
- if ext == ".php":
55
- return _check_cli(["php", "-l", path], "php")
56
- if ext in (".sh", ".bash"):
57
- return _check_cli(["bash", "-n", path], "bash")
58
- if ext == ".lua":
59
- return _check_cli(["luac", "-p", path], "luac")
60
- if ext == ".rs":
61
- # rustc syntax-only parse (no full build): --emit=metadata is slow;
62
- # a lightweight parse-only pass isn't stable, so skip unless a fast
63
- # tool exists. cargo check at completion covers Rust type errors.
64
- return None
54
+ lang = _TS_LANG.get(ext)
55
+ if lang:
56
+ return _check_treesitter(content, lang)
65
57
  except Exception:
66
58
  return None
67
59
  return None
68
60
 
69
61
 
70
- def _check_cli(argv: list, tool: str) -> str | None:
71
- import shutil
72
- if not shutil.which(tool):
73
- return None
74
- r = subprocess.run(argv, capture_output=True, text=True, timeout=_TIMEOUT)
75
- if r.returncode != 0:
76
- return _first_error_line((r.stderr or "") + "\n" + (r.stdout or ""))
77
- return None
78
-
79
-
80
62
  def _check_json(content: str) -> str | None:
81
63
  try:
82
64
  json.loads(content)
@@ -94,66 +76,47 @@ def _check_python(content: str, path: str) -> str | None:
94
76
  return f"Python syntax error at {loc}: {e.msg}"
95
77
 
96
78
 
97
- def _check_node(path: str) -> str | None:
98
- import shutil
99
- if not shutil.which("node"):
100
- return None
101
- r = subprocess.run(
102
- ["node", "--check", path],
103
- capture_output=True, text=True, timeout=_TIMEOUT,
104
- )
105
- if r.returncode != 0:
106
- return _first_error_line(r.stderr)
107
- return None
79
+ def _check_treesitter(content: str, lang: str) -> str | None:
80
+ """Parse with a bundled tree-sitter grammar; report the first syntax error.
108
81
 
109
-
110
- def _find_bin(start: str, name: str) -> str | None:
111
- """Walk up from the file's dir to find node_modules/.bin/<name>."""
112
- d = os.path.dirname(os.path.abspath(start))
113
- for _ in range(25):
114
- cand = os.path.join(d, "node_modules", ".bin", name)
115
- if os.path.exists(cand):
116
- return cand
117
- parent = os.path.dirname(d)
118
- if parent == d:
119
- break
120
- d = parent
121
- return None
122
-
123
-
124
- def _check_esbuild(path: str) -> str | None:
125
- # esbuild parses TS/TSX/JSX and reports syntax errors instantly. Only used
126
- # when the project has it (Vite/most JS projects do post-install); if not
127
- # present we skip rather than risk a false alarm from a wrong parser.
128
- esbuild = _find_bin(path, "esbuild")
129
- if not esbuild:
130
- return None
131
- loader = "tsx" if path.endswith((".tsx", ".jsx")) else "ts"
132
- r = subprocess.run(
133
- [esbuild, path, f"--loader={loader}", "--log-level=error"],
134
- capture_output=True, text=True, timeout=_TIMEOUT,
135
- )
136
- if r.returncode != 0 and r.stderr.strip():
137
- return _first_error_line(r.stderr)
138
- return None
139
-
140
-
141
- def _first_error_line(stderr: str) -> str | None:
142
- """Pull the human-meaningful error out of a node/esbuild stderr dump.
143
-
144
- Prefer the actual `SyntaxError: …` / `error: …` message, skipping node's
145
- internal loader/stack frames (`node:internal/…`, ` at …`).
82
+ No external tools, no network — the grammars are compiled into the wheel.
83
+ Returns None if tree-sitter isn't available (graceful) or the file parses.
146
84
  """
147
- lines = [l.rstrip() for l in stderr.splitlines()]
148
- # First choice: the real error message line.
149
- for s in lines:
150
- t = s.strip()
151
- if t.startswith(("SyntaxError", "TypeError", "ReferenceError")) or " error:" in t.lower() or t.lower().startswith("error"):
152
- return t[:200]
153
- # Fallback: the "file:line" location node prints first (skip internals/stack).
154
- for s in lines:
155
- t = s.strip()
156
- if not t or t.startswith(("node:internal", "at ", "^")):
157
- continue
158
- return t[:200]
85
+ try:
86
+ from tree_sitter_language_pack import get_language
87
+ from tree_sitter import Parser
88
+ except Exception:
89
+ return None
90
+ try:
91
+ parser = Parser(get_language(lang))
92
+ tree = parser.parse(content.encode("utf-8"))
93
+ except Exception:
94
+ return None
95
+ root = tree.root_node
96
+ if not root.has_error:
97
+ return None
98
+ node = _first_error_node(root)
99
+ if node is None:
100
+ return f"syntax error detected in this {lang} file (unbalanced brackets/quotes or a stray token)"
101
+ line = node.start_point[0] + 1
102
+ col = node.start_point[1] + 1
103
+ snippet = ""
104
+ try:
105
+ src_line = content.splitlines()[node.start_point[0]].strip()
106
+ snippet = f" — near: {src_line[:80]}"
107
+ except Exception:
108
+ snippet = ""
109
+ kind = "missing token" if node.is_missing else "unexpected token"
110
+ return f"syntax error (line {line} col {col}): {kind}{snippet}"
111
+
112
+
113
+ def _first_error_node(root):
114
+ """DFS for the first ERROR or MISSING node (the actual syntax fault)."""
115
+ stack = [root]
116
+ while stack:
117
+ n = stack.pop()
118
+ if n.is_error or n.is_missing:
119
+ return n
120
+ # push children in order so we return the earliest error
121
+ stack.extend(reversed(n.children))
159
122
  return None
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: localcode
3
- Version: 0.3.8
3
+ Version: 0.3.9
4
4
  Summary: High-performance AI coding on consumer hardware.
5
5
  Author: LocalCode contributors
6
6
  License-Expression: Apache-2.0
@@ -35,6 +35,8 @@ Requires-Dist: prompt_toolkit>=3.0.52
35
35
  Requires-Dist: rich>=13.7.1
36
36
  Requires-Dist: scikit-learn>=1.3.0
37
37
  Requires-Dist: textual>=8.2.4
38
+ Requires-Dist: tree-sitter>=0.23.0
39
+ Requires-Dist: tree-sitter-language-pack>=1.0.0
38
40
  Requires-Dist: tomli>=2.0.0; python_version < "3.11"
39
41
  Provides-Extra: voice
40
42
  Requires-Dist: pywhispercpp>=1.2.0; extra == "voice"
@@ -1,4 +1,4 @@
1
- localcode/__init__.py,sha256=_F9ixyUc6moIe8UvI5HG2cdeyBN6Z_4qdEM2TlPlas8,49
1
+ localcode/__init__.py,sha256=dv_OdIY5KS_HiRjBMSmtKnar5Axf3OVYIDjhWUodlAY,49
2
2
  localcode/__main__.py,sha256=_4Tblte9F1KOpB-oGBQuTlaSN-IncLmhihqhBkpH3pU,69
3
3
  localcode/_subproc_env.py,sha256=eltq_kKvqGj22pIFivB5BZft9-s5fziBW0lsV9FhEV4,2383
4
4
  localcode/app.py,sha256=FSwpce6Jfn7zJmhL1ipaqTq0xKh0MxCMnEnMQuFV8L8,67820
@@ -112,7 +112,7 @@ localcode/tools/plan_mode.py,sha256=2RU6QH1IJoOTKuoAr7Ga7uTaebAS8v7OFt5v1zLEXFw,
112
112
  localcode/tools/project_check.py,sha256=B3Q6IB2XfllW3Nlxwz2pCCQwF_b0VSUBnrpCHS4eOE8,5508
113
113
  localcode/tools/read_file.py,sha256=gH852nnPiVD2Go9VcdS_EFbtnqYaGOPyAk5jo1cNbX8,4556
114
114
  localcode/tools/skill_tool.py,sha256=vTVPyRWxICtDCVYWMiAMInGnc-wGAk2L1EZn-6e79nE,1478
115
- localcode/tools/syntax_check.py,sha256=U9niGqS4Ht0_6TyZKEwKBpnoXKatQwmSBtfMdekikNM,6050
115
+ localcode/tools/syntax_check.py,sha256=tpymYFeXs7LYU5qmfHGJeVX0WUdFYBineI9GN3jONYU,4726
116
116
  localcode/tools/todo_write.py,sha256=8A4k9MzoqOtzJ6y2Gd3lEGkINcpV3JVCd2lYBmdd6qI,5801
117
117
  localcode/tools/web_fetch.py,sha256=UPhMrLipwN5xJx5-zr634gL4p6nIRqYI2_7tbUw_8Go,2770
118
118
  localcode/tools/web_search.py,sha256=ILfBBx50p00OjA939HzdDmdao4QLL8sRuxlBoUDjmC8,1122
@@ -133,9 +133,9 @@ localcode/tui/widgets/chat_log.py,sha256=D5DUkrQXL_ilxyC8N1xIPLw4Pqeh0jSWt1mPgho
133
133
  localcode/tui/widgets/voice_visualizer.py,sha256=zOhrIxaMcg-F2Y8LvHJTddq227phmiTjtEyRbkx1XxU,4709
134
134
  localcode/tui/widgets/messages/__init__.py,sha256=952AZ1qVMGUIqPIry7mwxnYbMkgPlcY5FAwZtEa5YPg,967
135
135
  localcode/tui/widgets/messages/diff.py,sha256=khDf_MThrQz0S62-t-m7i3OU5cdNolUk1TCeKMRPDQI,10910
136
- localcode-0.3.8.dist-info/licenses/LICENSE,sha256=Ahg0cteZ-6bIRB7KC15WEtdqWnzy8HY_yzsZBoVRql0,11294
137
- localcode-0.3.8.dist-info/METADATA,sha256=-O_cA0e66WTvuIJyuJEQG6Z2ummHGDjFwe0atQyQRsc,7813
138
- localcode-0.3.8.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
139
- localcode-0.3.8.dist-info/entry_points.txt,sha256=rE7elTGUvMsBd-Quy22cuTVJ-zGVOmx0nEuC6ni9Tg4,87
140
- localcode-0.3.8.dist-info/top_level.txt,sha256=58eL3Rw8v0OGmYNxjx8uS4ERWatrzUbyCL5inlukkTo,10
141
- localcode-0.3.8.dist-info/RECORD,,
136
+ localcode-0.3.9.dist-info/licenses/LICENSE,sha256=Ahg0cteZ-6bIRB7KC15WEtdqWnzy8HY_yzsZBoVRql0,11294
137
+ localcode-0.3.9.dist-info/METADATA,sha256=iT5SYY0r4-zju8UthKU8MTPF4w_3HRk0H11CIB_8dQ0,7896
138
+ localcode-0.3.9.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
139
+ localcode-0.3.9.dist-info/entry_points.txt,sha256=rE7elTGUvMsBd-Quy22cuTVJ-zGVOmx0nEuC6ni9Tg4,87
140
+ localcode-0.3.9.dist-info/top_level.txt,sha256=58eL3Rw8v0OGmYNxjx8uS4ERWatrzUbyCL5inlukkTo,10
141
+ localcode-0.3.9.dist-info/RECORD,,