mycode-coding-agent 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.
Files changed (121) hide show
  1. mycode/__init__.py +0 -0
  2. mycode/adapters/__init__.py +21 -0
  3. mycode/adapters/jsonl.py +692 -0
  4. mycode/agent/__init__.py +25 -0
  5. mycode/agent/events.py +111 -0
  6. mycode/agent/outcome.py +103 -0
  7. mycode/agent/progress.py +373 -0
  8. mycode/agent/runner.py +1481 -0
  9. mycode/application/__init__.py +38 -0
  10. mycode/application/agent_session.py +367 -0
  11. mycode/application/events.py +59 -0
  12. mycode/application/runtime.py +211 -0
  13. mycode/application/sessions.py +180 -0
  14. mycode/cli.py +840 -0
  15. mycode/config.py +355 -0
  16. mycode/context/__init__.py +1 -0
  17. mycode/context/artifacts.py +672 -0
  18. mycode/context/budget.py +752 -0
  19. mycode/context/builder.py +112 -0
  20. mycode/context/compact.py +795 -0
  21. mycode/context/tool_result_format.py +199 -0
  22. mycode/context/tool_result_retention.py +261 -0
  23. mycode/conversation.py +78 -0
  24. mycode/error_handling.py +481 -0
  25. mycode/event_format.py +147 -0
  26. mycode/instructions.py +285 -0
  27. mycode/llm.py +771 -0
  28. mycode/mcp/__init__.py +41 -0
  29. mycode/mcp/client.py +44 -0
  30. mycode/mcp/config.py +207 -0
  31. mycode/mcp/errors.py +302 -0
  32. mycode/mcp/manager.py +339 -0
  33. mycode/mcp/models.py +20 -0
  34. mycode/mcp/result_adapter.py +58 -0
  35. mycode/mcp/tool_adapter.py +145 -0
  36. mycode/mcp/trust.py +313 -0
  37. mycode/memory.py +570 -0
  38. mycode/memory_context.py +245 -0
  39. mycode/messages.py +63 -0
  40. mycode/observability.py +28 -0
  41. mycode/permissions.py +262 -0
  42. mycode/persistence/__init__.py +1 -0
  43. mycode/persistence/filesystem.py +291 -0
  44. mycode/persistence/project_storage.py +208 -0
  45. mycode/persistence/session_lock.py +138 -0
  46. mycode/persistence/session_store.py +503 -0
  47. mycode/presentation/__init__.py +1 -0
  48. mycode/presentation/cli/__init__.py +14 -0
  49. mycode/presentation/cli/confirmer.py +116 -0
  50. mycode/presentation/cli/mcp_trust.py +61 -0
  51. mycode/presentation/cli/presenter.py +320 -0
  52. mycode/presentation/cli/session_menu.py +146 -0
  53. mycode/presentation/cli/subagent_observer.py +124 -0
  54. mycode/presentation/command_format.py +90 -0
  55. mycode/presentation/commands.py +95 -0
  56. mycode/presentation/tui/__init__.py +6 -0
  57. mycode/presentation/tui/app.py +1351 -0
  58. mycode/presentation/tui/interactions.py +253 -0
  59. mycode/presentation/tui/presenter.py +266 -0
  60. mycode/presentation/tui/screens.py +305 -0
  61. mycode/presentation/tui/widgets.py +214 -0
  62. mycode/project.py +22 -0
  63. mycode/prompts.py +181 -0
  64. mycode/reasoning.py +40 -0
  65. mycode/session.py +86 -0
  66. mycode/skills/__init__.py +27 -0
  67. mycode/skills/builtin/database-recovery/SKILL.md +138 -0
  68. mycode/skills/builtin/database-recovery/references/sqlite.md +235 -0
  69. mycode/skills/registry.py +295 -0
  70. mycode/skills/state.py +68 -0
  71. mycode/subagents/__init__.py +1 -0
  72. mycode/subagents/audit.py +212 -0
  73. mycode/subagents/concurrency.py +124 -0
  74. mycode/subagents/contracts.py +421 -0
  75. mycode/subagents/delegate.py +80 -0
  76. mycode/subagents/delegation.py +128 -0
  77. mycode/subagents/lifecycle.py +86 -0
  78. mycode/subagents/limits.py +7 -0
  79. mycode/subagents/observability.py +150 -0
  80. mycode/subagents/persistence.py +152 -0
  81. mycode/subagents/profiles.py +184 -0
  82. mycode/subagents/prompts.py +67 -0
  83. mycode/subagents/results.py +178 -0
  84. mycode/subagents/runtime.py +528 -0
  85. mycode/subagents/snapshots.py +211 -0
  86. mycode/subagents/tool_batch.py +260 -0
  87. mycode/tools/__init__.py +81 -0
  88. mycode/tools/base.py +222 -0
  89. mycode/tools/bounds.py +14 -0
  90. mycode/tools/command_executor.py +167 -0
  91. mycode/tools/command_output.py +166 -0
  92. mycode/tools/command_risk.py +596 -0
  93. mycode/tools/defaults.py +59 -0
  94. mycode/tools/edit_file.py +524 -0
  95. mycode/tools/file_mutation.py +30 -0
  96. mycode/tools/glob.py +247 -0
  97. mycode/tools/grep.py +324 -0
  98. mycode/tools/ignore.py +122 -0
  99. mycode/tools/inspect_changes.py +269 -0
  100. mycode/tools/load_skill.py +92 -0
  101. mycode/tools/memory.py +264 -0
  102. mycode/tools/path_permissions.py +78 -0
  103. mycode/tools/patterns.py +48 -0
  104. mycode/tools/permission_metadata.py +27 -0
  105. mycode/tools/process_tree.py +166 -0
  106. mycode/tools/read_file.py +242 -0
  107. mycode/tools/read_skill_resource.py +93 -0
  108. mycode/tools/registry.py +279 -0
  109. mycode/tools/run_command.py +237 -0
  110. mycode/tools/run_skill_script.py +206 -0
  111. mycode/tools/run_validation.py +107 -0
  112. mycode/tools/submit_result.py +93 -0
  113. mycode/tools/text.py +15 -0
  114. mycode/tools/validation_command.py +377 -0
  115. mycode/tools/workspace.py +33 -0
  116. mycode/tools/write_file.py +169 -0
  117. mycode_coding_agent-0.1.0.dist-info/METADATA +244 -0
  118. mycode_coding_agent-0.1.0.dist-info/RECORD +121 -0
  119. mycode_coding_agent-0.1.0.dist-info/WHEEL +4 -0
  120. mycode_coding_agent-0.1.0.dist-info/entry_points.txt +2 -0
  121. mycode_coding_agent-0.1.0.dist-info/licenses/LICENSE +21 -0
