uncoded 0.5.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.
uncoded/stubs.py ADDED
@@ -0,0 +1,389 @@
1
+ """Generate .pyi stub files for agent navigation."""
2
+
3
+ import ast
4
+ import re
5
+ from dataclasses import dataclass, field
6
+ from pathlib import Path
7
+
8
+ from uncoded.extract import _property_kind, iter_source_files
9
+ from uncoded.sync import remove_file, sync_file
10
+
11
+ # Width cap for inlining the right-hand side of an assignment. If the unparsed
12
+ # RHS exceeds this, it is elided to "..." so stubs stay compact and stable.
13
+ VALUE_WIDTH_CAP = 80
14
+
15
+
16
+ @dataclass
17
+ class StubParam:
18
+ """A function parameter with name and optional type annotation."""
19
+
20
+ name: str
21
+ annotation: str | None = None
22
+
23
+
24
+ @dataclass
25
+ class StubFunction:
26
+ """A function or method with its signature."""
27
+
28
+ name: str
29
+ params: list[StubParam] = field(default_factory=list)
30
+ return_annotation: str | None = None
31
+ docstring_excerpt: str | None = None
32
+ is_async: bool = False
33
+
34
+
35
+ @dataclass
36
+ class StubAssignment:
37
+ """A module-level or class-level assignment.
38
+
39
+ ``value_source`` is the rendered RHS if it fits within ``VALUE_WIDTH_CAP``,
40
+ or the literal string ``"..."`` if the RHS was elided. ``None`` means there
41
+ is no RHS at all (e.g. a bare annotation ``X: int``).
42
+ """
43
+
44
+ name: str
45
+ annotation: str | None = None
46
+ value_source: str | None = None
47
+ is_type_alias: bool = False
48
+
49
+
50
+ @dataclass
51
+ class StubClass:
52
+ """A class with its members."""
53
+
54
+ name: str
55
+ bases: list[str] = field(default_factory=list)
56
+ docstring_excerpt: str | None = None
57
+ attributes: list[StubAssignment] = field(default_factory=list)
58
+ methods: list[StubFunction] = field(default_factory=list)
59
+
60
+
61
+ @dataclass
62
+ class StubModule:
63
+ """All symbols extracted from a single Python module."""
64
+
65
+ rel_path: str
66
+ imports: list[str] = field(default_factory=list)
67
+ constants: list[StubAssignment] = field(default_factory=list)
68
+ classes: list[StubClass] = field(default_factory=list)
69
+ functions: list[StubFunction] = field(default_factory=list)
70
+
71
+
72
+ def _first_sentence(
73
+ node: ast.AsyncFunctionDef | ast.FunctionDef | ast.ClassDef | ast.Module,
74
+ ) -> str | None:
75
+ """Return the first sentence of a node's docstring, or None."""
76
+ docstring = ast.get_docstring(node)
77
+ if not docstring:
78
+ return None
79
+ text = docstring.strip()
80
+ match = re.match(r"(.+?\.)\s", text + " ")
81
+ if match:
82
+ return match.group(1)
83
+ return text.split("\n")[0].strip()
84
+
85
+
86
+ def _extract_params(args: ast.arguments) -> list[StubParam]:
87
+ """Extract parameters from a function argument node, without defaults."""
88
+ params: list[StubParam] = []
89
+
90
+ for arg in args.posonlyargs:
91
+ annotation = ast.unparse(arg.annotation) if arg.annotation else None
92
+ params.append(StubParam(name=arg.arg, annotation=annotation))
93
+ if args.posonlyargs:
94
+ params.append(StubParam(name="/"))
95
+
96
+ for arg in args.args:
97
+ annotation = ast.unparse(arg.annotation) if arg.annotation else None
98
+ params.append(StubParam(name=arg.arg, annotation=annotation))
99
+
100
+ if args.vararg:
101
+ annotation = (
102
+ ast.unparse(args.vararg.annotation) if args.vararg.annotation else None
103
+ )
104
+ params.append(StubParam(name=f"*{args.vararg.arg}", annotation=annotation))
105
+ elif args.kwonlyargs:
106
+ params.append(StubParam(name="*"))
107
+
108
+ for arg in args.kwonlyargs:
109
+ annotation = ast.unparse(arg.annotation) if arg.annotation else None
110
+ params.append(StubParam(name=arg.arg, annotation=annotation))
111
+
112
+ if args.kwarg:
113
+ annotation = (
114
+ ast.unparse(args.kwarg.annotation) if args.kwarg.annotation else None
115
+ )
116
+ params.append(StubParam(name=f"**{args.kwarg.arg}", annotation=annotation))
117
+
118
+ return params
119
+
120
+
121
+ def _render_value(value: ast.expr) -> str:
122
+ """Render an expression as source, eliding to '...' if too long or multi-line."""
123
+ source = ast.unparse(value)
124
+ if len(source) > VALUE_WIDTH_CAP or "\n" in source:
125
+ return "..."
126
+ return source
127
+
128
+
129
+ def _extract_assignment(
130
+ node: ast.Assign | ast.AnnAssign | ast.TypeAlias,
131
+ ) -> StubAssignment | None:
132
+ """Build a StubAssignment from an assignment-style AST node.
133
+
134
+ Returns None if the node's target is not a single simple name (e.g. tuple
135
+ unpacking or attribute assignment), which we can't represent cleanly.
136
+ """
137
+ if isinstance(node, ast.TypeAlias):
138
+ if not isinstance(node.name, ast.Name):
139
+ return None
140
+ return StubAssignment(
141
+ name=node.name.id,
142
+ value_source=_render_value(node.value),
143
+ is_type_alias=True,
144
+ )
145
+
146
+ if isinstance(node, ast.AnnAssign):
147
+ if not isinstance(node.target, ast.Name):
148
+ return None
149
+ annotation = ast.unparse(node.annotation)
150
+ value_source = _render_value(node.value) if node.value is not None else None
151
+ return StubAssignment(
152
+ name=node.target.id,
153
+ annotation=annotation,
154
+ value_source=value_source,
155
+ )
156
+
157
+ # ast.Assign
158
+ if len(node.targets) != 1 or not isinstance(node.targets[0], ast.Name):
159
+ return None
160
+ return StubAssignment(
161
+ name=node.targets[0].id,
162
+ value_source=_render_value(node.value),
163
+ )
164
+
165
+
166
+ def _extract_function(
167
+ node: ast.FunctionDef | ast.AsyncFunctionDef,
168
+ ) -> StubFunction:
169
+ """Build a StubFunction from a function or method AST node."""
170
+ return StubFunction(
171
+ name=node.name,
172
+ params=_extract_params(node.args),
173
+ return_annotation=ast.unparse(node.returns) if node.returns else None,
174
+ docstring_excerpt=_first_sentence(node),
175
+ is_async=isinstance(node, ast.AsyncFunctionDef),
176
+ )
177
+
178
+
179
+ def _property_attribute(
180
+ node: ast.FunctionDef | ast.AsyncFunctionDef,
181
+ ) -> StubAssignment:
182
+ """Build a StubAssignment representing a @property as a class attribute."""
183
+ return StubAssignment(
184
+ name=node.name,
185
+ annotation=ast.unparse(node.returns) if node.returns else None,
186
+ value_source=None,
187
+ is_type_alias=False,
188
+ )
189
+
190
+
191
+ def _extract_class(node: ast.ClassDef) -> StubClass:
192
+ """Build a StubClass from a class AST node."""
193
+ bases = [ast.unparse(b) for b in node.bases]
194
+
195
+ attributes: list[StubAssignment] = []
196
+ methods: list[StubFunction] = []
197
+
198
+ for child in ast.iter_child_nodes(node):
199
+ if isinstance(child, (ast.AnnAssign, ast.Assign)):
200
+ assignment = _extract_assignment(child)
201
+ if assignment is not None:
202
+ attributes.append(assignment)
203
+ elif isinstance(child, (ast.FunctionDef, ast.AsyncFunctionDef)):
204
+ kind = _property_kind(child)
205
+ if kind == "setter" or kind == "deleter":
206
+ continue
207
+ if kind == "property":
208
+ attributes.append(_property_attribute(child))
209
+ else:
210
+ methods.append(_extract_function(child))
211
+
212
+ return StubClass(
213
+ name=node.name,
214
+ bases=bases,
215
+ docstring_excerpt=_first_sentence(node),
216
+ attributes=attributes,
217
+ methods=methods,
218
+ )
219
+
220
+
221
+ def extract_stub(source: str, rel_path: str) -> StubModule:
222
+ """Parse Python source and extract imports, constants, classes, and functions."""
223
+ tree = ast.parse(source)
224
+ imports: list[str] = []
225
+ constants: list[StubAssignment] = []
226
+ classes: list[StubClass] = []
227
+ functions: list[StubFunction] = []
228
+
229
+ for node in ast.iter_child_nodes(tree):
230
+ if isinstance(node, (ast.Import, ast.ImportFrom)):
231
+ imports.append(ast.unparse(node))
232
+ elif isinstance(node, ast.ClassDef):
233
+ classes.append(_extract_class(node))
234
+ elif isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
235
+ functions.append(_extract_function(node))
236
+ elif isinstance(node, (ast.Assign, ast.AnnAssign, ast.TypeAlias)):
237
+ assignment = _extract_assignment(node)
238
+ if assignment is not None:
239
+ constants.append(assignment)
240
+
241
+ return StubModule(
242
+ rel_path=rel_path,
243
+ imports=imports,
244
+ constants=constants,
245
+ classes=classes,
246
+ functions=functions,
247
+ )
248
+
249
+
250
+ def _render_param(p: StubParam) -> str:
251
+ """Render a single parameter as a string for a function signature."""
252
+ if p.name in ("/", "*"):
253
+ return p.name
254
+ if p.annotation:
255
+ return f"{p.name}: {p.annotation}"
256
+ return p.name
257
+
258
+
259
+ def _render_function(func: StubFunction, indent: str = "") -> list[str]:
260
+ """Render a function or method as stub lines, indented for methods."""
261
+ params_str = ", ".join(_render_param(p) for p in func.params)
262
+ ret = f" -> {func.return_annotation}" if func.return_annotation else ""
263
+ prefix = "async def" if func.is_async else "def"
264
+ lines = [f"{indent}{prefix} {func.name}({params_str}){ret}:"]
265
+ if func.docstring_excerpt:
266
+ lines.append(f'{indent} """{func.docstring_excerpt}"""')
267
+ lines.append(f"{indent} ...")
268
+ return lines
269
+
270
+
271
+ def _format_assignment_body(a: StubAssignment) -> str:
272
+ """Render the 'name [: type] [= value]' portion of an assignment."""
273
+ if a.is_type_alias:
274
+ return f"type {a.name} = {a.value_source}"
275
+ head = f"{a.name}: {a.annotation}" if a.annotation else a.name
276
+ if a.value_source is None:
277
+ return head
278
+ return f"{head} = {a.value_source}"
279
+
280
+
281
+ def _render_assignment(a: StubAssignment, indent: str = "") -> str:
282
+ """Render a module-level assignment as a stub line."""
283
+ body = _format_assignment_body(a)
284
+ return f"{indent}{body}"
285
+
286
+
287
+ def render_stub(module: StubModule) -> str:
288
+ """Render a StubModule as a .pyi file string."""
289
+ lines: list[str] = [f"# {module.rel_path}", ""]
290
+
291
+ if module.imports:
292
+ lines.extend(module.imports)
293
+ lines.append("")
294
+
295
+ for const in module.constants:
296
+ lines.append(_render_assignment(const))
297
+ if module.constants:
298
+ lines.append("")
299
+
300
+ for func in module.functions:
301
+ lines.extend(_render_function(func))
302
+ lines.append("")
303
+
304
+ for cls in module.classes:
305
+ bases_str = f"({', '.join(cls.bases)})" if cls.bases else ""
306
+ lines.append(f"class {cls.name}{bases_str}:")
307
+ if cls.docstring_excerpt:
308
+ lines.append(f' """{cls.docstring_excerpt}"""')
309
+ lines.append("")
310
+
311
+ for attr in cls.attributes:
312
+ lines.append(_render_assignment(attr, indent=" "))
313
+ if cls.attributes:
314
+ lines.append("")
315
+
316
+ for method in cls.methods:
317
+ lines.extend(_render_function(method, indent=" "))
318
+ lines.append("")
319
+
320
+ return "\n".join(lines).rstrip() + "\n"
321
+
322
+
323
+ def _generate_stubs(source_root: Path) -> dict[Path, str]:
324
+ """Return a mapping from stub relative paths to rendered stub content."""
325
+ result: dict[Path, str] = {}
326
+ for source, rel_path in iter_source_files(source_root):
327
+ try:
328
+ module = extract_stub(source, rel_path)
329
+ except SyntaxError:
330
+ continue
331
+ if not module.classes and not module.functions and not module.constants:
332
+ continue
333
+ result[Path(rel_path).with_suffix(".pyi")] = render_stub(module)
334
+ return result
335
+
336
+
337
+ DEFAULT_STUBS_OUTPUT = Path(".uncoded/stubs")
338
+
339
+
340
+ def build_stubs(
341
+ source_root: Path,
342
+ output_dir: Path = DEFAULT_STUBS_OUTPUT,
343
+ *,
344
+ check: bool = False,
345
+ ) -> int:
346
+ """Sync stub files for all symbols under source_root, removing any orphans.
347
+
348
+ Writes only files whose content has changed. After reconciling the current
349
+ set of stubs, any pre-existing ``.pyi`` files in the corresponding subtree
350
+ of ``output_dir`` whose source has been removed or renamed are deleted,
351
+ and any directories left empty by the deletion are pruned. Only the
352
+ subtree corresponding to ``source_root`` is touched, so other source
353
+ roots' stubs are not affected.
354
+
355
+ When ``check=True``, the on-disk tree is not mutated; instead, prospective
356
+ writes and removals are reported and counted. Returns the number of
357
+ changes (or prospective changes).
358
+ """
359
+ changes = 0
360
+ expected: set[Path] = set()
361
+ for rel_stub_path, content in _generate_stubs(source_root).items():
362
+ stub_path = output_dir / rel_stub_path
363
+ if sync_file(stub_path, content, check=check):
364
+ changes += 1
365
+ expected.add(stub_path.resolve())
366
+
367
+ base = Path.cwd().resolve()
368
+ try:
369
+ source_rel = source_root.resolve().relative_to(base)
370
+ except ValueError:
371
+ # source_root is outside cwd; we have no safe subtree to clean.
372
+ return changes
373
+ stubs_root = output_dir / source_rel
374
+ if not stubs_root.exists():
375
+ return changes
376
+
377
+ for existing in stubs_root.rglob("*.pyi"):
378
+ if existing.resolve() not in expected and remove_file(existing, check=check):
379
+ changes += 1
380
+
381
+ if check:
382
+ return changes
383
+
384
+ # Prune now-empty directories, deepest-first, but keep stubs_root itself.
385
+ for d in sorted(stubs_root.rglob("*"), key=lambda p: len(p.parts), reverse=True):
386
+ if d.is_dir() and not any(d.iterdir()):
387
+ d.rmdir()
388
+
389
+ return changes
uncoded/sync.py ADDED
@@ -0,0 +1,50 @@
1
+ """Content-aware file writes with an optional check-only mode.
2
+
3
+ Every place that writes an artifact (namespace map, stubs, instruction-file
4
+ sections) routes through :func:`sync_file` / :func:`remove_file` so that two
5
+ concerns live in one place: only write when content actually changes, and
6
+ when ``check=True`` report the prospective action without touching disk.
7
+
8
+ ``check=True`` is what powers ``uncoded --check`` — the same pipeline runs,
9
+ but the writers never mutate the tree, and the CLI exits non-zero if any
10
+ helper reports a change. That gives CI a zero-mutation freshness gate.
11
+ """
12
+
13
+ from pathlib import Path
14
+
15
+
16
+ def sync_file(path: Path, content: str, *, check: bool = False) -> bool:
17
+ """Write ``content`` to ``path`` if it differs from what's on disk.
18
+
19
+ Prints ``Wrote``/``Updated`` in apply mode, ``Would write``/``Would
20
+ update`` in check mode. Returns ``True`` if a write was (or would be)
21
+ performed, ``False`` if the file was already up to date. Parent
22
+ directories are created as needed.
23
+ """
24
+ if not path.exists():
25
+ if not check:
26
+ path.parent.mkdir(parents=True, exist_ok=True)
27
+ path.write_text(content)
28
+ print(f"{'Would write' if check else 'Wrote'} {path}")
29
+ return True
30
+ if path.read_text() == content:
31
+ return False
32
+ if not check:
33
+ path.write_text(content)
34
+ print(f"{'Would update' if check else 'Updated'} {path}")
35
+ return True
36
+
37
+
38
+ def remove_file(path: Path, *, check: bool = False) -> bool:
39
+ """Remove ``path`` if it exists.
40
+
41
+ Prints ``Removed`` in apply mode, ``Would remove`` in check mode.
42
+ Returns ``True`` if a removal was (or would be) performed, ``False``
43
+ if the file was already absent.
44
+ """
45
+ if not path.exists():
46
+ return False
47
+ if not check:
48
+ path.unlink()
49
+ print(f"{'Would remove' if check else 'Removed'} {path}")
50
+ return True
@@ -0,0 +1,226 @@
1
+ Metadata-Version: 2.4
2
+ Name: uncoded
3
+ Version: 0.5.0
4
+ Summary: Static symbol indexes for AI-assisted code navigation
5
+ Project-URL: Homepage, https://github.com/alimanfoo/uncoded
6
+ Project-URL: Repository, https://github.com/alimanfoo/uncoded
7
+ Author-email: Alistair Miles <alimanfoo@googlemail.com>
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Programming Language :: Python :: 3
13
+ Requires-Python: >=3.12
14
+ Requires-Dist: pyyaml>=6.0
15
+ Provides-Extra: dev
16
+ Requires-Dist: pre-commit>=3.0; extra == 'dev'
17
+ Requires-Dist: pytest>=8.0; extra == 'dev'
18
+ Description-Content-Type: text/markdown
19
+
20
+ # uncoded
21
+
22
+ AI coding agents navigate codebases poorly. They grep for guessed keywords,
23
+ skim the first few lines of files, and fill gaps from pretraining rather than
24
+ reading the actual code. The result is plausible-looking output built on a
25
+ hallucinated understanding of code that's sitting right there, unread.
26
+
27
+ **uncoded** builds a static navigation index so agents can orient themselves
28
+ at the start of a task and navigate deterministically to what they need —
29
+ no guessing, no grep.
30
+
31
+ Additionally, **uncoded** provides some convenience for setting up your coding agent with a language server, so they can reliably find symbol usages and rename, edit, or delete symbols safely.
32
+
33
+ ## What it generates
34
+
35
+ Running `uncoded sync` produces:
36
+
37
+ **`.uncoded/namespace.yaml`** — a hierarchical YAML file listing every symbol:
38
+ directories, files, classes (with attributes and methods), functions. Covers
39
+ all configured source roots. An agent can load this at the start of a task
40
+ and immediately know the full vocabulary of the codebase.
41
+
42
+ **`.uncoded/stubs/`** — one `.pyi` stub per source file, with imports, full
43
+ signatures (parameter names, types, return types), first-sentence docstrings,
44
+ module constants, and class attributes.
45
+
46
+ **`.claude/skills/coherence-review/SKILL.md`** and
47
+ **`.agents/skills/coherence-review/SKILL.md`** — a coherence review skill,
48
+ written to both Claude Code and Codex skill directories (see
49
+ [Coherence review](#coherence-review) below).
50
+
51
+ `uncoded` also injects a navigation protocol into `CLAUDE.md`/`AGENTS.md`, so agents
52
+ working in the repo pick up the instructions automatically.
53
+
54
+ ## Install
55
+
56
+ ```
57
+ pip install uncoded
58
+ ```
59
+
60
+ Or with uv:
61
+
62
+ ```
63
+ uv add uncoded
64
+ ```
65
+
66
+ ## Configure
67
+
68
+ Add a `[tool.uncoded]` section to your `pyproject.toml`:
69
+
70
+ ```toml
71
+ [tool.uncoded]
72
+ source-roots = ["src", "tests"]
73
+ ```
74
+
75
+ ## Use
76
+
77
+ ```
78
+ uncoded sync
79
+ ```
80
+
81
+ Run it from the repo root. It reads `pyproject.toml` to find your source
82
+ roots, builds the index, and updates `CLAUDE.md`/`AGENTS.md`.
83
+
84
+ Commit the generated `.uncoded/` directory so agents working
85
+ in the repo always have a current index.
86
+
87
+ ## Keep it current with pre-commit
88
+
89
+ Add `uncoded sync` as a pre-commit hook so the index stays in sync
90
+ automatically:
91
+
92
+ ```yaml
93
+ - repo: local
94
+ hooks:
95
+ - id: uncoded
96
+ name: uncoded
97
+ entry: uncoded sync
98
+ language: system
99
+ pass_filenames: false
100
+ ```
101
+
102
+ Like `ruff format`: if `uncoded sync` modifies any files, the commit
103
+ fails and you stage the updated index before committing again.
104
+
105
+ You can also set up your CI to run `pre-commit run --all-files` to verify the index is up to date.
106
+
107
+ ## Verify the index is fresh
108
+
109
+ For CI or scripted checks that must not modify the working tree, use
110
+ the `check` subcommand:
111
+
112
+ ```
113
+ uncoded check
114
+ ```
115
+
116
+ It runs the same pipeline but writes nothing. Exits 0 if every generated
117
+ file is byte-identical to what a rebuild would produce, and 1 otherwise
118
+ — printing which files would change. A stale index is a silent failure
119
+ mode (agents read misleading names and signatures), so gating on this in
120
+ CI is worthwhile even alongside a pre-commit hook.
121
+
122
+ ## How agents use it
123
+
124
+ When `uncoded` is set up, a navigation section is automatically maintained in
125
+ the configured instruction files (by default, `CLAUDE.md` and `AGENTS.md`).
126
+ Agents following that protocol:
127
+
128
+ 1. Read `.uncoded/namespace.yaml` to orient — every symbol, at a glance.
129
+ 2. Read the relevant `.pyi` stubs to understand imports, signatures, constants, class members, and docstring summaries.
130
+ 3. Use Serena's `find_symbol(..., include_body=True)` when they need implementation detail for a specific symbol.
131
+
132
+ The split is deliberate: `uncoded` provides a stable map and semantic summary;
133
+ Serena resolves the current source body. No grep, no stale line-number
134
+ coordinates, no offset arithmetic.
135
+
136
+ ## Coherence review
137
+
138
+ AI coding agents tend to leave codebases in an incoherent state: names that
139
+ no longer match behaviour, docstrings that describe stale signatures, dead
140
+ symbols, pattern changes applied in some places but not others. `uncoded sync`
141
+ installs a `/coherence-review` skill that runs a structured diagnostic sweep to
142
+ find these problems.
143
+
144
+ Invoke it in Claude Code:
145
+
146
+ ```
147
+ /coherence-review
148
+ ```
149
+
150
+ The review works in four sweeps:
151
+
152
+ 1. **Orient** — loads `namespace.yaml` and forms a vocabulary map.
153
+ 2. **Lexical** — scans the namespace for naming inconsistency: concept
154
+ duplication, qualifier accretion (`_v2`, `_legacy`, `_final`), vocabulary
155
+ islands, name collision with drift.
156
+ 3. **Promissory** — reads stubs, checking each symbol's name / signature /
157
+ docstring triple for internal disagreement.
158
+ 4. **Structural** — checks for boundary violations (private symbols imported
159
+ across modules), overgrown public surfaces, cross-domain imports, and
160
+ zero-caller public symbols.
161
+
162
+ Output is a timestamped Markdown report saved to `.uncoded/reviews/`, with
163
+ verbatim evidence and a confidence level (high / medium / low) for each
164
+ finding. The review only reports — it proposes no fixes. The human decides
165
+ what to follow up.
166
+
167
+ ## Using uncoded with a language server
168
+
169
+ Symbol-level operations — finding callers, reading or editing a single
170
+ symbol, renaming, safe deletion — are better served by a language server
171
+ than by grep and freeform text edits. Uncoded's map supplies the
172
+ `name_path` and `relative_path` these tools take as input.
173
+
174
+ The recommended setup is [oraios/serena][serena] as the MCP bridge with
175
+ [astral-sh/ty][ty] as the Python language-server backend. Serena launches
176
+ via `uvx`, so there's nothing to install globally; ty is downloaded by
177
+ Serena on first use.
178
+
179
+ ### Setup
180
+
181
+ ```
182
+ uv run uncoded setup-serena
183
+ ```
184
+
185
+ Generates three files, tailored for Claude Code:
186
+
187
+ - **`.mcp.json`** — registers Serena as an MCP server, launched via `uvx`.
188
+ - **`.serena/project.yml`** — picks ty as the backend, ignores `.uncoded/`,
189
+ drops the redundant `execute_shell_command`.
190
+ - **`.claude/settings.json`** — enables the Serena server and allowlists
191
+ its navigation, edit, and memory tools.
192
+
193
+ Safe to re-run: JSON files merge into existing content (so pre-existing
194
+ MCP servers and permissions are preserved), and the Serena project YAML
195
+ is left alone once present. Restart your agent afterwards so the new
196
+ MCP server is picked up.
197
+
198
+ If you're not using Claude Code, the generated `.serena/project.yml` is
199
+ MCP-client-agnostic, and `.mcp.json` can serve as a starting point —
200
+ replace `claude-code` with your client's context name.
201
+
202
+ [serena]: https://github.com/oraios/serena
203
+ [ty]: https://github.com/astral-sh/ty
204
+
205
+ ## Dev setup
206
+
207
+ Clone, install dependencies, and wire up the pre-commit hooks:
208
+
209
+ ```
210
+ git clone https://github.com/alimanfoo/uncoded
211
+ cd uncoded
212
+ uv sync --extra dev
213
+ uv run pre-commit install
214
+ ```
215
+
216
+ Run the tests:
217
+
218
+ ```
219
+ uv run pytest
220
+ ```
221
+
222
+ Run all checks (the same suite CI runs):
223
+
224
+ ```
225
+ uv run pre-commit run --all-files
226
+ ```
@@ -0,0 +1,15 @@
1
+ uncoded/__init__.py,sha256=MjXnTazgjAK1flha1TC6c-Mz42ovqHnwtMt1fwXif2g,89
2
+ uncoded/cli.py,sha256=TflX5JSgfv9W4RekYmYEhXou1-_D3z-1CjGo7drcNzc,3310
3
+ uncoded/config.py,sha256=7GBz4IQ_i-o4cXpKjTfQHTC7HDOH4tFN_M5jUppaNj8,1832
4
+ uncoded/extract.py,sha256=9FwDgSawQd6IthL3Qy4q1kGpY19dnRK0OTU8zF1mZg8,4562
5
+ uncoded/instruction_files.py,sha256=scKbl3Y2c_80i5PCWfX_-7JIX7noveE10Bi-k9o8kzc,5955
6
+ uncoded/namespace_map.py,sha256=s_gsKxDd18UtFxu4RtIRbrGDQl1pq2Pu3urI4l1kMaA,2523
7
+ uncoded/serena_setup.py,sha256=DowjJOje2bg29lkVVqnSxtCf2yMBnnX3pflxsYUQdgE,6719
8
+ uncoded/skill.py,sha256=4SZ0zCCrLBcO5d84D7_X5b8IHO0HH-AaZNSEPIXHx08,16641
9
+ uncoded/stubs.py,sha256=ns9e7lSz3FqHOuXitLHCrGzJi_tmuYDxgI4y8LFGoLs,12967
10
+ uncoded/sync.py,sha256=CkAcliTeoFTJeCmcdZGxlcFq0YR4gHYzH-dxCD609R8,1892
11
+ uncoded-0.5.0.dist-info/METADATA,sha256=mJUY5SWEoD9V3xiRs3Xd9vzovZpY8dd3QWgfxOyDsLk,7525
12
+ uncoded-0.5.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
13
+ uncoded-0.5.0.dist-info/entry_points.txt,sha256=C6dE7We5ZSE3z2KTWBzPceaRCS8XbgZ1WG8DV4S06H0,45
14
+ uncoded-0.5.0.dist-info/licenses/LICENSE,sha256=gDwBEl-d7vujEM9osJvZhkrb3GTU8VwDfYC4ekNcT1s,1071
15
+ uncoded-0.5.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ uncoded = uncoded.cli:main