yycode 0.3.2__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 (131) hide show
  1. agent/__init__.py +33 -0
  2. agent/acp/__init__.py +2 -0
  3. agent/acp/approval_adapter.py +134 -0
  4. agent/acp/content_adapter.py +45 -0
  5. agent/acp/jsonrpc.py +92 -0
  6. agent/acp/server.py +197 -0
  7. agent/acp/session_manager.py +193 -0
  8. agent/acp/update_adapter.py +192 -0
  9. agent/app_paths.py +25 -0
  10. agent/approval.py +169 -0
  11. agent/cancellation.py +52 -0
  12. agent/change_snapshot.py +186 -0
  13. agent/context_compressor.py +116 -0
  14. agent/graph.py +137 -0
  15. agent/llm_retry.py +434 -0
  16. agent/logger.py +97 -0
  17. agent/lsp/__init__.py +13 -0
  18. agent/lsp/client.py +151 -0
  19. agent/lsp/manager.py +234 -0
  20. agent/lsp/types.py +119 -0
  21. agent/message_context_manager.py +322 -0
  22. agent/message_format.py +105 -0
  23. agent/nodes/llm_node.py +58 -0
  24. agent/nodes/state.py +12 -0
  25. agent/nodes/task_guard_node.py +50 -0
  26. agent/nodes/tools_node.py +70 -0
  27. agent/plan_snapshot.py +70 -0
  28. agent/providers/__init__.py +13 -0
  29. agent/providers/anthropic_provider.py +268 -0
  30. agent/providers/base.py +52 -0
  31. agent/providers/openai_provider.py +279 -0
  32. agent/providers/text_tool_calls.py +118 -0
  33. agent/runtime/approval_service.py +184 -0
  34. agent/runtime/context.py +43 -0
  35. agent/runtime/tool_events.py +368 -0
  36. agent/runtime/tool_executor.py +208 -0
  37. agent/runtime/tool_output.py +261 -0
  38. agent/runtime/tool_registry.py +91 -0
  39. agent/runtime/tool_scheduler.py +35 -0
  40. agent/runtime/workflow_guard.py +217 -0
  41. agent/runtime/workspace.py +5 -0
  42. agent/runtime/workspace_tools.py +22 -0
  43. agent/session.py +787 -0
  44. agent/session_replay.py +95 -0
  45. agent/session_store.py +186 -0
  46. agent/skills.py +254 -0
  47. agent/streaming.py +248 -0
  48. agent/subagent.py +634 -0
  49. agent/task_memory.py +340 -0
  50. agent/todo_manager.py +304 -0
  51. agent/tool_retry.py +106 -0
  52. agent/tui/__init__.py +14 -0
  53. agent/tui/app.py +1325 -0
  54. agent/tui/approval.py +53 -0
  55. agent/tui/commands/__init__.py +6 -0
  56. agent/tui/commands/base.py +48 -0
  57. agent/tui/commands/clear.py +37 -0
  58. agent/tui/commands/help.py +27 -0
  59. agent/tui/commands/registry.py +94 -0
  60. agent/tui/help_content.py +108 -0
  61. agent/tui/renderers.py +1961 -0
  62. agent/tui/runner.py +439 -0
  63. agent/tui/state.py +653 -0
  64. main.py +465 -0
  65. tools/__init__.py +50 -0
  66. tools/apply_patch.py +305 -0
  67. tools/bash.py +76 -0
  68. tools/diff_utils.py +139 -0
  69. tools/edit_file.py +40 -0
  70. tools/git_diff.py +72 -0
  71. tools/git_show.py +65 -0
  72. tools/grep.py +149 -0
  73. tools/list_files.py +90 -0
  74. tools/list_skills.py +24 -0
  75. tools/load_skill.py +30 -0
  76. tools/lsp_definition.py +27 -0
  77. tools/lsp_diagnostics.py +32 -0
  78. tools/lsp_document_symbols.py +23 -0
  79. tools/lsp_hover.py +29 -0
  80. tools/lsp_references.py +37 -0
  81. tools/lsp_utils.py +38 -0
  82. tools/lsp_workspace_symbols.py +23 -0
  83. tools/read_file.py +61 -0
  84. tools/read_many_files.py +50 -0
  85. tools/safety.py +50 -0
  86. tools/subagent.py +57 -0
  87. tools/todo.py +89 -0
  88. tools/verify.py +107 -0
  89. tools/web_search.py +250 -0
  90. tools/workspace.py +36 -0
  91. tools/workspace_state.py +60 -0
  92. tools/write_file.py +88 -0
  93. utils/__init__.py +5 -0
  94. utils/retry.py +13 -0
  95. yycode-0.3.2.data/data/skills/code_review.md +61 -0
  96. yycode-0.3.2.data/data/skills/code_workflow.md +404 -0
  97. yycode-0.3.2.data/data/skills/drawio/SKILL.md +636 -0
  98. yycode-0.3.2.data/data/skills/drawio/agents/openai.yaml +19 -0
  99. yycode-0.3.2.data/data/skills/drawio/assets/demo-erd.drawio +84 -0
  100. yycode-0.3.2.data/data/skills/drawio/assets/demo-layered-cn.drawio +91 -0
  101. yycode-0.3.2.data/data/skills/drawio/assets/demo-layered-cn.png +0 -0
  102. yycode-0.3.2.data/data/skills/drawio/assets/demo-layered.drawio +112 -0
  103. yycode-0.3.2.data/data/skills/drawio/assets/demo-layered.png +0 -0
  104. yycode-0.3.2.data/data/skills/drawio/assets/demo-ml.drawio +90 -0
  105. yycode-0.3.2.data/data/skills/drawio/assets/demo-ring-cn.drawio +68 -0
  106. yycode-0.3.2.data/data/skills/drawio/assets/demo-ring-cn.png +0 -0
  107. yycode-0.3.2.data/data/skills/drawio/assets/demo-ring.drawio +86 -0
  108. yycode-0.3.2.data/data/skills/drawio/assets/demo-ring.png +0 -0
  109. yycode-0.3.2.data/data/skills/drawio/assets/demo-sequence.drawio +116 -0
  110. yycode-0.3.2.data/data/skills/drawio/assets/demo-star-cn.drawio +66 -0
  111. yycode-0.3.2.data/data/skills/drawio/assets/demo-star-cn.png +0 -0
  112. yycode-0.3.2.data/data/skills/drawio/assets/demo-star.drawio +79 -0
  113. yycode-0.3.2.data/data/skills/drawio/assets/demo-star.png +0 -0
  114. yycode-0.3.2.data/data/skills/drawio/assets/demo-uml-class.drawio +64 -0
  115. yycode-0.3.2.data/data/skills/drawio/assets/microservices-example.drawio +173 -0
  116. yycode-0.3.2.data/data/skills/drawio/assets/microservices-example.png +0 -0
  117. yycode-0.3.2.data/data/skills/drawio/assets/workflow-cn.drawio +120 -0
  118. yycode-0.3.2.data/data/skills/drawio/assets/workflow-cn.png +0 -0
  119. yycode-0.3.2.data/data/skills/drawio/assets/workflow.drawio +120 -0
  120. yycode-0.3.2.data/data/skills/drawio/assets/workflow.png +0 -0
  121. yycode-0.3.2.data/data/skills/drawio/docs/index.html +469 -0
  122. yycode-0.3.2.data/data/skills/drawio/docs/zh.html +456 -0
  123. yycode-0.3.2.data/data/skills/drawio/references/style-extraction.md +254 -0
  124. yycode-0.3.2.data/data/skills/drawio/styles/schema.json +112 -0
  125. yycode-0.3.2.data/data/skills/plan.md +115 -0
  126. yycode-0.3.2.data/data/skills/ppt/SKILL.md +254 -0
  127. yycode-0.3.2.dist-info/METADATA +12 -0
  128. yycode-0.3.2.dist-info/RECORD +131 -0
  129. yycode-0.3.2.dist-info/WHEEL +5 -0
  130. yycode-0.3.2.dist-info/entry_points.txt +2 -0
  131. yycode-0.3.2.dist-info/top_level.txt +4 -0
