localcode 0.4.0__py3-none-macosx_13_0_arm64.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 (173) hide show
  1. localcode/__init__.py +11 -0
  2. localcode/__main__.py +5 -0
  3. localcode/_subproc_env.py +54 -0
  4. localcode/agent/__init__.py +177 -0
  5. localcode/agent/app_tasks.py +157 -0
  6. localcode/agent/constants.py +275 -0
  7. localcode/agent/context.py +1152 -0
  8. localcode/agent/goal.py +244 -0
  9. localcode/agent/helpers.py +1036 -0
  10. localcode/agent/hollow_module.py +230 -0
  11. localcode/agent/hooks.py +414 -0
  12. localcode/agent/loop.py +2855 -0
  13. localcode/agent/plateau.py +297 -0
  14. localcode/agent/project_check_gate.py +91 -0
  15. localcode/agent/prompt_context.py +264 -0
  16. localcode/agent/prompts.py +368 -0
  17. localcode/agent/reasoning_loop.py +79 -0
  18. localcode/agent/recovery.py +606 -0
  19. localcode/agent/recovery_controller.py +25 -0
  20. localcode/agent/round_policy.py +23 -0
  21. localcode/agent/sections.py +241 -0
  22. localcode/agent/state_machine.py +100 -0
  23. localcode/agent/streaming.py +324 -0
  24. localcode/agent/tool_execution.py +368 -0
  25. localcode/agent/tool_orchestration.py +54 -0
  26. localcode/agent/turn_finalization.py +135 -0
  27. localcode/app.py +1595 -0
  28. localcode/approvals.py +17 -0
  29. localcode/auto_compact.py +176 -0
  30. localcode/autonomy.py +103 -0
  31. localcode/bin/__init__.py +0 -0
  32. localcode/bin/llama-server +0 -0
  33. localcode/bin/localcode-ui +4 -0
  34. localcode/bootstrap.py +1191 -0
  35. localcode/cache.py +194 -0
  36. localcode/checkpoint.py +375 -0
  37. localcode/compact.py +73 -0
  38. localcode/compaction.py +341 -0
  39. localcode/composer.py +72 -0
  40. localcode/config.py +521 -0
  41. localcode/context.py +161 -0
  42. localcode/display.py +58 -0
  43. localcode/embeddings.py +480 -0
  44. localcode/entrypoint.py +536 -0
  45. localcode/errors.py +350 -0
  46. localcode/events.py +409 -0
  47. localcode/evidence.py +102 -0
  48. localcode/execution_policy.py +48 -0
  49. localcode/features.py +308 -0
  50. localcode/formatting.py +91 -0
  51. localcode/headless_json.py +265 -0
  52. localcode/health.py +288 -0
  53. localcode/hf_quants.py +203 -0
  54. localcode/history.py +594 -0
  55. localcode/indexer.py +144 -0
  56. localcode/injection_defense.py +204 -0
  57. localcode/launcher.py +373 -0
  58. localcode/logging_utils.py +28 -0
  59. localcode/lsp.py +153 -0
  60. localcode/mcp/__init__.py +88 -0
  61. localcode/mcp/_bridge.py +61 -0
  62. localcode/mcp/_config.py +33 -0
  63. localcode/mcp/_transports.py +128 -0
  64. localcode/mcp/client.py +395 -0
  65. localcode/memory_guard.py +291 -0
  66. localcode/model_config.py +288 -0
  67. localcode/model_delete.py +444 -0
  68. localcode/model_families.py +279 -0
  69. localcode/models.py +236 -0
  70. localcode/models_catalog.py +901 -0
  71. localcode/notebook.py +123 -0
  72. localcode/output.py +403 -0
  73. localcode/patching.py +65 -0
  74. localcode/paths.py +303 -0
  75. localcode/performance.py +623 -0
  76. localcode/permissions.py +79 -0
  77. localcode/permissions_v2.py +310 -0
  78. localcode/plans.py +198 -0
  79. localcode/process_registry.py +271 -0
  80. localcode/protocol/__init__.py +56 -0
  81. localcode/protocol/events.py +116 -0
  82. localcode/protocol/jsonl.py +153 -0
  83. localcode/protocol/outcomes.py +231 -0
  84. localcode/reasoning_capabilities.py +59 -0
  85. localcode/recommendations.py +87 -0
  86. localcode/recovery.py +134 -0
  87. localcode/redaction.py +164 -0
  88. localcode/runtime.py +2331 -0
  89. localcode/server_manager.py +1069 -0
  90. localcode/session.py +296 -0
  91. localcode/shell.py +83 -0
  92. localcode/skills/debug.md +21 -0
  93. localcode/skills/edit-verified.md +30 -0
  94. localcode/skills/explain.md +19 -0
  95. localcode/skills/finish-verified.md +11 -0
  96. localcode/skills/git-commit-safely.md +30 -0
  97. localcode/skills/locate.md +21 -0
  98. localcode/skills/plan-task.md +51 -0
  99. localcode/skills/review.md +22 -0
  100. localcode/skills/run-tests.md +24 -0
  101. localcode/skills.py +728 -0
  102. localcode/snapshots.py +195 -0
  103. localcode/sounds.py +50 -0
  104. localcode/telemetry.py +273 -0
  105. localcode/theme.py +137 -0
  106. localcode/thermal.py +160 -0
  107. localcode/thinking.py +114 -0
  108. localcode/tool_router.py +246 -0
  109. localcode/toolkit.py +1241 -0
  110. localcode/tools/__init__.py +400 -0
  111. localcode/tools/agent.py +259 -0
  112. localcode/tools/append_file.py +55 -0
  113. localcode/tools/background_process.py +115 -0
  114. localcode/tools/base.py +168 -0
  115. localcode/tools/bash.py +1160 -0
  116. localcode/tools/code_navigation.py +97 -0
  117. localcode/tools/edit_diff.py +80 -0
  118. localcode/tools/edit_file.py +477 -0
  119. localcode/tools/facts.py +149 -0
  120. localcode/tools/glob_tool.py +60 -0
  121. localcode/tools/grep.py +65 -0
  122. localcode/tools/inspect_symbol.py +266 -0
  123. localcode/tools/launch_app.py +93 -0
  124. localcode/tools/list_files.py +42 -0
  125. localcode/tools/multi_edit.py +167 -0
  126. localcode/tools/plan_mode.py +86 -0
  127. localcode/tools/project_check.py +685 -0
  128. localcode/tools/read_file.py +261 -0
  129. localcode/tools/read_state.py +223 -0
  130. localcode/tools/skill_tool.py +41 -0
  131. localcode/tools/syntax_check.py +216 -0
  132. localcode/tools/todo_write.py +200 -0
  133. localcode/tools/tool_call_repair.py +157 -0
  134. localcode/tools/web_fetch.py +182 -0
  135. localcode/tools/web_search.py +45 -0
  136. localcode/tools/write_file.py +281 -0
  137. localcode/tui/__init__.py +1 -0
  138. localcode/tui/app.py +485 -0
  139. localcode/tui/bridge.py +78 -0
  140. localcode/tui/clipboard_image.py +153 -0
  141. localcode/tui/paste_collapse.py +95 -0
  142. localcode/tui/screens/__init__.py +0 -0
  143. localcode/tui/screens/chat.py +5052 -0
  144. localcode/tui/screens/confirm.py +123 -0
  145. localcode/tui/screens/mode_picker.py +79 -0
  146. localcode/tui/screens/model_picker.py +962 -0
  147. localcode/tui/screens/setup.py +891 -0
  148. localcode/tui/styles/__init__.py +0 -0
  149. localcode/tui/styles/app.tcss +197 -0
  150. localcode/tui/widgets/__init__.py +0 -0
  151. localcode/tui/widgets/approval.py +2 -0
  152. localcode/tui/widgets/chat_log.py +2063 -0
  153. localcode/tui/widgets/messages/__init__.py +21 -0
  154. localcode/tui/widgets/messages/diff.py +287 -0
  155. localcode/tui/widgets/voice_visualizer.py +122 -0
  156. localcode/turn_diff.py +180 -0
  157. localcode/ui/FORK_COMMIT +1 -0
  158. localcode/ui/__init__.py +58 -0
  159. localcode/ui/launch.py +204 -0
  160. localcode/ui/picker_cli.py +165 -0
  161. localcode/ui/plugin/localcode.ts +626 -0
  162. localcode/ui/ports.py +42 -0
  163. localcode/ui/server_cmd.py +89 -0
  164. localcode/ui/supervisor.py +736 -0
  165. localcode/undo.py +121 -0
  166. localcode/verification.py +87 -0
  167. localcode/voice.py +948 -0
  168. localcode-0.4.0.dist-info/METADATA +160 -0
  169. localcode-0.4.0.dist-info/RECORD +173 -0
  170. localcode-0.4.0.dist-info/WHEEL +5 -0
  171. localcode-0.4.0.dist-info/entry_points.txt +3 -0
  172. localcode-0.4.0.dist-info/licenses/LICENSE +201 -0
  173. localcode-0.4.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,244 @@
