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.
- agent/__init__.py +33 -0
- agent/acp/__init__.py +2 -0
- agent/acp/approval_adapter.py +134 -0
- agent/acp/content_adapter.py +45 -0
- agent/acp/jsonrpc.py +92 -0
- agent/acp/server.py +197 -0
- agent/acp/session_manager.py +193 -0
- agent/acp/update_adapter.py +192 -0
- agent/app_paths.py +25 -0
- agent/approval.py +169 -0
- agent/cancellation.py +52 -0
- agent/change_snapshot.py +186 -0
- agent/context_compressor.py +116 -0
- agent/graph.py +137 -0
- agent/llm_retry.py +434 -0
- agent/logger.py +97 -0
- agent/lsp/__init__.py +13 -0
- agent/lsp/client.py +151 -0
- agent/lsp/manager.py +234 -0
- agent/lsp/types.py +119 -0
- agent/message_context_manager.py +322 -0
- agent/message_format.py +105 -0
- agent/nodes/llm_node.py +58 -0
- agent/nodes/state.py +12 -0
- agent/nodes/task_guard_node.py +50 -0
- agent/nodes/tools_node.py +70 -0
- agent/plan_snapshot.py +70 -0
- agent/providers/__init__.py +13 -0
- agent/providers/anthropic_provider.py +268 -0
- agent/providers/base.py +52 -0
- agent/providers/openai_provider.py +279 -0
- agent/providers/text_tool_calls.py +118 -0
- agent/runtime/approval_service.py +184 -0
- agent/runtime/context.py +43 -0
- agent/runtime/tool_events.py +368 -0
- agent/runtime/tool_executor.py +208 -0
- agent/runtime/tool_output.py +261 -0
- agent/runtime/tool_registry.py +91 -0
- agent/runtime/tool_scheduler.py +35 -0
- agent/runtime/workflow_guard.py +217 -0
- agent/runtime/workspace.py +5 -0
- agent/runtime/workspace_tools.py +22 -0
- agent/session.py +787 -0
- agent/session_replay.py +95 -0
- agent/session_store.py +186 -0
- agent/skills.py +254 -0
- agent/streaming.py +248 -0
- agent/subagent.py +634 -0
- agent/task_memory.py +340 -0
- agent/todo_manager.py +304 -0
- agent/tool_retry.py +106 -0
- agent/tui/__init__.py +14 -0
- agent/tui/app.py +1325 -0
- agent/tui/approval.py +53 -0
- agent/tui/commands/__init__.py +6 -0
- agent/tui/commands/base.py +48 -0
- agent/tui/commands/clear.py +37 -0
- agent/tui/commands/help.py +27 -0
- agent/tui/commands/registry.py +94 -0
- agent/tui/help_content.py +108 -0
- agent/tui/renderers.py +1961 -0
- agent/tui/runner.py +439 -0
- agent/tui/state.py +653 -0
- main.py +465 -0
- tools/__init__.py +50 -0
- tools/apply_patch.py +305 -0
- tools/bash.py +76 -0
- tools/diff_utils.py +139 -0
- tools/edit_file.py +40 -0
- tools/git_diff.py +72 -0
- tools/git_show.py +65 -0
- tools/grep.py +149 -0
- tools/list_files.py +90 -0
- tools/list_skills.py +24 -0
- tools/load_skill.py +30 -0
- tools/lsp_definition.py +27 -0
- tools/lsp_diagnostics.py +32 -0
- tools/lsp_document_symbols.py +23 -0
- tools/lsp_hover.py +29 -0
- tools/lsp_references.py +37 -0
- tools/lsp_utils.py +38 -0
- tools/lsp_workspace_symbols.py +23 -0
- tools/read_file.py +61 -0
- tools/read_many_files.py +50 -0
- tools/safety.py +50 -0
- tools/subagent.py +57 -0
- tools/todo.py +89 -0
- tools/verify.py +107 -0
- tools/web_search.py +250 -0
- tools/workspace.py +36 -0
- tools/workspace_state.py +60 -0
- tools/write_file.py +88 -0
- utils/__init__.py +5 -0
- utils/retry.py +13 -0
- yycode-0.3.2.data/data/skills/code_review.md +61 -0
- yycode-0.3.2.data/data/skills/code_workflow.md +404 -0
- yycode-0.3.2.data/data/skills/drawio/SKILL.md +636 -0
- yycode-0.3.2.data/data/skills/drawio/agents/openai.yaml +19 -0
- yycode-0.3.2.data/data/skills/drawio/assets/demo-erd.drawio +84 -0
- yycode-0.3.2.data/data/skills/drawio/assets/demo-layered-cn.drawio +91 -0
- yycode-0.3.2.data/data/skills/drawio/assets/demo-layered-cn.png +0 -0
- yycode-0.3.2.data/data/skills/drawio/assets/demo-layered.drawio +112 -0
- yycode-0.3.2.data/data/skills/drawio/assets/demo-layered.png +0 -0
- yycode-0.3.2.data/data/skills/drawio/assets/demo-ml.drawio +90 -0
- yycode-0.3.2.data/data/skills/drawio/assets/demo-ring-cn.drawio +68 -0
- yycode-0.3.2.data/data/skills/drawio/assets/demo-ring-cn.png +0 -0
- yycode-0.3.2.data/data/skills/drawio/assets/demo-ring.drawio +86 -0
- yycode-0.3.2.data/data/skills/drawio/assets/demo-ring.png +0 -0
- yycode-0.3.2.data/data/skills/drawio/assets/demo-sequence.drawio +116 -0
- yycode-0.3.2.data/data/skills/drawio/assets/demo-star-cn.drawio +66 -0
- yycode-0.3.2.data/data/skills/drawio/assets/demo-star-cn.png +0 -0
- yycode-0.3.2.data/data/skills/drawio/assets/demo-star.drawio +79 -0
- yycode-0.3.2.data/data/skills/drawio/assets/demo-star.png +0 -0
- yycode-0.3.2.data/data/skills/drawio/assets/demo-uml-class.drawio +64 -0
- yycode-0.3.2.data/data/skills/drawio/assets/microservices-example.drawio +173 -0
- yycode-0.3.2.data/data/skills/drawio/assets/microservices-example.png +0 -0
- yycode-0.3.2.data/data/skills/drawio/assets/workflow-cn.drawio +120 -0
- yycode-0.3.2.data/data/skills/drawio/assets/workflow-cn.png +0 -0
- yycode-0.3.2.data/data/skills/drawio/assets/workflow.drawio +120 -0
- yycode-0.3.2.data/data/skills/drawio/assets/workflow.png +0 -0
- yycode-0.3.2.data/data/skills/drawio/docs/index.html +469 -0
- yycode-0.3.2.data/data/skills/drawio/docs/zh.html +456 -0
- yycode-0.3.2.data/data/skills/drawio/references/style-extraction.md +254 -0
- yycode-0.3.2.data/data/skills/drawio/styles/schema.json +112 -0
- yycode-0.3.2.data/data/skills/plan.md +115 -0
- yycode-0.3.2.data/data/skills/ppt/SKILL.md +254 -0
- yycode-0.3.2.dist-info/METADATA +12 -0
- yycode-0.3.2.dist-info/RECORD +131 -0
- yycode-0.3.2.dist-info/WHEEL +5 -0
- yycode-0.3.2.dist-info/entry_points.txt +2 -0
- 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"]
|