main.py ADDED
@@ -0,0 +1,465 @@
1
+ #!/usr/bin/env python3
2
+ """Main entry point with TUI default startup."""
3
+
4
+ import os
5
+ import argparse
6
+ import asyncio
7
+ import contextlib
8
+ import textwrap
9
+ from datetime import datetime
10
+ from pathlib import Path
11
+
12
+
13
+ from dotenv import load_dotenv
14
+
15
+ from agent import Session
16
+ from agent.approval import ApprovalRequest
17
+ from agent.app_paths import resolve_app_root, resolve_runtime_data_dir
18
+ from agent.logger import setup_logging
19
+ from agent.session_store import FileSessionStore
20
+ from agent.streaming import colorize_diff
21
+ from agent.logger import LOG_FILE_NAME
22
+
23
+ # Try to enable readline for better input experience
24
+ try:
25
+ import readline
26
+
27
+ histfile = os.path.join(os.path.expanduser("~"), ".yycode_history")
28
+ try:
29
+ readline.read_history_file(histfile)
30
+ except FileNotFoundError:
31
+ pass
32
+
33
+ readline.set_history_length(1000)
34
+
35
+ if 'libedit' in readline.__doc__:
36
+ readline.parse_and_bind("bind ^I rl_complete")
37
+ else:
38
+ readline.parse_and_bind("tab: complete")
39
+ except ImportError:
40
+ readline = None
41
+
42
+
43
+ LOGO = """
44
+ __ __ ___ __
45
+ \\ \\/ /___ __ ______ / | ____ ____ ____ / /_
46
+ \\ / __ \\/ / / / __ \\ / /| |/ __ `/ _ \\/ __ \\/ __/
47
+ / / /_/ / /_/ / /_/ / / ___ / /_/ / __/ / / / /_
48
+ /_/\\____/\\__, /\\____/ /_/ |_\\__, /\\___/_/ /_/\\__/
49
+ /____/ /____/
50
+ """
51
+
52
+
53
+ PASTE_COMMANDS = {"/p", "/paste"}
54
+ READLINE_PROMPT_START = "\001"
55
+ READLINE_PROMPT_END = "\002"
56
+ ANSI_CYAN = "\033[36m"
57
+ ANSI_GRAY = "\033[90m"
58
+ ANSI_RESET = "\033[0m"
59
+
60
+
61
+ def _protect_prompt_color(sequence: str) -> str:
62
+ """Mark ANSI escape sequences as zero-width for readline prompt editing."""
63
+ if readline is None:
64
+ return sequence
65
+ return f"{READLINE_PROMPT_START}{sequence}{READLINE_PROMPT_END}"
66
+
67
+
68
+ def cyan(text: str) -> str:
69
+ """Return cyan text, with ANSI escapes protected for readline prompts."""
70
+ return f"{_protect_prompt_color(ANSI_CYAN)}{text}{_protect_prompt_color(ANSI_RESET)}"
71
+
72
+
73
+ def gray(text: str) -> str:
74
+ """Return gray text, with ANSI escapes protected for readline prompts."""
75
+ return f"{_protect_prompt_color(ANSI_GRAY)}{text}{_protect_prompt_color(ANSI_RESET)}"
76
+
77
+
78
+ def read_multiline_input(input_func=input) -> str:
79
+ """Read pasted multiline input until a line containing only /end."""
80
+ print("\033[90mPaste multiline input. Submit with /end on its own line.\033[0m")
81
+ lines = []
82
+ while True:
83
+ line = input_func(cyan("... >> "))
84
+ if line.strip() == "/end":
85
+ break
86
+ lines.append(line)
87
+ return "\n".join(lines)
88
+
89
+
90
+ async def read_user_query(input_func=input) -> str:
91
+ """Read a single-line query or a multiline paste block."""
92
+ query = await asyncio.to_thread(input_func, cyan("yoyo >> "))
93
+ if query.strip().lower() in PASTE_COMMANDS:
94
+ query = await asyncio.to_thread(read_multiline_input, input_func)
95
+ return query
96
+
97
+
98
+ def build_prompt(session: Session) -> str:
99
+ """Build the interactive prompt with current context window pressure."""
100
+ estimated_tokens = session.estimate_token_usage()
101
+ formatted_used = format_token_count(estimated_tokens)
102
+ formatted_window = format_token_count(session.context_window_tokens)
103
+ percent = format_context_percent(session.estimate_context_window_percent())
104
+ return f"{gray(f'[{formatted_used}/{formatted_window} {percent}]')} {cyan('yoyo >> ')}"
105
+
106
+
107
+ def format_token_count(count: int) -> str:
108
+ """Format token counts using k/m suffixes."""
109
+ if count < 1_000:
110
+ return str(count)
111
+ if count < 1_000_000:
112
+ return _format_compact_number(count / 1_000, "k")
113
+ return _format_compact_number(count / 1_000_000, "m")
114
+
115
+
116
+ def format_context_percent(percent: float) -> str:
117
+ """Format context window usage percentage."""
118
+ if percent < 10:
119
+ return f"{percent:.1f}%"
120
+ return f"{percent:.0f}%"
121
+
122
+
123
+ def _format_compact_number(value: float, suffix: str) -> str:
124
+ """Format a compact number and trim a trailing .0."""
125
+ formatted = f"{value:.1f}"
126
+ if formatted.endswith(".0"):
127
+ formatted = formatted[:-2]
128
+ return f"{formatted}{suffix}"
129
+
130
+
131
+ async def read_user_query_with_session(session: Session, input_func=input) -> str:
132
+ """Read a query using a prompt that includes current token usage."""
133
+ prompt = build_prompt(session)
134
+ query = await asyncio.to_thread(input_func, prompt)
135
+ if query.strip().lower() in PASTE_COMMANDS:
136
+ query = await asyncio.to_thread(read_multiline_input, input_func)
137
+ return query
138
+
139
+
140
+ async def console_approval_callback(request: ApprovalRequest) -> bool:
141
+ """Ask the user to approve a risky tool execution in the console."""
142
+ print()
143
+ print(request.format(include_diff=False))
144
+ if request.diff_preview:
145
+ print("\033[90mdiff_preview:\033[0m")
146
+ print(colorize_diff(request.diff_preview))
147
+ answer = await asyncio.to_thread(
148
+ input,
149
+ "\033[33mApprove this action? [y/N] \033[0m",
150
+ )
151
+ return answer.strip().lower() in {"y", "yes"}
152
+
153
+
154
+ async def auto_approval_callback(_request: ApprovalRequest) -> bool:
155
+ """Approve runtime approval requests without prompting."""
156
+ return True
157
+
158
+
159
+ def env_flag_enabled(name: str) -> bool:
160
+ """Return whether an environment flag is truthy."""
161
+ return os.environ.get(name, "").strip().lower() in {"1", "true", "yes", "y", "on"}
162
+
163
+
164
+ def format_startup_info(session: Session) -> str:
165
+ """Return non-sensitive startup details for the current session."""
166
+ model = getattr(session.provider, "model", "(unknown)")
167
+ skill_names = [skill.name for skill in session.skill_registry.list_skills()]
168
+ skills = ", ".join(skill_names) if skill_names else "(none)"
169
+ restored_message_count = getattr(session, "restored_message_count", 0)
170
+ restore_line = (
171
+ f"\033[90mRestored messages: {restored_message_count}\033[0m"
172
+ if restored_message_count
173
+ else None
174
+ )
175
+ lines = [
176
+ f"\033[90mSession ID: {session.id}\033[0m",
177
+ f"\033[90mModel: {model}\033[0m",
178
+ f"\033[90mSkills: {skills}\033[0m",
179
+ ]
180
+ if restore_line:
181
+ lines.append(restore_line)
182
+ return "\n".join(
183
+ lines
184
+ )
185
+
186
+
187
+ def resolve_startup_workdir(raw_workdir: str | None) -> Path:
188
+ """Resolve and validate the optional positional workspace argument."""
189
+ workdir = Path(raw_workdir).expanduser().resolve() if raw_workdir else Path.cwd().resolve()
190
+ if not workdir.exists():
191
+ raise SystemExit(f"Error: workspace does not exist: {workdir}")
192
+ if not workdir.is_dir():
193
+ raise SystemExit(f"Error: workspace is not a directory: {workdir}")
194
+ return workdir
195
+
196
+
197
+ async def run_agent_task(session: Session, query: str) -> bool:
198
+ """Run one agent task and let Ctrl+C cancel the task without exiting the CLI."""
199
+ task = asyncio.create_task(session.send(query))
200
+ try:
201
+ await task
202
+ print("\n")
203
+ return True
204
+ except (KeyboardInterrupt, asyncio.CancelledError):
205
+ task.cancel()
206
+ with contextlib.suppress(asyncio.CancelledError):
207
+ await task
208
+ print("\n\033[90m[current task cancelled]\033[0m\n")
209
+ return False
210
+
211
+
212
+ def build_arg_parser() -> argparse.ArgumentParser:
213
+ """Build the command-line parser with user-facing help text."""
214
+ examples = """\
215
+ Examples:
216
+ yycode
217
+ yycode ~/project
218
+ yycode --acp
219
+ yycode acp
220
+ yycode -s
221
+ yycode -r bugfix-123
222
+ yycode -x bugfix-123
223
+ yycode ~/project -t
224
+ yycode -a
225
+ yycode --plain
226
+
227
+ Session data:
228
+ Messages are saved by default under {app_root}/sessions/{workspace_hash}/{session_id}.json.
229
+ Use -s/--sessions to inspect saved sessions for WORKDIR.
230
+ Use -r/--resume ID to continue a previous conversation in the same workspace.
231
+ Use -x/--delete ID to delete a saved session for WORKDIR.
232
+
233
+ Environment:
234
+ PROVIDER LLM provider: anthropic or openai.
235
+ API_KEY API key for the selected provider.
236
+ API_BASE Optional custom API base URL.
237
+ AI_MODEL Model name override.
238
+ YOYO_APP_ROOT Yoyo Agent app/release directory.
239
+ YOYO_RUNTIME_DATA_DIR Runtime data directory; defaults to app_root.
240
+ YOYO_SESSION_DIR Session messages directory override.
241
+ YOYO_SKILL_DIRS Extra skill directories, separated by comma/pathsep.
242
+ YOYO_CONTEXT_WINDOW_TOKENS Context window size override for token pressure.
243
+ YOYO_SILENT Auto-approve risky actions when truthy.
244
+ YOYO_AUTO_APPROVE Alias for YOYO_SILENT.
245
+ """
246
+ parser = argparse.ArgumentParser(
247
+ prog="yycode",
248
+ description="yycode - terminal coding assistant with workspace tools and session persistence.",
249
+ epilog=textwrap.dedent(examples),
250
+ formatter_class=argparse.RawDescriptionHelpFormatter,
251
+ )
252
+ parser.add_argument(
253
+ "workdir",
254
+ nargs="?",
255
+ metavar="WORKDIR",
256
+ help="Workspace directory to operate on. Defaults to the current directory.",
257
+ )
258
+ parser.add_argument(
259
+ "-d",
260
+ "--debug",
261
+ action="store_true",
262
+ help="Enable debug logging to console.",
263
+ )
264
+ parser.add_argument(
265
+ "--acp",
266
+ action="store_true",
267
+ help="Run the Agent Client Protocol stdio server.",
268
+ )
269
+ parser.add_argument(
270
+ "--log-file",
271
+ action="store_true",
272
+ help="Write logs to agent_debug.log.",
273
+ )
274
+ parser.add_argument(
275
+ "--plain",
276
+ action="store_true",
277
+ help="Use plain terminal input mode instead of the Textual TUI.",
278
+ )
279
+ parser.add_argument(
280
+ "-a",
281
+ "--auto",
282
+ dest="auto",
283
+ action="store_true",
284
+ help="Auto-approve risky actions.",
285
+ )
286
+ parser.add_argument(
287
+ "--silent",
288
+ dest="auto",
289
+ action="store_true",
290
+ help=argparse.SUPPRESS,
291
+ )
292
+ parser.add_argument(
293
+ "-r",
294
+ "--resume",
295
+ metavar="ID",
296
+ help="Resume messages from the persisted session id in the same workspace.",
297
+ )
298
+ parser.add_argument(
299
+ "-s",
300
+ "--sessions",
301
+ dest="sessions",
302
+ action="store_true",
303
+ help="List persisted sessions for WORKDIR and exit.",
304
+ )
305
+ parser.add_argument(
306
+ "--list-sessions",
307
+ dest="sessions",
308
+ action="store_true",
309
+ help=argparse.SUPPRESS,
310
+ )
311
+ parser.add_argument(
312
+ "-t",
313
+ "--temp",
314
+ dest="temp",
315
+ action="store_true",
316
+ help="Temporary session; do not save messages.",
317
+ )
318
+ parser.add_argument(
319
+ "-x",
320
+ "--delete",
321
+ metavar="ID",
322
+ help="Delete a persisted session id for WORKDIR and exit.",
323
+ )
324
+ parser.add_argument(
325
+ "--no-persist",
326
+ dest="temp",
327
+ action="store_true",
328
+ help=argparse.SUPPRESS,
329
+ )
330
+ return parser
331
+
332
+
333
+ async def run_plain_loop(args: argparse.Namespace, input_func=input) -> None:
334
+ """Run the agent with ordinary terminal input as a TUI fallback."""
335
+ approval_callback = auto_approval_callback if args.auto else console_approval_callback
336
+ session = Session.from_config(
337
+ workdir=args.workdir,
338
+ session_id=args.session_id,
339
+ approval_callback=approval_callback,
340
+ persist_messages=not args.temp,
341
+ resume=bool(args.resume),
342
+ )
343
+ print(format_startup_info(session))
344
+ print("\033[90mPlain input mode. Type q or exit to quit. Use /paste and /end for multiline input.\033[0m\n")
345
+ try:
346
+ while True:
347
+ try:
348
+ query = await read_user_query_with_session(session, input_func)
349
+ except EOFError:
350
+ print()
351
+ break
352
+ except KeyboardInterrupt:
353
+ print("\n\033[90mInterrupted. Type q or exit to quit.\033[0m\n")
354
+ continue
355
+ if query.strip().lower() in {"q", "exit"}:
356
+ break
357
+ if not query.strip():
358
+ continue
359
+ await run_agent_task(session, query)
360
+ finally:
361
+ await session.close()
362
+
363
+
364
+ def list_sessions_for_workdir(workdir: Path) -> str:
365
+ """Return a display table of persisted sessions for a workspace."""
366
+ store = create_session_store_for_workdir(workdir)
367
+ records = store.list_sessions()
368
+ if not records:
369
+ return f"No sessions found for workspace: {workdir}"
370
+
371
+ lines = [
372
+ f"Sessions for workspace: {workdir}",
373
+ "",
374
+ f"{'Session ID':<40} {'Updated':<25} Workdir",
375
+ f"{'-' * 40} {'-' * 25} {'-' * 7}",
376
+ ]
377
+ for record in records:
378
+ lines.append(f"{record.session_id:<40} {format_session_updated_at(record.updated_at):<25} {record.workdir}")
379
+ return "\n".join(lines)
380
+
381
+
382
+ def delete_session_for_workdir(workdir: Path, session_id: str) -> str:
383
+ """Delete a persisted session for a workspace."""
384
+ store = create_session_store_for_workdir(workdir)
385
+ before = {record.session_id for record in store.list_sessions()}
386
+ store.delete(session_id)
387
+ if session_id not in before:
388
+ return f"No session found for workspace {workdir}: {session_id}"
389
+ return f"Deleted session for workspace {workdir}: {session_id}"
390
+
391
+
392
+ def create_session_store_for_workdir(workdir: Path) -> FileSessionStore:
393
+ """Create the default file session store for a workspace."""
394
+ app_root = resolve_app_root()
395
+ runtime_data_dir = resolve_runtime_data_dir(app_root)
396
+ session_root = None if os.environ.get("YOYO_SESSION_DIR") else runtime_data_dir / "sessions"
397
+ return FileSessionStore(app_root=app_root, workdir=workdir, root=session_root)
398
+
399
+
400
+ def resolve_log_file_path() -> Path:
401
+ """Return the fixed application log file path."""
402
+ app_root = resolve_app_root()
403
+ runtime_data_dir = resolve_runtime_data_dir(app_root)
404
+ return runtime_data_dir / "logs" / LOG_FILE_NAME
405
+
406
+
407
+ def format_session_updated_at(value: str) -> str:
408
+ """Format persisted session timestamps for CLI display."""
409
+ if not value:
410
+ return ""
411
+ try:
412
+ parsed = datetime.fromisoformat(value.replace("Z", "+00:00"))
413
+ except ValueError:
414
+ return value
415
+ return parsed.strftime("%Y-%m-%d %H:%M")
416
+
417
+
418
+ def main() -> None:
419
+ """Parse startup args and launch the TUI on the main thread."""
420
+ parser = build_arg_parser()
421
+ args = parser.parse_args()
422
+ log_file_path = resolve_log_file_path()
423
+ if args.acp or args.workdir == "acp":
424
+ setup_logging(debug=args.debug, log_to_file=args.log_file, log_file=log_file_path)
425
+ load_dotenv(override=True)
426
+ auto_approve = args.auto or env_flag_enabled("YOYO_SILENT") or env_flag_enabled("YOYO_AUTO_APPROVE")
427
+ from agent.acp.server import main as acp_main
428
+
429
+ acp_main(auto_approve=auto_approve)
430
+ return
431
+ args.workdir = resolve_startup_workdir(args.workdir)
432
+ args.session_id = args.resume
433
+
434
+ if args.sessions:
435
+ print(list_sessions_for_workdir(args.workdir))
436
+ return
437
+ if args.delete:
438
+ print(delete_session_for_workdir(args.workdir, args.delete))
439
+ return
440
+
441
+ # Set up logging
442
+ setup_logging(debug=args.debug, log_to_file=args.log_file, log_file=log_file_path)
443
+
444
+ print("\033[33m" + LOGO + "\033[0m")
445
+ startup_mode = "plain input" if args.plain else "TUI"
446
+ print(f"yycode - Starting {startup_mode}...\n")
447
+ if args.debug:
448
+ print(f"\033[90m[DEBUG] Debug mode enabled. Logs written to {log_file_path}\033[0m\n")
449
+
450
+ load_dotenv(override=True)
451
+ if args.auto or env_flag_enabled("YOYO_SILENT") or env_flag_enabled("YOYO_AUTO_APPROVE"):
452
+ args.auto = True
453
+ print("\033[90m[SILENT] Approval prompts disabled; risky actions auto-approved.\033[0m\n")
454
+
455
+ if args.plain:
456
+ asyncio.run(run_plain_loop(args))
457
+ return
458
+
459
+ from agent.tui.app import run_tui
460
+
461
+ run_tui(args)
462
+
463
+
464
+ if __name__ == "__main__":
465
+ main()
tools/__init__.py ADDED
@@ -0,0 +1,50 @@
1
+ """Tools package - auto-register tools."""
2
+
3
+ import importlib
4
+ import pkgutil
5
+ from pathlib import Path
6
+ from typing import Dict, Callable, Any
7
+
8
+ TOOL_HANDLERS: Dict[str, Callable] = {}
9
+ TOOLS: list[Dict[str, Any]] = []
10
+
11
+
12
+ def register_tool(handler: Callable, tool_def: Dict[str, Any]) -> None:
13
+ """Register a tool with its handler."""
14
+ tool_name = tool_def["name"]
15
+ TOOL_HANDLERS[tool_name] = handler
16
+ TOOLS.append(tool_def)
17
+
18
+
19
+ def auto_register_tools() -> None:
20
+ """Auto-discover and register all tools in the tools package."""
21
+ package_dir = Path(__file__).parent
22
+
23
+ for _, module_name, _ in pkgutil.iter_modules([str(package_dir)]):
24
+ if module_name == "__init__":
25
+ continue
26
+
27
+ module = importlib.import_module(f".{module_name}", __name__)
28
+
29
+ # Look for tool definition (ends with '_tool')
30
+ tool_def = None
31
+ handler = None
32
+ tool_name = None
33
+
34
+ for attr_name in dir(module):
35
+ if attr_name.endswith("_tool") and isinstance(getattr(module, attr_name), dict):
36
+ tool_def = getattr(module, attr_name)
37
+ tool_name = tool_def["name"]
38
+ # Try to find matching handler (same name as tool)
39
+ if hasattr(module, tool_name):
40
+ handler = getattr(module, tool_name)
41
+ break
42
+
43
+ if tool_def and handler:
44
+ register_tool(handler, tool_def)
45
+
46
+
47
+ # Auto-register tools on import
48
+ auto_register_tools()
49
+
50
+ __all__ = ["TOOL_HANDLERS", "TOOLS", "register_tool"]