1
+ """Goal-state inference for a single user turn.
2
+
3
+ LocalCode's earlier loop inferred "done" from surface form:
4
+ no tool calls + some assistant text. That is too weak for agentic work.
5
+ This module gives each turn a first-class goal object so prompting,
6
+ telemetry, history, and completion gates all speak the same language.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ from dataclasses import asdict, dataclass
11
+ import re
12
+
13
+
14
+ __all__ = [
15
+ "GoalState",
16
+ "infer_goal_state",
17
+ "extract_feature_criteria",
18
+ ]
19
+
20
+
21
+ _FEATURE_EXTRACTION_PROMPT = """\
22
+ You are a requirements analyst. Read the user's request and output the discrete, \
23
+ verifiable features the user wants. ONE per line, prefixed with "- ". \
24
+ Output ONLY the checklist — no preamble, no commentary, no numbering, no headings.
25
+
26
+ Each item must:
27
+ - be a single concrete outcome the user wants (not a step or action),
28
+ - be specific enough that someone else could verify it later (with a probe, a read, or a test),
29
+ - restate ONLY what the user asked for — do NOT invent features the user did not request.
30
+
31
+ If the request is trivial (e.g. a question, a one-line edit, a quick command), output \
32
+ the single line "- (trivial — no feature checklist needed)" and nothing else.
33
+
34
+ User request:
35
+ \"\"\"
36
+ {user_text}
37
+ \"\"\"
38
+
39
+ Checklist:
40
+ """
41
+
42
+
43
+ def extract_feature_criteria(runtime, user_text: str, *, max_features: int = 12) -> list[str]:
44
+ """Best-effort extraction of discrete user-requested features.
45
+
46
+ Calls the underlying model once at task intake to decompose the user's
47
+ request into a verifiable checklist. The model sees ONLY the user's
48
+ text — no app-specific scaffolding — so the result is general across
49
+ any kind of request (build_app, edit, dashboard, fix, etc.).
50
+
51
+ Returns [] on any failure or if the model decides the request is
52
+ trivial. Caller should fall back to the pre-existing generic
53
+ success_criteria in that case.
54
+
55
+ Why a model call rather than a heuristic: phrasings like
56
+ 'with audio AND text AND quiz' vs 'should support uploading and exporting CSV'
57
+ vs 'a dashboard showing revenue / churn / MRR' don't share a common
58
+ parse tree. Heuristic splitters miss most of them. The model's
59
+ natural-language understanding gets it right cheaply (~3 s of TTFT,
60
+ ~50–150 decoded tokens at task start).
61
+ """
62
+ text = (user_text or "").strip()
63
+ if not text or len(text) < 20:
64
+ return []
65
+ try:
66
+ prompt = _FEATURE_EXTRACTION_PROMPT.format(user_text=text)
67
+ raw = runtime.generate_once(
68
+ [{"role": "user", "content": prompt}],
69
+ max_tokens=512,
70
+ )
71
+ except Exception:
72
+ return []
73
+ if not raw:
74
+ return []
75
+ out: list[str] = []
76
+ for line in raw.splitlines():
77
+ s = line.strip()
78
+ if not s:
79
+ continue
80
+ if s.startswith(("- ", "* ", "• ")):
81
+ s = s[2:].strip()
82
+ elif s[:1].isdigit():
83
+ # Strip leading "1. " / "1) " numbering if the model added it
84
+ # despite our instruction.
85
+ m = re.match(r"^\d+[.)]\s*(.*)", s)
86
+ if m:
87
+ s = m.group(1).strip()
88
+ if not s:
89
+ continue
90
+ if "trivial" in s.lower() and "no feature" in s.lower():
91
+ return []
92
+ # Drop obvious meta-commentary the model sometimes emits despite
93
+ # the instruction.
94
+ if s.lower().startswith(("here is", "here's", "checklist", "the user")):
95
+ continue
96
+ if len(s) > 200:
97
+ s = s[:197] + "…"
98
+ if s not in out:
99
+ out.append(s)
100
+ if len(out) >= max_features:
101
+ break
102
+ return out
103
+
104
+
105
+ _SLUG_STOPWORDS = {
106
+ "a",
107
+ "an",
108
+ "and",
109
+ "app",
110
+ "application",
111
+ "ability",
112
+ "able",
113
+ "all",
114
+ "build",
115
+ "can",
116
+ "create",
117
+ "for",
118
+ "from",
119
+ "have",
120
+ "help",
121
+ "i",
122
+ "in",
123
+ "it",
124
+ "make",
125
+ "me",
126
+ "my",
127
+ "of",
128
+ "please",
129
+ "should",
130
+ "that",
131
+ "the",
132
+ "this",
133
+ "to",
134
+ "using",
135
+ "via",
136
+ "with",
137
+ }
138
+
139
+
140
+ @dataclass(frozen=True)
141
+ class GoalState:
142
+ raw_user_text: str
143
+ goal_type: str
144
+ task_kind: str
145
+ task_slug: str
146
+ goal_summary: str
147
+ success_criteria: tuple[str, ...]
148
+ allows_blocking_question: bool = True
149
+
150
+ def as_dict(self) -> dict:
151
+ data = asdict(self)
152
+ data["success_criteria"] = list(self.success_criteria)
153
+ return data
154
+
155
+
156
+ # NOTE ON THIS CLASSIFIER: goal_type is a WEAK hint, not the authority on
157
+ # whether a task is finished. Completion is governed by the model's own todo
158
+ # list (see the open-todo gate in agent/loop.py) — never by these regexes or by
159
+ # a detected server port. History: "Build me an Anki clone … so I can launch it
160
+ # like a native app" used to match the run_or_launch pattern on the phrase
161
+ # "launch it", which then let a freshly-opened dev-server port count as "app
162
+ # built" at ~10% of the work. The lasting fix is that a port no longer ends a
163
+ # turn while todos are open; the widening below just keeps genuine builds out of
164
+ # run_or_launch in the first place. Misclassifying an edit as build_app is
165
+ # cheap (its criteria are just "code exists + verification passes"); the
166
+ # dangerous direction is a build leaking into run_or_launch, so bias toward
167
+ # build_app and require run_or_launch to be an *unambiguous* run request.
168
+ _BUILD_VERB = r"(?:build|create|make|scaffold|implement|develop|generate|write|design|code)"
169
+ _APP_NOUN = (
170
+ r"(?:app|application|web ?app|website|web ?page|site|dashboard|api|service|"
171
+ r"project|clone|game|tool|extension|widget|spa|pwa|frontend|back ?end|"
172
+ r"platform|bot|cli|ui|component|library|package|script|program|"
173
+ r"tracker|editor|viewer|generator)"
174
+ )
175
+
176
+
177
+ _TRIVIAL_TURN_RE = re.compile(
178
+ r"^(hi|hey|hello|yo|sup|thanks|thank you|thx|ta|ok|okay|k|cool|nice|great|"
179
+ r"good (morning|afternoon|evening|night)|how are you|what'?s up|"
180
+ r"morning|evening)[\s!.?,]*$",
181
+ re.IGNORECASE,
182
+ )
183
+
184
+
185
+ def is_trivial_turn(user_text: str) -> bool:
186
+ """True for a greeting or acknowledgement - conversation, not a task.
187
+
188
+ The system prompt tells the model to plan multi-step work with todo_write,
189
+ and its carve-out is written in units of task size ("skip the plan for
190
+ straightforward one/two-step tasks"). Nothing there covers a turn that is
191
+ not a task at all, so a small quant reading a header that shouts FINISH THE
192
+ WHOLE TASK will happily open a todo list for "hi". Callers use this to stop
193
+ offering the tool at all on such turns - a model cannot call what it was
194
+ never given.
195
+ """
196
+ return bool(_TRIVIAL_TURN_RE.match((user_text or "").strip()))
197
+
198
+
199
+ def infer_goal_state(user_text: str) -> GoalState:
200
+ text = (user_text or "").strip()
201
+ lower = text.lower()
202
+ # build verb + app-ish noun anywhere in the request → it's a build. No tight
203
+ # proximity window (the old `.{0,40}` cap is exactly why "Build me a … web
204
+ # app" fell through — the noun sat 48 chars past the verb).
205
+ if re.search(rf"\b{_BUILD_VERB}\b", lower) and re.search(rf"\b{_APP_NOUN}\b", lower):
206
+ goal_type, task_kind = "build_app", "new_app"
207
+ criteria = ("Requested implementation exists on disk", "Relevant build or tests pass", "Runtime behavior is verified when applicable")
208
+ elif re.search(r"\b(run|launch|start|serve|open)\b.{0,24}\b(app|server|site|project|it)\b", lower):
209
+ goal_type, task_kind = "run_or_launch", "run"
210
+ criteria = ("Target process is running", "Exact access URL or process status is reported")
211
+ elif re.search(r"\b(fix|change|edit|update|refactor|rename|remove|add)\b", lower):
212
+ goal_type, task_kind = "edit_existing", "edit"
213
+ criteria = ("Requested code change exists", "Relevant verification passes")
214
+ elif re.match(r"^(what|why|how|where|when|who|is|are|can|could|should|does|do)\b", lower) or text.endswith("?"):
215
+ goal_type, task_kind = "question", "question"
216
+ criteria = ("Question is answered from grounded evidence",)
217
+ else:
218
+ goal_type, task_kind = "general_task", "general_task"
219
+ criteria = ("Assistant makes concrete progress toward the user's request", "Assistant stops only on completion or a focused blocking question")
220
+ return GoalState(
221
+ raw_user_text=text,
222
+ goal_type=goal_type,
223
+ task_kind=task_kind,
224
+ task_slug=_make_task_slug(text),
225
+ goal_summary=text[:240],
226
+ success_criteria=criteria,
227
+ )
228
+
229
+
230
+ def _make_task_slug(text: str) -> str:
231
+ """Short internal task label, not a forced project directory."""
232
+ words = re.findall(r"[a-z0-9]+", (text or "").lower())
233
+ seen: set[str] = set()
234
+ content_words: list[str] = []
235
+ for word in words:
236
+ if word in _SLUG_STOPWORDS or len(word) <= 1 or word in seen:
237
+ continue
238
+ seen.add(word)
239
+ content_words.append(word)
240
+ selected = content_words[:4] or words[:3] or ["task"]
241
+ clean = "-".join(selected)
242
+ clean = re.sub(r"-+", "-", clean).strip("-") or "task"
243
+ clean = clean[:30].strip("-") or "task"
244
+ return clean