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
localcode/paths.py ADDED
@@ -0,0 +1,303 @@
1
+ """Single source of truth for where LocalCode reads/writes state on disk.
2
+
3
+ Two-tier layout:
4
+
5
+ <project_root>/.localcode/ ← per-project state (this module's main job)
6
+ lifecycle.log
7
+ last-pressure-kill.txt
8
+ last_error.log
9
+ notebook/<session>/
10
+ test-results/<timestamp>/
11
+ sessions/ (future move)
12
+ history.db (future move)
13
+
14
+ ~/.localcode/ ← truly-global, machine-wide state
15
+ llama-server.pid (one server runs at a time, system-wide)
16
+ stuck-server.txt (D-state marker, system-wide)
17
+ config.toml (user's preferences across projects)
18
+ models cache (10 GB+ files, can't duplicate per project)
19
+
20
+ Why split this way
21
+ ------------------
22
+ Per-project: anything specific to "what terminal coding tools did in THIS codebase" — sessions,
23
+ test results, lifecycle of THIS project's runs, the agent's notebook scratch.
24
+ You can `git ignore .localcode/` and the project tree stays clean. When you move
25
+ or rename a project, its conversation history comes with it.
26
+
27
+ Global: anything that's a singleton on the machine — the running llama-server
28
+ (only one on port 8081), the model files (10 GB each), and your global
29
+ preferences (autonomy level, default model). Duplicating these per project
30
+ would waste disk and create state confusion.
31
+
32
+ Project-root detection
33
+ ----------------------
34
+ Walks up from cwd looking for, in priority order:
35
+ 1. existing `.localcode/` directory (explicit, the user already has one here)
36
+ 2. `.git/` (most common — git repo root)
37
+ 3. `LOCALCODE.md` (explicit localcode project marker)
38
+ 4. Standard project files (pyproject.toml, package.json, Cargo.toml, go.mod)
39
+ Falls back to the cwd itself if nothing is found, so launching from `/tmp` or
40
+ `~` still works (you just get `<cwd>/.localcode/` wherever you ran from).
41
+ """
42
+ from __future__ import annotations
43
+
44
+ import os
45
+ from pathlib import Path
46
+ from typing import Optional
47
+
48
+
49
+ GLOBAL_STATE_DIR = Path.home() / ".localcode"
50
+
51
+ _PROJECT_MARKERS = (
52
+ ".localcode",
53
+ ".git",
54
+ "LOCALCODE.md",
55
+ "pyproject.toml",
56
+ "package.json",
57
+ "Cargo.toml",
58
+ "go.mod",
59
+ )
60
+
61
+
62
+ def find_project_root(start: Optional[Path] = None) -> Path:
63
+ """Walk up from `start` (default: cwd) looking for a project marker.
64
+
65
+ Returns the deepest ancestor that contains one of `_PROJECT_MARKERS`.
66
+ If none found, returns `start` (or cwd) — guarantees a usable path.
67
+ """
68
+ p = (start or Path.cwd()).resolve()
69
+ for ancestor in (p, *p.parents):
70
+ for marker in _PROJECT_MARKERS:
71
+ if (ancestor / marker).exists():
72
+ return ancestor
73
+ return p
74
+
75
+
76
+ def chmod_quiet(path: Path | str, mode: int) -> None:
77
+ """Best-effort `chmod`. Permission hardening is a nice-to-have on
78
+ POSIX and a no-op on Windows / exotic filesystems, so a failure here
79
+ must never take the process down. Only OS-level errors are absorbed —
80
+ a programming error (bad type) still raises."""
81
+ try:
82
+ os.chmod(path, mode)
83
+ except (OSError, NotImplementedError):
84
+ pass
85
+
86
+
87
+ def write_private(path: Path | str, text: str, encoding: str = "utf-8") -> None:
88
+ """Write `text` to `path` with 0600 permissions, atomically w.r.t. mode.
89
+
90
+ Uses `os.open(..., 0o600)` so the file is never momentarily
91
+ world-readable between `write_text()` and a follow-up `chmod` — the
92
+ window a `Path.write_text()` + `chmod` pair leaves open. An existing
93
+ file keeps its inode (O_TRUNC), so its mode is re-applied explicitly.
94
+
95
+ Used for anything containing user prompts, model output, or API keys.
96
+ """
97
+ path = Path(path)
98
+ data = text.encode(encoding, "replace")
99
+ fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
100
+ try:
101
+ os.write(fd, data)
102
+ finally:
103
+ os.close(fd)
104
+ # O_CREAT's mode only applies to a *newly created* file; a pre-existing
105
+ # 0644 file keeps its old mode, so tighten it unconditionally.
106
+ chmod_quiet(path, 0o600)
107
+
108
+
109
+ def project_state_dir(cwd: Optional[Path] = None) -> Path:
110
+ """Return (and create) `<project_root>/.localcode/`.
111
+
112
+ Idempotent — creating an existing directory is a no-op. The
113
+ directory is materialised on first access so callers can
114
+ immediately write into it without checking existence.
115
+
116
+ The directory holds `events.jsonl` (full prompts, full model
117
+ responses, tool-call slices), session state and error logs — all of
118
+ it inside the USER'S OWN REPO. Two hardening steps happen on
119
+ creation:
120
+
121
+ * a `.gitignore` containing `*` is dropped in, so a stray
122
+ `git add -A` can never stage a multi-megabyte transcript of the
123
+ user's prompts into a repo they're about to push;
124
+ * the directory is chmod 0700, so other accounts on a shared
125
+ machine can't read it.
126
+
127
+ Both are guarded by an existence check because this function is on
128
+ hot paths — the steady-state cost is one extra `stat`.
129
+ """
130
+ d = find_project_root(cwd) / ".localcode"
131
+ existed = d.is_dir()
132
+ d.mkdir(parents=True, exist_ok=True)
133
+ if not existed:
134
+ chmod_quiet(d, 0o700)
135
+ gitignore = d / ".gitignore"
136
+ if not gitignore.exists():
137
+ try:
138
+ gitignore.write_text("# Created by localcode. Local state only, never commit.\n*\n")
139
+ except OSError:
140
+ pass
141
+ return d
142
+
143
+
144
+ def global_state_dir() -> Path:
145
+ """Return (and create) `~/.localcode/`.
146
+
147
+ Reserved for machine-wide singletons (llama-server PID, stuck
148
+ server marker, user-global config, model cache). Anything that
149
+ belongs to a specific project should NOT live here.
150
+ """
151
+ existed = GLOBAL_STATE_DIR.is_dir()
152
+ GLOBAL_STATE_DIR.mkdir(parents=True, exist_ok=True)
153
+ if not existed:
154
+ chmod_quiet(GLOBAL_STATE_DIR, 0o700)
155
+ return GLOBAL_STATE_DIR
156
+
157
+
158
+ # ── Specific artifact paths ──────────────────────────────────────────
159
+ #
160
+ # Each helper returns the canonical path for one artifact. Consumers
161
+ # should import the helper, never hardcode `Path.home() / ".localcode"`.
162
+ # That way moving an artifact between project / global tiers requires
163
+ # changing exactly one line here, not hunting through the codebase.
164
+
165
+
166
+ def lifecycle_log_path(cwd: Optional[Path] = None) -> Path:
167
+ """DEPRECATED — use `events_log_path()`. Kept as a back-compat shim
168
+ while in-flight references are migrated; will be removed once the
169
+ consolidation lands fully and no caller imports this name."""
170
+ return project_state_dir(cwd) / "lifecycle.log"
171
+
172
+
173
+ def events_log_path(cwd: Optional[Path] = None) -> Path:
174
+ """Single source of truth for ALL structured developer events:
175
+ server lifecycle, tool calls, redactions, pressure kills, stall
176
+ recovery, turn start/end. JSONL — one event per line, each line is
177
+ valid JSON with at minimum `t` (ISO timestamp) and `type` fields.
178
+
179
+ Per-project (lives at `<project_root>/.localcode/events.jsonl`)
180
+ so each project gets its own audit trail. Append-only; never
181
+ truncated. Local-only: nothing is uploaded anywhere. Designed to be
182
+ tail-able with `jq` for live debugging. Any future remote sink would
183
+ be a new opt-in feature that must strip user content first, not a
184
+ flag flip."""
185
+ return project_state_dir(cwd) / "events.jsonl"
186
+
187
+
188
+ def pressure_kill_marker_path(cwd: Optional[Path] = None) -> Path:
189
+ """Marker the pressure monitor writes when it SIGTERMs the server.
190
+ Per-project so an error message in project A doesn't reference a
191
+ pressure-kill that happened while you were working on project B."""
192
+ return project_state_dir(cwd) / "last-pressure-kill.txt"
193
+
194
+
195
+ def last_error_log_path(cwd: Optional[Path] = None) -> Path:
196
+ """Verbose technical detail of the most recent user-facing error
197
+ in THIS project — referenced by the error formatter's "(full
198
+ technical detail saved to ...)" line."""
199
+ return project_state_dir(cwd) / "last_error.log"
200
+
201
+
202
+ def notebook_root(cwd: Optional[Path] = None) -> Path:
203
+ """Per-project notebook root. Each session creates a subdirectory
204
+ `<notebook_root>/<session_id>/`. Per-project so the agent's working
205
+ drafts for project A aren't visible / reachable from project B."""
206
+ return project_state_dir(cwd) / "notebook"
207
+
208
+
209
+ def test_results_root(cwd: Optional[Path] = None) -> Path:
210
+ """Where `tests/e2e/run.py` writes per-run reports. Per-project
211
+ because test results pertain to the project being tested, and they
212
+ should be `git ignore`-able (and visible in the project tree)."""
213
+ return project_state_dir(cwd) / "test-results"
214
+
215
+
216
+ # ── Global-only paths ────────────────────────────────────────────────
217
+
218
+
219
+ def server_pid_file() -> Path:
220
+ """llama-server PID file. STAYS GLOBAL because only one
221
+ llama-server can run on the machine at a time (one port, one chunk
222
+ of GPU memory). If you `cd` between projects mid-session the
223
+ running server stays serving you — the per-project lifecycle log
224
+ just records a fresh `server_started` entry on next launch from
225
+ the new project."""
226
+ return global_state_dir() / "llama-server.pid"
227
+
228
+
229
+ def stuck_server_marker_path() -> Path:
230
+ """D-state marker for a server that won't die. Global because the
231
+ stuck process is a system-level concern, not a project concern."""
232
+ return global_state_dir() / "stuck-server.txt"
233
+
234
+
235
+ # ── Write containment ────────────────────────────────────────────────
236
+ #
237
+ # Every file-writing tool funnels its `path` argument through
238
+ # `contain_write_path()`. Before this existed, `ToolContext.resolve_path`
239
+ # computed `repo / raw` and returned it untouched — and because
240
+ # `Path.__truediv__` DISCARDS the left operand when the right one is
241
+ # absolute, `raw="/Users/victim/.zshrc"` produced exactly that path. A
242
+ # `../../..` prefix escaped just as easily, and a symlink committed inside
243
+ # the repo pointing outside was a third route (pathlib writes follow
244
+ # symlinks). Reads are deliberately NOT contained — the agent is expected to
245
+ # read anywhere on the machine — but writes now stay inside the project.
246
+
247
+
248
+ class PathContainmentError(ValueError):
249
+ """A write was aimed outside the project root (or an allowed extra root)."""
250
+
251
+ def __init__(self, raw: str, resolved: Path, root: Path):
252
+ self.raw = raw
253
+ self.resolved = resolved
254
+ self.root = root
255
+ super().__init__(
256
+ f"path escapes the project root: {raw!r} resolves to {resolved} "
257
+ f"which is outside {root}. Write only inside the project."
258
+ )
259
+
260
+
261
+ def is_within(path: Path, root: Path) -> bool:
262
+ """True if `path`, fully resolved, lives at or under `root`.
263
+
264
+ Symlink-aware in both directions: `.resolve()` follows every component
265
+ (including the final one), so a symlink inside the repo whose target
266
+ escapes resolves to the escaping target and returns False. Resolving
267
+ `root` too keeps `/tmp` → `/private/tmp` style prefixes from producing
268
+ false negatives on macOS.
269
+ """
270
+ try:
271
+ return Path(path).resolve().is_relative_to(Path(root).resolve())
272
+ except (OSError, ValueError):
273
+ return False
274
+
275
+
276
+ def contain_write_path(
277
+ candidate: Path | str,
278
+ root: Path | str,
279
+ extra_roots: "tuple[Path, ...] | list[Path] | None" = None,
280
+ ) -> Path:
281
+ """Return the resolved write target, or raise `PathContainmentError`.
282
+
283
+ `candidate` is the already-healed absolute path (see
284
+ `ToolContext.resolve_path`); `root` is the project root. `extra_roots`
285
+ are additional sanctioned trees (the agent's notebook scratch dir) that
286
+ may legitimately sit outside the project.
287
+ """
288
+ cand = Path(candidate)
289
+ try:
290
+ resolved = cand.resolve()
291
+ except (OSError, ValueError):
292
+ resolved = cand
293
+ roots = [Path(root)]
294
+ for extra in (extra_roots or ()):
295
+ roots.append(Path(extra))
296
+ for r in roots:
297
+ if is_within(resolved, r):
298
+ return resolved
299
+ try:
300
+ root_resolved = Path(root).resolve()
301
+ except (OSError, ValueError):
302
+ root_resolved = Path(root)
303
+ raise PathContainmentError(str(candidate), resolved, root_resolved)