sycommon-python-lib 0.2.6a4__py3-none-any.whl → 0.2.6a5__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.
- sycli/acp/__init__.py +48 -0
- sycli/acp/config.py +112 -0
- sycli/acp/executor.py +332 -0
- sycli/acp/registry.py +155 -0
- sycli/acp/server.py +69 -0
- sycli/cli.py +11 -0
- sycli/commands/acp_cmd.py +116 -0
- {sycommon_python_lib-0.2.6a4.dist-info → sycommon_python_lib-0.2.6a5.dist-info}/METADATA +10 -9
- {sycommon_python_lib-0.2.6a4.dist-info → sycommon_python_lib-0.2.6a5.dist-info}/RECORD +12 -6
- {sycommon_python_lib-0.2.6a4.dist-info → sycommon_python_lib-0.2.6a5.dist-info}/WHEEL +0 -0
- {sycommon_python_lib-0.2.6a4.dist-info → sycommon_python_lib-0.2.6a5.dist-info}/entry_points.txt +0 -0
- {sycommon_python_lib-0.2.6a4.dist-info → sycommon_python_lib-0.2.6a5.dist-info}/top_level.txt +0 -0
sycli/acp/__init__.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""ACP (Agent Communication Protocol) support for sycli.
|
|
2
|
+
|
|
3
|
+
Dynamically exposes local CLI tools as ACP agents.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
# Patch uvicorn config type aliases for compatibility with uvicorn>=0.34
|
|
7
|
+
# acp-sdk uses uvicorn.config.LoopSetupType etc. which were removed in uvicorn 0.48+
|
|
8
|
+
import uvicorn.config as _uvicorn_config
|
|
9
|
+
|
|
10
|
+
_TYPE_ALIASES = (
|
|
11
|
+
"LoopSetupType",
|
|
12
|
+
"HTTPProtocolType",
|
|
13
|
+
"WSProtocolType",
|
|
14
|
+
"LifespanType",
|
|
15
|
+
"InterfaceType",
|
|
16
|
+
)
|
|
17
|
+
for _alias in _TYPE_ALIASES:
|
|
18
|
+
if not hasattr(_uvicorn_config, _alias):
|
|
19
|
+
setattr(_uvicorn_config, _alias, str)
|
|
20
|
+
|
|
21
|
+
if not hasattr(_uvicorn_config, "LOGGING_CONFIG"):
|
|
22
|
+
_uvicorn_config.LOGGING_CONFIG = {
|
|
23
|
+
"version": 1,
|
|
24
|
+
"disable_existing_loggers": False,
|
|
25
|
+
"formatters": {},
|
|
26
|
+
"handlers": {},
|
|
27
|
+
"loggers": {},
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
del _uvicorn_config, _TYPE_ALIASES, _alias
|
|
31
|
+
|
|
32
|
+
from sycli.acp.config import ACPConfig, ToolArgument, ToolDef
|
|
33
|
+
from sycli.acp.executor import build_cli_command, execute_command, stream_command
|
|
34
|
+
from sycli.acp.registry import CLIToolAgent, build_agents
|
|
35
|
+
from sycli.acp.server import create_acp_server, run_acp_server
|
|
36
|
+
|
|
37
|
+
__all__ = [
|
|
38
|
+
"ACPConfig",
|
|
39
|
+
"CLIToolAgent",
|
|
40
|
+
"ToolArgument",
|
|
41
|
+
"ToolDef",
|
|
42
|
+
"build_agents",
|
|
43
|
+
"build_cli_command",
|
|
44
|
+
"create_acp_server",
|
|
45
|
+
"execute_command",
|
|
46
|
+
"run_acp_server",
|
|
47
|
+
"stream_command",
|
|
48
|
+
]
|
sycli/acp/config.py
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""ACP configuration models.
|
|
2
|
+
|
|
3
|
+
Defines the Pydantic models for declaring which CLI tools to expose
|
|
4
|
+
as ACP agents. Configuration is loaded from a YAML or JSON file.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
import yaml
|
|
13
|
+
from pydantic import BaseModel, Field
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ToolArgument(BaseModel):
|
|
17
|
+
"""Definition of a named argument that the CLI tool accepts."""
|
|
18
|
+
|
|
19
|
+
name: str = Field(description="Argument name (used for key=value parsing)")
|
|
20
|
+
description: str = Field(default="", description="Human-readable description")
|
|
21
|
+
required: bool = Field(default=False, description="Whether this argument is required")
|
|
22
|
+
default: str | None = Field(default=None, description="Default value if not provided")
|
|
23
|
+
flag: str | None = Field(
|
|
24
|
+
default=None,
|
|
25
|
+
description="CLI flag (e.g. '--path', '-v'); None means positional",
|
|
26
|
+
)
|
|
27
|
+
separator: str = Field(
|
|
28
|
+
default=" ",
|
|
29
|
+
description="Separator between flag and value: ' ' for '--flag value', '=' for '--flag=value'",
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class ToolDef(BaseModel):
|
|
34
|
+
"""Definition of a single CLI tool to expose as an ACP agent."""
|
|
35
|
+
|
|
36
|
+
name: str = Field(description="ACP agent name")
|
|
37
|
+
command: str = Field(description="The actual CLI command to run")
|
|
38
|
+
description: str = Field(default="", description="Agent description for ACP discovery")
|
|
39
|
+
args: list[str] = Field(default_factory=list, description="Fixed arguments always appended")
|
|
40
|
+
tool_args: list[ToolArgument] = Field(
|
|
41
|
+
default_factory=list, description="Dynamic arguments mapped from ACP message input"
|
|
42
|
+
)
|
|
43
|
+
timeout: int = Field(default=120, description="Execution timeout in seconds")
|
|
44
|
+
working_dir: str | None = Field(default=None, description="Override working directory")
|
|
45
|
+
env: dict[str, str] = Field(default_factory=dict, description="Extra environment variables")
|
|
46
|
+
stream_output: bool = Field(default=True, description="Stream stdout line-by-line")
|
|
47
|
+
input_mode: str = Field(
|
|
48
|
+
default="args",
|
|
49
|
+
description="How to pass input: 'args' (parse into CLI args) | 'stdin' (pipe to stdin)",
|
|
50
|
+
)
|
|
51
|
+
output_content_type: str = Field(
|
|
52
|
+
default="text/plain", description="ACP content type for output"
|
|
53
|
+
)
|
|
54
|
+
allowed_exit_codes: list[int] = Field(
|
|
55
|
+
default_factory=lambda: [0], description="Exit codes considered success"
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class ACPConfig(BaseModel):
|
|
60
|
+
"""Root ACP server configuration."""
|
|
61
|
+
|
|
62
|
+
host: str = Field(default="127.0.0.1", description="Server bind host")
|
|
63
|
+
port: int = Field(default=8100, description="Server bind port")
|
|
64
|
+
tools: list[ToolDef] = Field(default_factory=list, description="Registered CLI tools")
|
|
65
|
+
global_timeout: int = Field(default=300, description="Global maximum execution timeout")
|
|
66
|
+
max_output_bytes: int = Field(
|
|
67
|
+
default=100_000, description="Maximum output size in bytes (matches sandbox.py)"
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
@classmethod
|
|
71
|
+
def load(cls, path: Path) -> ACPConfig:
|
|
72
|
+
"""Load ACP configuration from a YAML or JSON file.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
path: Path to the configuration file.
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
Parsed ACPConfig instance.
|
|
79
|
+
|
|
80
|
+
Raises:
|
|
81
|
+
FileNotFoundError: If the config file doesn't exist.
|
|
82
|
+
ValueError: If the file cannot be parsed.
|
|
83
|
+
"""
|
|
84
|
+
if not path.exists():
|
|
85
|
+
raise FileNotFoundError(f"ACP config file not found: {path}")
|
|
86
|
+
|
|
87
|
+
text = path.read_text(encoding="utf-8")
|
|
88
|
+
|
|
89
|
+
if path.suffix in (".yaml", ".yml"):
|
|
90
|
+
try:
|
|
91
|
+
data = yaml.safe_load(text)
|
|
92
|
+
except yaml.YAMLError as e:
|
|
93
|
+
raise ValueError(f"Failed to parse YAML config: {e}") from e
|
|
94
|
+
elif path.suffix == ".json":
|
|
95
|
+
try:
|
|
96
|
+
data = json.loads(text)
|
|
97
|
+
except json.JSONDecodeError as e:
|
|
98
|
+
raise ValueError(f"Failed to parse JSON config: {e}") from e
|
|
99
|
+
else:
|
|
100
|
+
# Try YAML first, then JSON
|
|
101
|
+
try:
|
|
102
|
+
data = yaml.safe_load(text)
|
|
103
|
+
except yaml.YAMLError:
|
|
104
|
+
try:
|
|
105
|
+
data = json.loads(text)
|
|
106
|
+
except json.JSONDecodeError as e:
|
|
107
|
+
raise ValueError(f"Failed to parse config file: {e}") from e
|
|
108
|
+
|
|
109
|
+
if not isinstance(data, dict):
|
|
110
|
+
raise ValueError("Config file must contain a mapping (dict)")
|
|
111
|
+
|
|
112
|
+
return cls.model_validate(data)
|
sycli/acp/executor.py
ADDED
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
"""Async subprocess executor for ACP tool agents.
|
|
2
|
+
|
|
3
|
+
Provides command execution with process isolation, timeout handling,
|
|
4
|
+
output truncation, and line-by-line streaming — reusing patterns from
|
|
5
|
+
sycommon's sandbox service.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import asyncio
|
|
11
|
+
import os
|
|
12
|
+
import signal
|
|
13
|
+
import sys
|
|
14
|
+
from collections.abc import AsyncGenerator
|
|
15
|
+
from typing import TYPE_CHECKING
|
|
16
|
+
|
|
17
|
+
from pydantic import BaseModel
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
from sycli.acp.config import ToolDef
|
|
21
|
+
|
|
22
|
+
# Process group isolation on Unix
|
|
23
|
+
_SUBPROCESS_NEW_SESSION = sys.platform != "win32"
|
|
24
|
+
|
|
25
|
+
# Maximum output size per stream (matches sandbox.py)
|
|
26
|
+
MAX_OUTPUT_BYTES = 100_000
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class ExecutionResult(BaseModel):
|
|
30
|
+
"""Result of a CLI command execution."""
|
|
31
|
+
|
|
32
|
+
stdout: str = ""
|
|
33
|
+
stderr: str = ""
|
|
34
|
+
exit_code: int = 0
|
|
35
|
+
truncated: bool = False
|
|
36
|
+
timed_out: bool = False
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _kill_process_tree(process: asyncio.subprocess.Process) -> None:
|
|
40
|
+
"""Kill a process and its entire process group.
|
|
41
|
+
|
|
42
|
+
Mirrors sandbox.py's _kill_process_tree pattern.
|
|
43
|
+
"""
|
|
44
|
+
try:
|
|
45
|
+
if sys.platform == "win32":
|
|
46
|
+
process.kill()
|
|
47
|
+
else:
|
|
48
|
+
pgid = os.getpgid(process.pid)
|
|
49
|
+
os.killpg(pgid, signal.SIGKILL)
|
|
50
|
+
except (ProcessLookupError, PermissionError, OSError):
|
|
51
|
+
try:
|
|
52
|
+
process.kill()
|
|
53
|
+
except ProcessLookupError:
|
|
54
|
+
pass
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _truncate_output(output: str, max_bytes: int = MAX_OUTPUT_BYTES) -> tuple[str, bool]:
|
|
58
|
+
"""Truncate output to max_bytes, returning (truncated_output, was_truncated)."""
|
|
59
|
+
encoded = output.encode("utf-8", errors="replace")
|
|
60
|
+
if len(encoded) <= max_bytes:
|
|
61
|
+
return output, False
|
|
62
|
+
truncated = encoded[:max_bytes].decode("utf-8", errors="replace")
|
|
63
|
+
return truncated + f"\n... (truncated, {len(encoded)} bytes total)", True
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
async def execute_command(
|
|
67
|
+
command: str,
|
|
68
|
+
*,
|
|
69
|
+
cwd: str | None = None,
|
|
70
|
+
timeout: int = 120,
|
|
71
|
+
env: dict[str, str] | None = None,
|
|
72
|
+
max_output_bytes: int = MAX_OUTPUT_BYTES,
|
|
73
|
+
stdin_input: str | None = None,
|
|
74
|
+
) -> ExecutionResult:
|
|
75
|
+
"""Execute a CLI command asynchronously.
|
|
76
|
+
|
|
77
|
+
Mirrors sandbox.py execute endpoint logic:
|
|
78
|
+
- asyncio.create_subprocess_shell with process group isolation
|
|
79
|
+
- Timeout with process tree kill
|
|
80
|
+
- Output truncation at max_output_bytes
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
command: Shell command string to execute.
|
|
84
|
+
cwd: Working directory for the command.
|
|
85
|
+
timeout: Maximum execution time in seconds.
|
|
86
|
+
env: Additional environment variables (merged with os.environ).
|
|
87
|
+
max_output_bytes: Maximum output size before truncation.
|
|
88
|
+
stdin_input: Optional text to pipe to stdin.
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
ExecutionResult with stdout, stderr, exit_code, and status flags.
|
|
92
|
+
"""
|
|
93
|
+
full_env = None
|
|
94
|
+
if env:
|
|
95
|
+
full_env = {**os.environ, **env}
|
|
96
|
+
|
|
97
|
+
stdin_data = stdin_input.encode("utf-8") if stdin_input else None
|
|
98
|
+
|
|
99
|
+
process = await asyncio.create_subprocess_shell(
|
|
100
|
+
command,
|
|
101
|
+
stdout=asyncio.subprocess.PIPE,
|
|
102
|
+
stderr=asyncio.subprocess.PIPE,
|
|
103
|
+
stdin=asyncio.subprocess.PIPE if stdin_data else asyncio.subprocess.DEVNULL,
|
|
104
|
+
start_new_session=_SUBPROCESS_NEW_SESSION,
|
|
105
|
+
cwd=cwd,
|
|
106
|
+
env=full_env,
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
timed_out = False
|
|
110
|
+
try:
|
|
111
|
+
stdout_bytes, stderr_bytes = await asyncio.wait_for(
|
|
112
|
+
process.communicate(input=stdin_data),
|
|
113
|
+
timeout=timeout,
|
|
114
|
+
)
|
|
115
|
+
except asyncio.TimeoutError:
|
|
116
|
+
_kill_process_tree(process)
|
|
117
|
+
try:
|
|
118
|
+
await process.wait()
|
|
119
|
+
except Exception:
|
|
120
|
+
pass
|
|
121
|
+
timed_out = True
|
|
122
|
+
stdout_bytes = b""
|
|
123
|
+
stderr_bytes = b""
|
|
124
|
+
|
|
125
|
+
stdout_raw = stdout_bytes.decode("utf-8", errors="replace") if stdout_bytes else ""
|
|
126
|
+
stderr_raw = stderr_bytes.decode("utf-8", errors="replace") if stderr_bytes else ""
|
|
127
|
+
|
|
128
|
+
stdout, stdout_truncated = _truncate_output(stdout_raw, max_output_bytes)
|
|
129
|
+
stderr, stderr_truncated = _truncate_output(stderr_raw, max_output_bytes)
|
|
130
|
+
|
|
131
|
+
return ExecutionResult(
|
|
132
|
+
stdout=stdout,
|
|
133
|
+
stderr=stderr,
|
|
134
|
+
exit_code=process.returncode if process.returncode is not None else -1,
|
|
135
|
+
truncated=stdout_truncated or stderr_truncated,
|
|
136
|
+
timed_out=timed_out,
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
async def stream_command(
|
|
141
|
+
command: str,
|
|
142
|
+
*,
|
|
143
|
+
cwd: str | None = None,
|
|
144
|
+
timeout: int = 120,
|
|
145
|
+
env: dict[str, str] | None = None,
|
|
146
|
+
max_output_bytes: int = MAX_OUTPUT_BYTES,
|
|
147
|
+
) -> AsyncGenerator[str, None]:
|
|
148
|
+
"""Execute a CLI command and yield stdout lines as they arrive.
|
|
149
|
+
|
|
150
|
+
For ACP stream mode: each yielded line becomes a MessagePart.
|
|
151
|
+
Uses asyncio.create_subprocess_shell with stdout=PIPE and
|
|
152
|
+
iterates process.stdout line by line.
|
|
153
|
+
|
|
154
|
+
Args:
|
|
155
|
+
command: Shell command string to execute.
|
|
156
|
+
cwd: Working directory for the command.
|
|
157
|
+
timeout: Maximum execution time in seconds.
|
|
158
|
+
env: Additional environment variables.
|
|
159
|
+
max_output_bytes: Maximum total output before stopping.
|
|
160
|
+
|
|
161
|
+
Yields:
|
|
162
|
+
Each line from stdout as a string.
|
|
163
|
+
"""
|
|
164
|
+
full_env = None
|
|
165
|
+
if env:
|
|
166
|
+
full_env = {**os.environ, **env}
|
|
167
|
+
|
|
168
|
+
process = await asyncio.create_subprocess_shell(
|
|
169
|
+
command,
|
|
170
|
+
stdout=asyncio.subprocess.PIPE,
|
|
171
|
+
stderr=asyncio.subprocess.PIPE,
|
|
172
|
+
start_new_session=_SUBPROCESS_NEW_SESSION,
|
|
173
|
+
cwd=cwd,
|
|
174
|
+
env=full_env,
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
total_bytes = 0
|
|
178
|
+
try:
|
|
179
|
+
try:
|
|
180
|
+
async with asyncio.timeout(timeout):
|
|
181
|
+
if process.stdout is not None:
|
|
182
|
+
async for raw_line in process.stdout:
|
|
183
|
+
line = raw_line.decode("utf-8", errors="replace")
|
|
184
|
+
total_bytes += len(raw_line)
|
|
185
|
+
if total_bytes > max_output_bytes:
|
|
186
|
+
yield "... (output truncated)"
|
|
187
|
+
break
|
|
188
|
+
yield line
|
|
189
|
+
except asyncio.TimeoutError:
|
|
190
|
+
_kill_process_tree(process)
|
|
191
|
+
yield f"\n... (timed out after {timeout}s)"
|
|
192
|
+
|
|
193
|
+
# Wait for process to finish
|
|
194
|
+
try:
|
|
195
|
+
await asyncio.wait_for(process.wait(), timeout=5)
|
|
196
|
+
except asyncio.TimeoutError:
|
|
197
|
+
_kill_process_tree(process)
|
|
198
|
+
finally:
|
|
199
|
+
try:
|
|
200
|
+
if process.returncode is None:
|
|
201
|
+
_kill_process_tree(process)
|
|
202
|
+
try:
|
|
203
|
+
await process.wait()
|
|
204
|
+
except Exception:
|
|
205
|
+
pass
|
|
206
|
+
except Exception:
|
|
207
|
+
pass
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def build_cli_command(tool_def: ToolDef, input_text: str) -> str:
|
|
211
|
+
"""Build the full CLI command string from a ToolDef and user input.
|
|
212
|
+
|
|
213
|
+
Parses the input message text to extract tool_args values.
|
|
214
|
+
|
|
215
|
+
For input_mode="args":
|
|
216
|
+
Parses key=value pairs or positional args from message text.
|
|
217
|
+
Example: "target=tests/test_api.py verbose=true"
|
|
218
|
+
-> "pytest tests/test_api.py -v"
|
|
219
|
+
|
|
220
|
+
For input_mode="stdin":
|
|
221
|
+
Returns the command as-is (input will be piped to stdin).
|
|
222
|
+
|
|
223
|
+
Args:
|
|
224
|
+
tool_def: Tool configuration.
|
|
225
|
+
input_text: Raw text extracted from ACP input messages.
|
|
226
|
+
|
|
227
|
+
Returns:
|
|
228
|
+
Complete shell command string ready for execution.
|
|
229
|
+
"""
|
|
230
|
+
parts = [tool_def.command]
|
|
231
|
+
|
|
232
|
+
# Add fixed arguments
|
|
233
|
+
if tool_def.args:
|
|
234
|
+
parts.extend(tool_def.args)
|
|
235
|
+
|
|
236
|
+
if tool_def.input_mode == "stdin":
|
|
237
|
+
# Input will be piped separately; no arg mapping needed
|
|
238
|
+
return " ".join(parts)
|
|
239
|
+
|
|
240
|
+
# Parse input text into key-value pairs
|
|
241
|
+
parsed_args = _parse_input_text(input_text)
|
|
242
|
+
|
|
243
|
+
# Map tool_args to CLI flags/positionals
|
|
244
|
+
for arg_def in tool_def.tool_args:
|
|
245
|
+
value = parsed_args.get(arg_def.name)
|
|
246
|
+
|
|
247
|
+
if value is None:
|
|
248
|
+
if arg_def.required and arg_def.default is None:
|
|
249
|
+
# Required arg missing — skip gracefully, let the CLI tool report the error
|
|
250
|
+
continue
|
|
251
|
+
if arg_def.default is not None:
|
|
252
|
+
value = arg_def.default
|
|
253
|
+
else:
|
|
254
|
+
continue
|
|
255
|
+
|
|
256
|
+
if arg_def.flag is not None:
|
|
257
|
+
if _is_truthy(value):
|
|
258
|
+
# Boolean flag present — only emit the flag itself (e.g. -v, --verbose)
|
|
259
|
+
parts.append(arg_def.flag)
|
|
260
|
+
else:
|
|
261
|
+
# Value flag (e.g. --path /some/dir, -e PATTERN)
|
|
262
|
+
parts.append(f"{arg_def.flag}{arg_def.separator}{value}")
|
|
263
|
+
else:
|
|
264
|
+
# Positional argument
|
|
265
|
+
parts.append(value)
|
|
266
|
+
|
|
267
|
+
# Any remaining unparsed input text becomes trailing positional args
|
|
268
|
+
remaining = _get_remaining_input(parsed_args, tool_def.tool_args)
|
|
269
|
+
if remaining:
|
|
270
|
+
parts.append(remaining)
|
|
271
|
+
|
|
272
|
+
return " ".join(parts)
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def _parse_input_text(text: str) -> dict[str, str]:
|
|
276
|
+
"""Parse input text into key=value pairs.
|
|
277
|
+
|
|
278
|
+
Supports:
|
|
279
|
+
- "key=value" pairs (space or newline separated)
|
|
280
|
+
- Bare values are stored under the key "__positional__"
|
|
281
|
+
|
|
282
|
+
Args:
|
|
283
|
+
text: Raw input text from ACP message.
|
|
284
|
+
|
|
285
|
+
Returns:
|
|
286
|
+
Dict mapping argument names to values.
|
|
287
|
+
"""
|
|
288
|
+
result: dict[str, str] = {}
|
|
289
|
+
positional: list[str] = []
|
|
290
|
+
|
|
291
|
+
# Split on whitespace (spaces, newlines, tabs)
|
|
292
|
+
tokens = text.strip().split()
|
|
293
|
+
|
|
294
|
+
for token in tokens:
|
|
295
|
+
if "=" in token:
|
|
296
|
+
key, _, val = token.partition("=")
|
|
297
|
+
key = key.strip()
|
|
298
|
+
val = val.strip()
|
|
299
|
+
if key:
|
|
300
|
+
result[key] = val
|
|
301
|
+
else:
|
|
302
|
+
positional.append(token)
|
|
303
|
+
|
|
304
|
+
if positional:
|
|
305
|
+
result["__positional__"] = " ".join(positional)
|
|
306
|
+
|
|
307
|
+
return result
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def _is_truthy(value: str) -> bool:
|
|
311
|
+
"""Check if a string value represents a boolean 'true' (flag present, no value needed)."""
|
|
312
|
+
return value.lower() in ("true", "yes", "1", "")
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def _get_remaining_input(
|
|
316
|
+
parsed: dict[str, str], tool_args: list
|
|
317
|
+
) -> str:
|
|
318
|
+
"""Get remaining input that wasn't matched to any tool_arg definition."""
|
|
319
|
+
known_keys = {arg.name for arg in tool_args}
|
|
320
|
+
known_keys.add("__positional__")
|
|
321
|
+
|
|
322
|
+
remaining = []
|
|
323
|
+
for key, value in parsed.items():
|
|
324
|
+
if key not in known_keys:
|
|
325
|
+
remaining.append(f"{key}={value}" if "=" in f"{key}={value}" else value)
|
|
326
|
+
|
|
327
|
+
# Also include positional text
|
|
328
|
+
positional = parsed.get("__positional__")
|
|
329
|
+
if positional:
|
|
330
|
+
remaining.insert(0, positional)
|
|
331
|
+
|
|
332
|
+
return " ".join(remaining)
|
sycli/acp/registry.py
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"""Dynamic ACP agent registry.
|
|
2
|
+
|
|
3
|
+
Creates CLIToolAgent (AgentManifest subclass) instances at runtime,
|
|
4
|
+
one per ToolDef in the configuration. Each agent wraps a CLI tool
|
|
5
|
+
and executes it via the executor module when a run is requested.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
from collections.abc import AsyncGenerator
|
|
12
|
+
from typing import TYPE_CHECKING
|
|
13
|
+
|
|
14
|
+
from acp_sdk.models import Message, MessagePart, Metadata
|
|
15
|
+
from acp_sdk.server.agent import AgentManifest
|
|
16
|
+
from acp_sdk.server.context import Context
|
|
17
|
+
from acp_sdk.server.types import RunYield, RunYieldResume
|
|
18
|
+
|
|
19
|
+
from sycli.acp.executor import build_cli_command, execute_command, stream_command
|
|
20
|
+
|
|
21
|
+
if TYPE_CHECKING:
|
|
22
|
+
from sycli.acp.config import ACPConfig, ToolDef
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class CLIToolAgent(AgentManifest):
|
|
26
|
+
"""An AgentManifest that wraps a CLI tool.
|
|
27
|
+
|
|
28
|
+
Each instance is configured with a ToolDef. The run() method
|
|
29
|
+
is an async generator, enabling both streaming and batch modes
|
|
30
|
+
via the ACP SDK executor.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
def __init__(self, tool_def: ToolDef, project_root: str) -> None:
|
|
34
|
+
self._tool_def = tool_def
|
|
35
|
+
self._project_root = project_root
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def name(self) -> str:
|
|
39
|
+
return self._tool_def.name
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def description(self) -> str:
|
|
43
|
+
return self._tool_def.description or f"CLI tool: {self._tool_def.command}"
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def input_content_types(self) -> list[str]:
|
|
47
|
+
return ["text/plain"]
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def output_content_types(self) -> list[str]:
|
|
51
|
+
return [self._tool_def.output_content_type]
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def metadata(self) -> Metadata:
|
|
55
|
+
return Metadata(
|
|
56
|
+
tags=["cli-tool", self._tool_def.command],
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
async def run(
|
|
60
|
+
self, input: list[Message], context: Context
|
|
61
|
+
) -> AsyncGenerator[RunYield, RunYieldResume]:
|
|
62
|
+
"""Execute the CLI tool and yield results.
|
|
63
|
+
|
|
64
|
+
Async generator enabling:
|
|
65
|
+
- Stream mode: yields MessagePart per stdout line
|
|
66
|
+
- Sync/async mode: yields complete Message at the end
|
|
67
|
+
|
|
68
|
+
Flow:
|
|
69
|
+
1. Extract text from input Messages (concatenate text/plain parts)
|
|
70
|
+
2. Build CLI command via build_cli_command()
|
|
71
|
+
3. If stream_output=True: stream_command(), yield MessagePart per line
|
|
72
|
+
4. Otherwise: execute_command(), yield single Message
|
|
73
|
+
"""
|
|
74
|
+
# Step 1: Extract input text
|
|
75
|
+
input_text = _extract_input_text(input)
|
|
76
|
+
|
|
77
|
+
# Step 2: Build command
|
|
78
|
+
command = build_cli_command(self._tool_def, input_text)
|
|
79
|
+
|
|
80
|
+
# Step 3: Determine working directory
|
|
81
|
+
cwd = self._tool_def.working_dir or self._project_root
|
|
82
|
+
|
|
83
|
+
# Step 4: Build environment
|
|
84
|
+
env = None
|
|
85
|
+
if self._tool_def.env:
|
|
86
|
+
env = {**os.environ, **self._tool_def.env}
|
|
87
|
+
|
|
88
|
+
if self._tool_def.stream_output:
|
|
89
|
+
# Streaming: yield MessagePart per stdout line
|
|
90
|
+
async for line in stream_command(
|
|
91
|
+
command,
|
|
92
|
+
cwd=cwd,
|
|
93
|
+
timeout=self._tool_def.timeout,
|
|
94
|
+
env=env,
|
|
95
|
+
):
|
|
96
|
+
yield MessagePart(
|
|
97
|
+
content=line,
|
|
98
|
+
content_type="text/plain",
|
|
99
|
+
)
|
|
100
|
+
else:
|
|
101
|
+
# Batch: execute and yield complete output
|
|
102
|
+
result = await execute_command(
|
|
103
|
+
command,
|
|
104
|
+
cwd=cwd,
|
|
105
|
+
timeout=self._tool_def.timeout,
|
|
106
|
+
env=env,
|
|
107
|
+
max_output_bytes=100_000,
|
|
108
|
+
stdin_input=input_text if self._tool_def.input_mode == "stdin" else None,
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
if result.timed_out:
|
|
112
|
+
output = f"Error: Command timed out after {self._tool_def.timeout}s\n{result.stdout}"
|
|
113
|
+
elif result.exit_code not in self._tool_def.allowed_exit_codes:
|
|
114
|
+
parts = []
|
|
115
|
+
if result.stdout:
|
|
116
|
+
parts.append(result.stdout)
|
|
117
|
+
if result.stderr:
|
|
118
|
+
parts.append(f"[stderr]\n{result.stderr}")
|
|
119
|
+
parts.append(f"Exit code: {result.exit_code}")
|
|
120
|
+
output = "\n".join(parts)
|
|
121
|
+
else:
|
|
122
|
+
output = result.stdout or "(no output)"
|
|
123
|
+
|
|
124
|
+
yield Message(
|
|
125
|
+
role="agent",
|
|
126
|
+
parts=[MessagePart(content=output, content_type="text/plain")],
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _extract_input_text(messages: list[Message]) -> str:
|
|
131
|
+
"""Extract and concatenate all text/plain content from input messages."""
|
|
132
|
+
parts: list[str] = []
|
|
133
|
+
for msg in messages:
|
|
134
|
+
for part in msg.parts:
|
|
135
|
+
content_type = getattr(part, "content_type", None) or "text/plain"
|
|
136
|
+
content = getattr(part, "content", None)
|
|
137
|
+
if content_type in ("text/plain", None) and content:
|
|
138
|
+
parts.append(content)
|
|
139
|
+
return "\n".join(parts)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def build_agents(config: ACPConfig, project_root: str) -> list[AgentManifest]:
|
|
143
|
+
"""Create AgentManifest instances for all tools in config.
|
|
144
|
+
|
|
145
|
+
Args:
|
|
146
|
+
config: ACP configuration with tool definitions.
|
|
147
|
+
project_root: Project root directory for command execution.
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
List of CLIToolAgent instances ready for Server.register().
|
|
151
|
+
"""
|
|
152
|
+
return [
|
|
153
|
+
CLIToolAgent(tool_def=tool_def, project_root=project_root)
|
|
154
|
+
for tool_def in config.tools
|
|
155
|
+
]
|
sycli/acp/server.py
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""ACP server bootstrap for sycli.
|
|
2
|
+
|
|
3
|
+
Creates and starts an ACP server with all CLI tool agents registered.
|
|
4
|
+
Uses the acp_sdk FastAPI app directly with uvicorn to avoid version
|
|
5
|
+
compatibility issues between acp-sdk and uvicorn's positional parameters.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import TYPE_CHECKING
|
|
11
|
+
|
|
12
|
+
from acp_sdk.server.app import create_app
|
|
13
|
+
import uvicorn
|
|
14
|
+
|
|
15
|
+
from sycli.acp.registry import build_agents
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from acp_sdk.server import Server
|
|
19
|
+
from sycli.acp.config import ACPConfig
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def create_acp_app(config: ACPConfig, project_root: str):
|
|
23
|
+
"""Create a FastAPI app with all registered tool agents.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
config: ACP configuration (loaded from .sycli/acp.yaml).
|
|
27
|
+
project_root: Project root directory for command execution.
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
FastAPI application instance with ACP routes.
|
|
31
|
+
"""
|
|
32
|
+
agents = build_agents(config, project_root)
|
|
33
|
+
app = create_app(*agents)
|
|
34
|
+
return app
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def create_acp_server(config: ACPConfig, project_root: str) -> Server:
|
|
38
|
+
"""Create and configure an ACP Server with all registered tool agents.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
config: ACP configuration (loaded from .sycli/acp.yaml).
|
|
42
|
+
project_root: Project root directory for command execution.
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
Configured Server instance (not yet started).
|
|
46
|
+
"""
|
|
47
|
+
from acp_sdk.server import Server
|
|
48
|
+
|
|
49
|
+
server = Server()
|
|
50
|
+
agents = build_agents(config, project_root)
|
|
51
|
+
server.register(*agents)
|
|
52
|
+
return server
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def run_acp_server(config: ACPConfig, project_root: str) -> None:
|
|
56
|
+
"""Create and start the ACP server (blocking until shutdown).
|
|
57
|
+
|
|
58
|
+
This is the main entry point called from acp_cmd.py.
|
|
59
|
+
Uses uvicorn directly to avoid acp-sdk / uvicorn version mismatches.
|
|
60
|
+
"""
|
|
61
|
+
app = create_acp_app(config, project_root)
|
|
62
|
+
|
|
63
|
+
uvicorn.run(
|
|
64
|
+
app,
|
|
65
|
+
host=config.host,
|
|
66
|
+
port=config.port,
|
|
67
|
+
log_level="info",
|
|
68
|
+
headers=[("server", "acp")],
|
|
69
|
+
)
|
sycli/cli.py
CHANGED
|
@@ -168,6 +168,13 @@ def _create_parser() -> argparse.ArgumentParser:
|
|
|
168
168
|
# interactive
|
|
169
169
|
cdp_sub.add_parser("interactive", help="Start interactive CDP REPL")
|
|
170
170
|
|
|
171
|
+
# ── acp ──
|
|
172
|
+
p_acp = sub.add_parser("acp", help="Start ACP server exposing CLI tools as agents")
|
|
173
|
+
p_acp.add_argument("--config", "-c", help="Path to ACP config file (default: .sycli/acp.yaml)")
|
|
174
|
+
p_acp.add_argument("--host", help="Override server host")
|
|
175
|
+
p_acp.add_argument("--port", "-p", type=int, help="Override server port")
|
|
176
|
+
p_acp.add_argument("--project-root", "-r", default=".", help="Project root directory")
|
|
177
|
+
|
|
171
178
|
# ── version ──
|
|
172
179
|
sub.add_parser("version", help="Show version")
|
|
173
180
|
|
|
@@ -234,6 +241,10 @@ def main() -> None:
|
|
|
234
241
|
from sycli.commands.cdp_cmd import handle_cdp
|
|
235
242
|
sys.exit(handle_cdp(args))
|
|
236
243
|
|
|
244
|
+
elif args.command == "acp":
|
|
245
|
+
from sycli.commands.acp_cmd import handle_acp
|
|
246
|
+
sys.exit(handle_acp(args))
|
|
247
|
+
|
|
237
248
|
|
|
238
249
|
def _handle_status(args) -> None:
|
|
239
250
|
"""Show current RL state."""
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""sycli acp command — start ACP server exposing CLI tools as agents."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import signal
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from sycli.core.display import error, header, info, success, warning
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def handle_acp(args) -> int:
|
|
13
|
+
"""Handle `sycli acp` command.
|
|
14
|
+
|
|
15
|
+
Flow:
|
|
16
|
+
1. Resolve project root
|
|
17
|
+
2. Load .sycli/acp.yaml (or --config path)
|
|
18
|
+
3. Validate tool definitions
|
|
19
|
+
4. Print registered agents summary
|
|
20
|
+
5. Start ACP server (blocking)
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
args: Parsed argparse namespace with config, host, port, project_root.
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
Exit code (0 for success, 1 for error).
|
|
27
|
+
"""
|
|
28
|
+
project_root = Path(args.project_root or ".").resolve()
|
|
29
|
+
sycli_dir = project_root / ".sycli"
|
|
30
|
+
|
|
31
|
+
if not sycli_dir.exists():
|
|
32
|
+
error(".sycli/ not found. Run `sycli init` first.")
|
|
33
|
+
return 1
|
|
34
|
+
|
|
35
|
+
# Determine config path
|
|
36
|
+
config_path = Path(args.config) if args.config else sycli_dir / "acp.yaml"
|
|
37
|
+
if not config_path.exists():
|
|
38
|
+
config_path = sycli_dir / "acp.json"
|
|
39
|
+
|
|
40
|
+
if not config_path.exists():
|
|
41
|
+
error(f"ACP config not found at {sycli_dir / 'acp.yaml'}")
|
|
42
|
+
error("Create one with your tool definitions. Example:")
|
|
43
|
+
error("""
|
|
44
|
+
# .sycli/acp.yaml
|
|
45
|
+
host: "127.0.0.1"
|
|
46
|
+
port: 8100
|
|
47
|
+
tools:
|
|
48
|
+
- name: "git-status"
|
|
49
|
+
command: "git"
|
|
50
|
+
args: ["status", "--porcelain"]
|
|
51
|
+
description: "Show git working tree status"
|
|
52
|
+
timeout: 30
|
|
53
|
+
""")
|
|
54
|
+
return 1
|
|
55
|
+
|
|
56
|
+
# Load config
|
|
57
|
+
from sycli.acp.config import ACPConfig
|
|
58
|
+
|
|
59
|
+
try:
|
|
60
|
+
config = ACPConfig.load(config_path)
|
|
61
|
+
except Exception as e:
|
|
62
|
+
error(f"Failed to load ACP config: {e}")
|
|
63
|
+
return 1
|
|
64
|
+
|
|
65
|
+
# Override port/host from CLI args
|
|
66
|
+
if args.host:
|
|
67
|
+
config.host = args.host
|
|
68
|
+
if args.port:
|
|
69
|
+
config.port = args.port
|
|
70
|
+
|
|
71
|
+
if not config.tools:
|
|
72
|
+
error("No tools defined in ACP config. Add tools to acp.yaml.")
|
|
73
|
+
return 1
|
|
74
|
+
|
|
75
|
+
# Print summary
|
|
76
|
+
header("ACP Server Configuration")
|
|
77
|
+
info(f"Config: {config_path}")
|
|
78
|
+
info(f"Host: {config.host}")
|
|
79
|
+
info(f"Port: {config.port}")
|
|
80
|
+
info(f"Agents: {len(config.tools)} registered")
|
|
81
|
+
print()
|
|
82
|
+
for tool in config.tools:
|
|
83
|
+
desc = tool.description or "(no description)"
|
|
84
|
+
info(f" {tool.name}: {tool.command} {' '.join(tool.args) if tool.args else ''}")
|
|
85
|
+
info(f" {desc}")
|
|
86
|
+
print()
|
|
87
|
+
|
|
88
|
+
# Start server
|
|
89
|
+
from sycli.acp.server import run_acp_server
|
|
90
|
+
|
|
91
|
+
# Graceful shutdown on SIGINT
|
|
92
|
+
_shutdown = False
|
|
93
|
+
|
|
94
|
+
def _signal_handler(sig, frame):
|
|
95
|
+
nonlocal _shutdown
|
|
96
|
+
if _shutdown:
|
|
97
|
+
sys.exit(1)
|
|
98
|
+
_shutdown = True
|
|
99
|
+
warning("\nShutting down ACP server...")
|
|
100
|
+
|
|
101
|
+
signal.signal(signal.SIGINT, _signal_handler)
|
|
102
|
+
|
|
103
|
+
try:
|
|
104
|
+
success(f"Starting ACP server on http://{config.host}:{config.port}")
|
|
105
|
+
run_acp_server(config, str(project_root))
|
|
106
|
+
except KeyboardInterrupt:
|
|
107
|
+
warning("ACP server stopped.")
|
|
108
|
+
return 0
|
|
109
|
+
except Exception as e:
|
|
110
|
+
error(f"ACP server failed: {e}")
|
|
111
|
+
import traceback
|
|
112
|
+
|
|
113
|
+
traceback.print_exc()
|
|
114
|
+
return 1
|
|
115
|
+
|
|
116
|
+
return 0
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sycommon-python-lib
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.6a5
|
|
4
4
|
Summary: Add your description here
|
|
5
5
|
Requires-Python: >=3.11
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
7
7
|
Requires-Dist: aio-pika==9.6.2
|
|
8
|
-
Requires-Dist: aiohttp==3.
|
|
8
|
+
Requires-Dist: aiohttp==3.14.1
|
|
9
9
|
Requires-Dist: aiomysql==0.3.2
|
|
10
10
|
Requires-Dist: anyio==4.13.0
|
|
11
11
|
Requires-Dist: decorator==5.3.1
|
|
@@ -13,9 +13,9 @@ Requires-Dist: deepagents==0.6.8
|
|
|
13
13
|
Requires-Dist: elasticsearch==9.4.1
|
|
14
14
|
Requires-Dist: fastapi==0.136.3
|
|
15
15
|
Requires-Dist: jinja2==3.1.6
|
|
16
|
-
Requires-Dist: kafka-python==2.3.
|
|
17
|
-
Requires-Dist: langchain==1.3.
|
|
18
|
-
Requires-Dist: langchain-core==1.4.
|
|
16
|
+
Requires-Dist: kafka-python==2.3.2
|
|
17
|
+
Requires-Dist: langchain==1.3.7
|
|
18
|
+
Requires-Dist: langchain-core==1.4.6
|
|
19
19
|
Requires-Dist: langchain-openai==1.2.2
|
|
20
20
|
Requires-Dist: langfuse==4.7.1
|
|
21
21
|
Requires-Dist: langgraph==1.2.4
|
|
@@ -29,20 +29,21 @@ Requires-Dist: psutil==7.2.2
|
|
|
29
29
|
Requires-Dist: pyxxl==0.4.7
|
|
30
30
|
Requires-Dist: pydantic==2.13.4
|
|
31
31
|
Requires-Dist: python-dotenv==1.2.2
|
|
32
|
-
Requires-Dist: python-multipart==0.0.
|
|
32
|
+
Requires-Dist: python-multipart==0.0.32
|
|
33
33
|
Requires-Dist: pyyaml==6.0.3
|
|
34
34
|
Requires-Dist: redis==7.4.0
|
|
35
|
-
Requires-Dist: sentry-sdk[fastapi]==2.
|
|
35
|
+
Requires-Dist: sentry-sdk[fastapi]==2.62.0
|
|
36
36
|
Requires-Dist: sqlalchemy[asyncio]==2.0.50
|
|
37
|
-
Requires-Dist: starlette[full]==1.
|
|
37
|
+
Requires-Dist: starlette[full]==1.3.0
|
|
38
38
|
Requires-Dist: tiktoken==0.13.0
|
|
39
|
-
Requires-Dist: uvicorn==0.
|
|
39
|
+
Requires-Dist: uvicorn==0.49.0
|
|
40
40
|
Requires-Dist: wecom-aibot-python-sdk==1.0.2
|
|
41
41
|
Requires-Dist: twine==6.2.0
|
|
42
42
|
Requires-Dist: minio==7.2.20
|
|
43
43
|
Requires-Dist: langchain-mcp-adapters==0.2.2
|
|
44
44
|
Requires-Dist: fastapi-mcp==0.4.0
|
|
45
45
|
Requires-Dist: psycopg[binary,pool]==3.3.4
|
|
46
|
+
Requires-Dist: acp-sdk>=1.0.3
|
|
46
47
|
|
|
47
48
|
# sycommon-python-lib
|
|
48
49
|
|
|
@@ -61,7 +61,12 @@ command/templates/web/tools/mq.py.tpl,sha256=FQpV_dXpDzC-b7kWtSLylhqBCLgUN6GyRhr
|
|
|
61
61
|
nexus/__init__.py,sha256=MDZdgwMEFUSPvQ2ovibj7GE-4X4JX6ihNAB6GwVPa9g,1225
|
|
62
62
|
sycli/__init__.py,sha256=mKtD5rrPbg4cF_FrYku5KJrcovLjVdJHvOPW8dit3zY,81
|
|
63
63
|
sycli/__main__.py,sha256=sYL4dyeRvAN2ef71s2_K8plLCzlaVcuYo7_y6VGz9Tg,115
|
|
64
|
-
sycli/cli.py,sha256=
|
|
64
|
+
sycli/cli.py,sha256=nlThDw1yqtnXoI_jJgF4Zc1ipArTTQefrTE3IvZNotI,22208
|
|
65
|
+
sycli/acp/__init__.py,sha256=wDhvqqIUNvCor3K47ymrdIltVdn-oTJ9AaOBgtNQEgY,1331
|
|
66
|
+
sycli/acp/config.py,sha256=1BLgj2Dao7233ZfSYG9NhIVkJGFL46bm2LkBsTQ5XUk,4360
|
|
67
|
+
sycli/acp/executor.py,sha256=D2XOoaSfksdit72AiSWiyekaBOIJLsX1k85aSIaagSI,10247
|
|
68
|
+
sycli/acp/registry.py,sha256=qaPN9RGdu5YWI3axgPV-vSGVlRGxSqCMrYBPJWnHFNo,5281
|
|
69
|
+
sycli/acp/server.py,sha256=guMLa6yzP3eMyzaHOpciwoSw9VYJszrv_D2oS2N4rmo,1955
|
|
65
70
|
sycli/agents/__init__.py,sha256=th-UR3Upcb0xHhjmt_x1b26q80wZGxt_HJMjxyuTV7g,31
|
|
66
71
|
sycli/agents/factory.py,sha256=huSwOSw1hHsl_XJs1YVMo47EcW5SqAK3hivI_aV1glE,8999
|
|
67
72
|
sycli/agents/prompts.py,sha256=YVVm9kEsoSSOWxvoEDa3DiKPDdkH6KtC18R5euqylW8,16466
|
|
@@ -78,6 +83,7 @@ sycli/cdp/capabilities/page_errors.py,sha256=bZlyJHC09hMuYN04X_Z60Nh4Nq2V8v65xxg
|
|
|
78
83
|
sycli/cdp/capabilities/performance.py,sha256=z_OQNnd8n2luhOURY-RpkuLG3JpjpAqkK3yTjZxDS5g,1971
|
|
79
84
|
sycli/cdp/capabilities/screenshot.py,sha256=qCi2niTs0vNMJtu2bFuwAPH7FRQpJPoQTgWNKHQC2VU,1736
|
|
80
85
|
sycli/commands/__init__.py,sha256=ikkj1Ig33tgxEK9Sm9IQp3DwyCe2WzgawSB5vtER9SU,30
|
|
86
|
+
sycli/commands/acp_cmd.py,sha256=XOzczk8IIxHIXNDmN29SALM_Dt7vF1lsSQgkdMCxr94,3138
|
|
81
87
|
sycli/commands/cdp_cmd.py,sha256=7Jziye60Vwqz0jD1sUU9z0Iry7NsMJ8m5MTRsaE4UV0,8790
|
|
82
88
|
sycli/commands/chat_cmd.py,sha256=kdT2WIAIF2zXBiA53iHKMjAH1jK4e4LwQstfflBiSaI,8812
|
|
83
89
|
sycli/commands/create_cmd.py,sha256=VU9ZJs6NPcgi_TKe2YjOi-zaplnVdckwNu8JF8wRw6M,3698
|
|
@@ -284,8 +290,8 @@ sycommon/tools/syemail.py,sha256=BDFhgf7WDOQeTcjxJEQdu0dQhnHFPO_p3eI0-Ni3LhQ,561
|
|
|
284
290
|
sycommon/tools/timing.py,sha256=OiiE7P07lRoMzX9kzb8sZU9cDb0zNnqIlY5pWqHcnkY,2064
|
|
285
291
|
sycommon/xxljob/__init__.py,sha256=7eoBlQxv-B39IfRSCY2bkqdGYs1QRe1umAWd88VMEEM,86
|
|
286
292
|
sycommon/xxljob/xxljob_service.py,sha256=JIEJaGXhqrTLcyxlyynSrsHg9bBnDNzX-D4qIWLRPUE,6815
|
|
287
|
-
sycommon_python_lib-0.2.
|
|
288
|
-
sycommon_python_lib-0.2.
|
|
289
|
-
sycommon_python_lib-0.2.
|
|
290
|
-
sycommon_python_lib-0.2.
|
|
291
|
-
sycommon_python_lib-0.2.
|
|
293
|
+
sycommon_python_lib-0.2.6a5.dist-info/METADATA,sha256=TopwMFzHdAQpiLylalKZk40bf9Oe9mgm2QswKOcIRcs,7943
|
|
294
|
+
sycommon_python_lib-0.2.6a5.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
295
|
+
sycommon_python_lib-0.2.6a5.dist-info/entry_points.txt,sha256=gsR4SssKxDWjRU8ggidzNcdMXDPRSKRS7UaGyNP84Qg,92
|
|
296
|
+
sycommon_python_lib-0.2.6a5.dist-info/top_level.txt,sha256=RgphKrg7nJyZ7irJqbxFr-5H2LUYTvI7ivoWZH2hcD0,29
|
|
297
|
+
sycommon_python_lib-0.2.6a5.dist-info/RECORD,,
|
|
File without changes
|
{sycommon_python_lib-0.2.6a4.dist-info → sycommon_python_lib-0.2.6a5.dist-info}/entry_points.txt
RENAMED
|
File without changes
|
{sycommon_python_lib-0.2.6a4.dist-info → sycommon_python_lib-0.2.6a5.dist-info}/top_level.txt
RENAMED
|
File without changes
|