mycode/instructions.py ADDED
@@ -0,0 +1,285 @@
1
+ from dataclasses import dataclass
2
+ import hashlib
3
+ from pathlib import Path
4
+ from typing import Literal
5
+
6
+
7
+ NATIVE_INSTRUCTION_FILE = "MYCODE.md"
8
+ SHARED_INSTRUCTION_FILE = "AGENTS.md"
9
+ COMPATIBLE_INSTRUCTION_FILE = "CLAUDE.md"
10
+ INSTRUCTION_FILE_PRIORITY = (
11
+ NATIVE_INSTRUCTION_FILE,
12
+ SHARED_INSTRUCTION_FILE,
13
+ COMPATIBLE_INSTRUCTION_FILE,
14
+ )
15
+ DEFAULT_MAX_INSTRUCTION_FILE_BYTES = 32 * 1024
16
+ DEFAULT_MAX_INSTRUCTION_CHARS = 64 * 1024
17
+
18
+ InstructionScope = Literal["user", "project", "directory"]
19
+ InstructionIssueCode = Literal[
20
+ "outside_scope",
21
+ "broken_symlink",
22
+ "not_a_file",
23
+ "file_too_large",
24
+ "invalid_utf8",
25
+ "total_too_large",
26
+ "read_error",
27
+ ]
28
+
29
+
30
+ class InstructionPathError(ValueError):
31
+ pass
32
+
33
+
34
+ @dataclass(frozen=True)
35
+ class InstructionLimits:
36
+ max_file_bytes: int = DEFAULT_MAX_INSTRUCTION_FILE_BYTES
37
+ max_total_chars: int = DEFAULT_MAX_INSTRUCTION_CHARS
38
+
39
+ def __post_init__(self) -> None:
40
+ if self.max_file_bytes < 1:
41
+ raise ValueError("max_file_bytes must be at least 1.")
42
+ if self.max_total_chars < 1:
43
+ raise ValueError("max_total_chars must be at least 1.")
44
+
45
+
46
+ @dataclass(frozen=True)
47
+ class InstructionSource:
48
+ path: Path
49
+ scope: InstructionScope
50
+ content: str
51
+ content_bytes: int = 0
52
+ sha256: str = ""
53
+
54
+ @property
55
+ def label(self) -> str:
56
+ return f"{self.scope}: {self.path}"
57
+
58
+
59
+ @dataclass(frozen=True)
60
+ class InstructionIssue:
61
+ path: Path
62
+ code: InstructionIssueCode
63
+ message: str
64
+
65
+ @property
66
+ def display(self) -> str:
67
+ return f"{self.code}: {self.path}: {self.message}"
68
+
69
+
70
+ @dataclass(frozen=True)
71
+ class InstructionBundle:
72
+ sources: tuple[InstructionSource, ...] = ()
73
+ issues: tuple[InstructionIssue, ...] = ()
74
+
75
+ @property
76
+ def total_chars(self) -> int:
77
+ return sum(len(source.content) for source in self.sources)
78
+
79
+ def to_prompt_text(self) -> str:
80
+ sections: list[str] = []
81
+ for source in self.sources:
82
+ sections.append(
83
+ f"### {source.label}\n\n{source.content.rstrip()}"
84
+ )
85
+ return "\n\n".join(sections)
86
+
87
+
88
+ def default_user_instruction_directory() -> Path:
89
+ return Path.home() / ".mycode"
90
+
91
+
92
+ def load_instruction_bundle(
93
+ workspace_root: str | Path,
94
+ *,
95
+ working_directory: str | Path | None = None,
96
+ user_instruction_directory: str | Path | None = None,
97
+ limits: InstructionLimits | None = None,
98
+ ) -> InstructionBundle:
99
+ resolved_workspace = _require_directory(workspace_root, label="workspace_root")
100
+ resolved_working_directory = _require_directory(
101
+ resolved_workspace if working_directory is None else working_directory,
102
+ label="working_directory",
103
+ )
104
+ if not resolved_working_directory.is_relative_to(resolved_workspace):
105
+ raise InstructionPathError(
106
+ "working_directory must be inside workspace_root: "
107
+ f"{resolved_working_directory}"
108
+ )
109
+
110
+ resolved_user_directory = Path(
111
+ default_user_instruction_directory()
112
+ if user_instruction_directory is None
113
+ else user_instruction_directory
114
+ ).resolve(strict=False)
115
+ effective_limits = InstructionLimits() if limits is None else limits
116
+
117
+ candidates: list[tuple[Path, InstructionScope, Path]] = []
118
+ user_candidate = _select_instruction_file(resolved_user_directory)
119
+ if user_candidate is not None:
120
+ candidates.append(
121
+ (user_candidate, "user", resolved_user_directory.parent)
122
+ )
123
+
124
+ directories = _instruction_directories(
125
+ resolved_workspace,
126
+ resolved_working_directory,
127
+ )
128
+ for index, directory in enumerate(directories):
129
+ candidate = _select_instruction_file(directory)
130
+ if candidate is None:
131
+ continue
132
+ scope: InstructionScope = "project" if index == 0 else "directory"
133
+ candidates.append((candidate, scope, resolved_workspace))
134
+
135
+ sources: list[InstructionSource] = []
136
+ issues: list[InstructionIssue] = []
137
+ seen_paths: set[Path] = set()
138
+ total_chars = 0
139
+
140
+ for candidate, scope, allowed_root in candidates:
141
+ source, issue = _read_instruction_source(
142
+ candidate,
143
+ scope=scope,
144
+ allowed_root=allowed_root,
145
+ limits=effective_limits,
146
+ )
147
+ if issue is not None:
148
+ issues.append(issue)
149
+ continue
150
+ if source is None or source.path in seen_paths:
151
+ continue
152
+
153
+ next_total = total_chars + len(source.content)
154
+ if next_total > effective_limits.max_total_chars:
155
+ issues.append(
156
+ InstructionIssue(
157
+ path=source.path,
158
+ code="total_too_large",
159
+ message=(
160
+ "instruction total would exceed "
161
+ f"{effective_limits.max_total_chars} characters"
162
+ ),
163
+ )
164
+ )
165
+ continue
166
+
167
+ seen_paths.add(source.path)
168
+ sources.append(source)
169
+ total_chars = next_total
170
+
171
+ return InstructionBundle(
172
+ sources=tuple(sources),
173
+ issues=tuple(issues),
174
+ )
175
+
176
+
177
+ def _require_directory(path: str | Path, *, label: str) -> Path:
178
+ resolved = Path(path).resolve(strict=False)
179
+ if not resolved.exists():
180
+ raise InstructionPathError(f"{label} does not exist: {resolved}")
181
+ if not resolved.is_dir():
182
+ raise InstructionPathError(f"{label} is not a directory: {resolved}")
183
+ return resolved
184
+
185
+
186
+ def _select_instruction_file(directory: Path) -> Path | None:
187
+ for file_name in INSTRUCTION_FILE_PRIORITY:
188
+ candidate = directory / file_name
189
+ if candidate.exists() or candidate.is_symlink():
190
+ return candidate
191
+
192
+ return None
193
+
194
+
195
+ def _instruction_directories(workspace_root: Path, working_directory: Path) -> list[Path]:
196
+ directories = [workspace_root]
197
+ current = workspace_root
198
+ relative = working_directory.relative_to(workspace_root)
199
+ for part in relative.parts:
200
+ current = current / part
201
+ directories.append(current)
202
+ return directories
203
+
204
+
205
+ def _read_instruction_source(
206
+ path: Path,
207
+ *,
208
+ scope: InstructionScope,
209
+ allowed_root: Path,
210
+ limits: InstructionLimits,
211
+ ) -> tuple[InstructionSource | None, InstructionIssue | None]:
212
+ if path.is_symlink() and not path.exists():
213
+ return None, InstructionIssue(
214
+ path=path,
215
+ code="broken_symlink",
216
+ message="instruction symlink target does not exist",
217
+ )
218
+
219
+ try:
220
+ resolved = path.resolve(strict=True)
221
+ except OSError as error:
222
+ return None, InstructionIssue(
223
+ path=path,
224
+ code="read_error",
225
+ message=str(error),
226
+ )
227
+
228
+ if not resolved.is_relative_to(allowed_root.resolve(strict=False)):
229
+ return None, InstructionIssue(
230
+ path=path,
231
+ code="outside_scope",
232
+ message=f"resolved path is outside {allowed_root}",
233
+ )
234
+
235
+ if not resolved.is_file():
236
+ return None, InstructionIssue(
237
+ path=resolved,
238
+ code="not_a_file",
239
+ message="instruction path is not a regular file",
240
+ )
241
+
242
+ try:
243
+ file_size = resolved.stat().st_size
244
+ if file_size > limits.max_file_bytes:
245
+ return None, InstructionIssue(
246
+ path=resolved,
247
+ code="file_too_large",
248
+ message=(
249
+ f"file is {file_size} bytes; limit is "
250
+ f"{limits.max_file_bytes} bytes"
251
+ ),
252
+ )
253
+ raw_content = resolved.read_bytes()
254
+ if len(raw_content) > limits.max_file_bytes:
255
+ return None, InstructionIssue(
256
+ path=resolved,
257
+ code="file_too_large",
258
+ message=(
259
+ f"file is {len(raw_content)} bytes; limit is "
260
+ f"{limits.max_file_bytes} bytes"
261
+ ),
262
+ )
263
+ except OSError as error:
264
+ return None, InstructionIssue(
265
+ path=resolved,
266
+ code="read_error",
267
+ message=str(error),
268
+ )
269
+
270
+ try:
271
+ content = raw_content.decode("utf-8-sig")
272
+ except UnicodeDecodeError:
273
+ return None, InstructionIssue(
274
+ path=resolved,
275
+ code="invalid_utf8",
276
+ message="instruction file must be valid UTF-8",
277
+ )
278
+
279
+ return InstructionSource(
280
+ path=resolved,
281
+ scope=scope,
282
+ content=content,
283
+ content_bytes=len(raw_content),
284
+ sha256=hashlib.sha256(raw_content).hexdigest(),
285
+ ), None