agent-claw: automated task changes

This commit is contained in:
daniel
2026-05-06 18:55:16 -05:00
parent 38905bb1e9
commit 732b00fb66
8494 changed files with 2018127 additions and 4 deletions

View File

@@ -0,0 +1,41 @@
# Environment variables
.env
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
# Virtual environments
.venv/
venv/
ENV/
env/
# IDE
.vscode/
.idea/
*.swp
*.swo
*~
# OS
.DS_Store
Thumbs.db

View File

@@ -0,0 +1,38 @@
This is a project generated by the AgentCore CLI!
# Layout
The generated application code lives at the agent root directory. At the root, there is a `.gitignore` file, an
`agentcore/` folder which represents the configurations and state associated with this project. Other `agentcore`
commands like `deploy`, `dev`, and `invoke` rely on the configuration stored here.
## Agent Root
The main entrypoint to your app is defined in `main.py`. Using the AgentCore SDK `@app.entrypoint` decorator, this
file defines a Starlette ASGI app with the chosen Agent framework SDK running within.
`model/load.py` instantiates your chosen model provider.
## Environment Variables
| Variable | Required | Description |
| --- | --- | --- |
| `LOCAL_DEV` | No | Set to `1` to use `.env.local` instead of AgentCore Identity |
# Developing locally
If installation was successful, a virtual environment is already created with dependencies installed.
Run `source .venv/bin/activate` before developing.
`agentcore dev` will start a local server on 0.0.0.0:8080.
In a new terminal, you can invoke that server with:
`agentcore invoke --dev "What can you do"`
# Deployment
After providing credentials, `agentcore deploy` will deploy your project into Amazon Bedrock AgentCore.
Use `agentcore invoke` to invoke your deployed agent.

View File

@@ -0,0 +1,4 @@
from .adapter import ChannelAdapter
from .telegram import TelegramAdapter
__all__ = ['ChannelAdapter', 'TelegramAdapter']

View File

@@ -0,0 +1,18 @@
from typing import Protocol, runtime_checkable
@runtime_checkable
class ChannelAdapter(Protocol):
"""Protocol for channel-specific message delivery."""
def send(self, text: str) -> str:
"""Send a message. Returns message_id if available."""
...
def send_typing(self) -> None:
"""Send a typing indicator (best-effort)."""
...
def edit(self, message_id: str, text: str) -> None:
"""Edit an existing message in-place."""
...

View File

@@ -0,0 +1,71 @@
import os
import threading
import urllib.request
import json
import boto3
class TelegramAdapter:
"""Channel adapter for Telegram Bot API."""
def __init__(self, chat_id: str, bot_token_secret_arn: str = ''):
self.chat_id = str(chat_id)
self._secret_arn = bot_token_secret_arn
self._token: str | None = None
self._lock = threading.Lock()
def _get_token(self) -> str:
if self._token is None:
with self._lock:
if self._token is None:
secret_arn = self._secret_arn or os.environ.get(
'TELEGRAM_BOT_TOKEN_SECRET_ARN',
'arn:aws:secretsmanager:us-east-1:495395224548:secret:agent-claw/telegram-bot-token-Oq3in3'
)
sm = boto3.client('secretsmanager')
self._token = sm.get_secret_value(
SecretId=secret_arn
)['SecretString']
return self._token
def _api(self, method: str, data: dict) -> dict:
token = self._get_token()
body = json.dumps(data).encode()
req = urllib.request.Request(
f'https://api.telegram.org/bot{token}/{method}',
data=body,
headers={'Content-Type': 'application/json'},
)
with urllib.request.urlopen(req, timeout=30) as resp:
return json.loads(resp.read())
def send(self, text: str) -> str:
"""Send message, return message_id."""
resp = self._api('sendMessage', {
'chat_id': self.chat_id,
'text': text,
'parse_mode': 'Markdown',
})
return str(resp.get('result', {}).get('message_id', ''))
def send_typing(self) -> None:
"""Send typing action (best-effort)."""
try:
self._api('sendChatAction', {
'chat_id': self.chat_id,
'action': 'typing',
})
except Exception:
pass
def edit(self, message_id: str, text: str) -> None:
"""Edit an existing message in-place."""
try:
self._api('editMessageText', {
'chat_id': self.chat_id,
'message_id': int(message_id),
'text': text,
'parse_mode': 'Markdown',
})
except Exception:
pass

View File

@@ -0,0 +1,201 @@
"""
agent-claw Runtime 1 — main assistant agent.
Entrypoint for AgentCore CodeZip deployment.
"""
import os
from strands import Agent, tool
from strands.models import BedrockModel
from bedrock_agentcore.runtime import BedrockAgentCoreApp
from channels.telegram import TelegramAdapter
from prompt_builder import build_system_prompt, invalidate_prompt
from tools import web as web_tools
from tools import workspace as ws_tools
from tools import messaging
from tools.home_assistant import home_assistant
from mcp.client.streamable_http import streamablehttp_client
from strands.tools.mcp.mcp_client import MCPClient
import httpx
import botocore.auth
import botocore.awsrequest
import boto3
from urllib.parse import urlparse as _urlparse
WORKSPACE_MCP_URL = 'https://25hugrzw4uwtueeg77jsmft6lq0wunmd.lambda-url.us-east-1.on.aws/mcp'
class _SigV4HttpxAuth(httpx.Auth):
"""SigV4 auth for Lambda Function URL with AWS_IAM."""
def __init__(self, region: str = 'us-east-1'):
self._region = region
def auth_flow(self, request):
creds = boto3.Session().get_credentials().get_frozen_credentials()
parsed = _urlparse(str(request.url))
aws_req = botocore.awsrequest.AWSRequest(
method=request.method,
url=str(request.url),
data=request.content or b'',
headers={
'Host': parsed.hostname,
'Content-Type': request.headers.get('content-type', 'application/json'),
'Accept': request.headers.get('accept', 'application/json, text/event-stream'),
}
)
botocore.auth.SigV4Auth(creds, 'lambda', self._region).add_auth(aws_req)
for k, v in aws_req.headers.items():
request.headers[k] = v
yield request
from bedrock_agentcore.memory.integrations.strands.config import AgentCoreMemoryConfig
from bedrock_agentcore.memory.integrations.strands.session_manager import AgentCoreMemorySessionManager
from strands_tools.code_interpreter import AgentCoreCodeInterpreter as _CodeInterpreterClient
# Initialise once per warm session
_code_interpreter = _CodeInterpreterClient(region='us-east-1')
app = BedrockAgentCoreApp()
# ── Tool definitions ──────────────────────────────────────────────────────
@tool
def send_message(text: str) -> str:
"""Send a message to the user through their channel (Telegram, Slack, etc.)"""
return messaging.send(text)
@tool
def web_search(query: str) -> str:
"""Search the web using Brave Search. Returns titles, URLs, and snippets."""
return web_tools.brave_search(query)
@tool
def web_fetch(url: str) -> str:
"""Fetch and extract readable text content from a URL."""
return web_tools.web_fetch(url)
@tool
def read_workspace_file(path: str) -> str:
"""Read a file from the agent workspace (SOUL.md, HEARTBEAT.md, etc.)"""
return ws_tools.read_file(path)
@tool
def write_workspace_file(path: str, content: str) -> str:
"""Write or update a file in the agent workspace."""
result = ws_tools.write_file(path, content)
invalidate_prompt() # force system prompt rebuild if persona files changed
return result
# ── Entrypoint ────────────────────────────────────────────────────────────
@app.entrypoint
def main(payload: dict, context) -> dict:
"""Handle an invocation from agent-runner Lambda."""
# Set up channel adapter
adapter_config = payload.get('channel_adapter', {})
channel_type = adapter_config.get('type', 'telegram')
if channel_type == 'telegram':
adapter = TelegramAdapter(
chat_id=adapter_config.get('target_id', ''),
bot_token_secret_arn=adapter_config.get('bot_token_secret_arn', ''),
)
else:
# Future channels: instantiate appropriate adapter
raise ValueError(f"Unsupported channel type: {channel_type}")
messaging.set_adapter(adapter)
# Start typing indicator immediately, keep it alive in background
import threading
_typing_active = True
def _keep_typing():
adapter.send_typing()
import time
while _typing_active:
time.sleep(4)
if _typing_active:
adapter.send_typing()
typing_thread = threading.Thread(target=_keep_typing, daemon=True)
typing_thread.start()
# Set up AgentCore Memory session manager (short + long term via session_manager)
MEMORY_ID = 'agentclaw_AgentClawMemory-i7Csf776AH'
actor_id = payload.get('actor_id', adapter_config.get('target_id', 'default'))
session_id = payload.get('session_id', f'session-{actor_id}')
memory_config = AgentCoreMemoryConfig(
memory_id=MEMORY_ID,
session_id=session_id,
actor_id=actor_id,
)
session_manager = AgentCoreMemorySessionManager(
agentcore_memory_config=memory_config,
region_name='us-east-1',
)
# Build system prompt (cached across warm invocations)
system_prompt = build_system_prompt()
# Model: claude-sonnet-4-6 via cross-region inference
model = BedrockModel(
model_id="us.anthropic.claude-sonnet-4-6",
region_name="us-east-1",
)
base_tools = [send_message, web_search, web_fetch, read_workspace_file, write_workspace_file,
_code_interpreter.code_interpreter, home_assistant]
def _run_agent(tools):
agent = Agent(
model=model,
system_prompt=system_prompt,
session_manager=session_manager,
tools=tools,
)
return agent(payload.get('prompt', ''))
workspace_mcp_client = MCPClient(
lambda: streamablehttp_client(WORKSPACE_MCP_URL, timeout=20, auth=_SigV4HttpxAuth())
)
workspace_tools = []
try:
with workspace_mcp_client:
workspace_tools = workspace_mcp_client.list_tools_sync()
except Exception as e:
print(f'[main] workspace_mcp unavailable ({type(e).__name__}) — continuing without it')
try:
result = _run_agent(base_tools + list(workspace_tools))
finally:
_typing_active = False
# Flush buffered memory events
session_manager.close()
# Deliver final response — only send if agent didn't already call send_message tool.
# If the tool was used, the response is already delivered. The fallback handles
# cases where the agent responds directly without calling the tool.
if not messaging.was_sent() and result.message:
# Extract plain text from Strands result (avoid sending raw dict/JSON)
msg = result.message
if isinstance(msg, dict):
content = msg.get('content', {})
if isinstance(content, dict):
msg = content.get('text', str(content))
elif isinstance(content, list):
msg = ' '.join(c.get('text', '') for c in content if isinstance(c, dict))
else:
msg = str(content)
adapter.send(str(msg))
return {'result': result.message}
app.run()

View File

@@ -0,0 +1 @@
# Package marker

View File

@@ -0,0 +1,14 @@
import os
import logging
from mcp.client.streamable_http import streamablehttp_client
from strands.tools.mcp.mcp_client import MCPClient
logger = logging.getLogger(__name__)
# ExaAI provides information about code through web searches, crawling and code context searches through their platform. Requires no authentication
EXAMPLE_MCP_ENDPOINT = "https://mcp.exa.ai/mcp"
def get_streamable_http_mcp_client() -> MCPClient:
"""Returns an MCP Client compatible with Strands"""
# to use an MCP server that supports bearer authentication, add headers={"Authorization": f"Bearer {access_token}"}
return MCPClient(lambda: streamablehttp_client(EXAMPLE_MCP_ENDPOINT))

View File

@@ -0,0 +1 @@
# Package marker

View File

@@ -0,0 +1,6 @@
from strands.models.bedrock import BedrockModel
def load_model() -> BedrockModel:
"""Get Bedrock model client using IAM credentials."""
return BedrockModel(model_id="global.anthropic.claude-sonnet-4-5-20250929-v1:0")

View File

@@ -0,0 +1,44 @@
import os
import boto3
# Cache: built once per warm session
_system_prompt: str | None = None
def build_system_prompt() -> str:
"""Build system prompt from S3 workspace files (cached for warm session)."""
global _system_prompt
if _system_prompt is not None:
return _system_prompt
bucket = os.environ.get('WORKSPACE_BUCKET_NAME', '') or 'agent-claw-workspace-495395224548'
print(f'[prompt_builder] Loading from bucket: {bucket!r}')
if not bucket:
print('[prompt_builder] WARNING: WORKSPACE_BUCKET_NAME not set!')
_system_prompt = 'You are a helpful personal assistant.'
return _system_prompt
s3 = boto3.client('s3')
parts = []
for fname in ['SOUL.md', 'AGENTS.md', 'IDENTITY.md', 'USER.md', 'MEMORY.md', 'TOOLS.md']:
try:
obj = s3.get_object(Bucket=bucket, Key=fname)
content = obj['Body'].read().decode('utf-8')
parts.append(f'## {fname}\n{content}')
print(f'[prompt_builder] Loaded {fname} ({len(content)} bytes)')
except Exception as e:
print(f'[prompt_builder] Failed to load {fname}: {e}')
parts.append('## Runtime\nRuntime: agent-claw | host=AgentCore | model=bedrock-claude-sonnet | channel=telegram\nCurrent date/time is provided by the system. Timezone: America/Chicago.')
_system_prompt = '\n\n---\n\n'.join(parts)
print(f'[prompt_builder] System prompt built: {len(_system_prompt)} chars')
return _system_prompt
def invalidate_prompt() -> None:
"""Force rebuild of system prompt on next invocation (call after workspace write)."""
global _system_prompt
_system_prompt = None

View File

@@ -0,0 +1,21 @@
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "agent_claw_main"
version = "0.1.0"
description = "AgentCore Runtime Application using Strands SDK"
readme = "README.md"
requires-python = ">=3.10"
dependencies = [
"aws-opentelemetry-distro",
"bedrock-agentcore >= 1.0.3",
"botocore[crt] >= 1.35.0",
"strands-agents-tools >= 0.5.0",
"strands-agents >= 1.13.0",
]
[tool.hatch.build.targets.wheel]
packages = ["."]

View File

@@ -0,0 +1,5 @@
from .web import brave_search, web_fetch
from .workspace import read_file, write_file
from .messaging import send, set_adapter
__all__ = ['brave_search', 'web_fetch', 'read_file', 'write_file', 'send', 'set_adapter']

View File

@@ -0,0 +1,68 @@
"""Code interpreter tool — runs Python code in AgentCore managed sandbox."""
import os
import base64
from strands import tool
def _parse_stream(result: dict) -> str:
"""Parse the streaming response from invoke_code_interpreter."""
parts = []
if "stream" not in result:
return str(result)
for event in result["stream"]:
if "result" not in event:
continue
for item in event["result"].get("content", []):
item_type = item.get("type", "")
if item_type == "text":
text = item.get("text", "")
if text:
parts.append(text)
elif item_type == "resource":
resource = item.get("resource", {})
if "text" in resource:
parts.append(resource["text"])
elif "blob" in resource:
try:
parts.append(base64.b64decode(resource["blob"]).decode("utf-8"))
except Exception:
parts.append(f"<binary resource: {resource.get('uri', '?')}>")
elif item_type == "image":
# Base64-encoded image
image_data = item.get("source", {}).get("data", "")
mime = item.get("source", {}).get("mediaType", "image/png")
parts.append(f"<image: {mime}, {len(image_data)} bytes base64>")
return "\n".join(parts) if parts else "(no output)"
@tool
def run_code(code: str, packages: list[str] | None = None) -> str:
"""Execute Python code in a secure managed sandbox and return the output.
Optionally install pip packages before running (e.g. ['pandas', 'numpy']).
Args:
code: Python code to execute.
packages: Optional list of pip packages to install first.
Returns:
Execution output (stdout, results, errors).
"""
try:
from bedrock_agentcore.tools import CodeInterpreter, code_session
region = os.environ.get('AWS_REGION', 'us-east-1')
with code_session(region) as client:
if packages:
install_raw = client.install_packages(packages)
install_out = _parse_stream(install_raw) if isinstance(install_raw, dict) else str(install_raw)
print(f'[code_interpreter] install: {install_out[:200]}')
raw = client.execute_code(code)
return _parse_stream(raw)
except Exception as e:
import traceback
return f'Code interpreter error: {type(e).__name__}: {e}\n{traceback.format_exc()[-500:]}'

View File

@@ -0,0 +1,91 @@
"""Home Assistant tool — control and query HA entities via REST API."""
import json
import os
import urllib.request
import urllib.error
from strands import tool
HA_URL = "https://homeassistant.home.everyonce.com"
# Token stored in workspace or env; fallback to hardcoded for AgentCore runtime
HA_TOKEN = os.environ.get(
"HA_TOKEN",
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJlMDExN2YwNzhlM2Q0NjViODJhNjJiZWFiMzI1ZWU4MiIsImlhdCI6MTc3MTM1MjU0MiwiZXhwIjoyMDg2NzEyNTQyfQ.UySLD6JV4e_bdd1nQjdbZcimdCD6B3kBGDftcRz1H6Q"
)
def _ha_request(method: str, path: str, body: dict | None = None) -> dict | list:
url = f"{HA_URL}{path}"
headers = {
"Authorization": f"Bearer {HA_TOKEN}",
"Content-Type": "application/json",
}
data = json.dumps(body).encode() if body else None
req = urllib.request.Request(url, data=data, headers=headers, method=method)
try:
with urllib.request.urlopen(req, timeout=10) as resp:
return json.loads(resp.read().decode())
except urllib.error.HTTPError as e:
return {"error": f"HTTP {e.code}: {e.reason}", "body": e.read().decode()[:500]}
except Exception as e:
return {"error": str(e)}
@tool
def home_assistant(action: str, entity_id: str = "", domain: str = "", service: str = "",
service_data: dict | None = None) -> str:
"""Control and query your Home Assistant smart home.
Actions:
- "get_state": Get the current state of a specific entity (requires entity_id).
- "list_states": List all entity states (optionally filter by domain prefix like 'light', 'switch', 'climate', 'sensor').
- "call_service": Call a HA service (requires domain, service, and optional service_data with entity_id).
- "get_history": Not yet implemented.
Common service examples:
- Turn light on: domain="light", service="turn_on", service_data={"entity_id": "light.living_room"}
- Turn light off: domain="light", service="turn_off", service_data={"entity_id": "light.living_room"}
- Set brightness: domain="light", service="turn_on", service_data={"entity_id": "light.x", "brightness_pct": 50}
- Lock door: domain="lock", service="lock", service_data={"entity_id": "lock.front_door"}
- Set thermostat: domain="climate", service="set_temperature", service_data={"entity_id": "climate.x", "temperature": 72}
Args:
action: One of "get_state", "list_states", "call_service".
entity_id: Entity ID for get_state (e.g. "light.living_room").
domain: Service domain for call_service (e.g. "light", "switch", "lock", "climate").
service: Service name for call_service (e.g. "turn_on", "turn_off", "lock").
service_data: Dict of extra params for call_service (e.g. {"entity_id": "light.x", "brightness_pct": 80}).
Returns:
JSON string with the result.
"""
if action == "get_state":
if not entity_id:
return "entity_id is required for get_state"
result = _ha_request("GET", f"/api/states/{entity_id}")
if isinstance(result, dict) and "error" not in result:
return f"{entity_id}: {result.get('state')} (attrs: {json.dumps(result.get('attributes', {}))[:300]})"
return json.dumps(result)
elif action == "list_states":
result = _ha_request("GET", "/api/states")
if isinstance(result, list):
# Filter by domain prefix if entity_id used as filter
prefix = entity_id or domain
if prefix:
result = [s for s in result if s.get("entity_id", "").startswith(prefix)]
# Return concise summary
lines = [f"{s['entity_id']}: {s['state']}" for s in result[:50]]
return "\n".join(lines) + (f"\n... ({len(result)} total)" if len(result) > 50 else "")
return json.dumps(result)
elif action == "call_service":
if not domain or not service:
return "domain and service are required for call_service"
body = service_data or {}
if entity_id and "entity_id" not in body:
body["entity_id"] = entity_id
result = _ha_request("POST", f"/api/services/{domain}/{service}", body)
return f"Service {domain}.{service} called successfully" if isinstance(result, list) else json.dumps(result)
else:
return f"Unknown action: {action}. Use 'get_state', 'list_states', or 'call_service'."

View File

@@ -0,0 +1,30 @@
"""Messaging tool — channel-adapter-backed send_message for the agent."""
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from channels.adapter import ChannelAdapter
# Injected by main.py before each invocation
_adapter: 'ChannelAdapter | None' = None
_message_sent: bool = False
def set_adapter(adapter: 'ChannelAdapter') -> None:
global _adapter, _message_sent
_adapter = adapter
_message_sent = False # reset on each new invocation
def was_sent() -> bool:
"""Returns True if send() was called during this invocation."""
return _message_sent
def send(text: str) -> str:
"""Send a message to the user via the active channel adapter."""
global _message_sent
if _adapter is None:
return 'No channel adapter configured.'
msg_id = _adapter.send(text)
_message_sent = True
return f"Sent (id={msg_id})" if msg_id else 'Sent'

View File

@@ -0,0 +1,68 @@
import os
import threading
import urllib.request
import urllib.parse
import json
import boto3
# Brave Search API
_brave_key: str | None = None
_brave_lock = threading.Lock()
def _get_brave_key() -> str:
global _brave_key
if _brave_key is None:
with _brave_lock:
if _brave_key is None:
secret_arn = os.environ.get(
'BRAVE_API_KEY_SECRET_ARN',
'arn:aws:secretsmanager:us-east-1:495395224548:secret:agent-claw/brave-api-key-uUSgzi'
)
sm = boto3.client('secretsmanager')
_brave_key = sm.get_secret_value(SecretId=secret_arn)['SecretString']
return _brave_key
def brave_search(query: str, count: int = 5) -> str:
"""Search the web using Brave Search API."""
api_key = _get_brave_key()
params = urllib.parse.urlencode({'q': query, 'count': count})
req = urllib.request.Request(
f'https://api.search.brave.com/res/v1/web/search?{params}',
headers={
'Accept': 'application/json',
'X-Subscription-Token': api_key,
},
)
with urllib.request.urlopen(req, timeout=10) as resp:
data = json.loads(resp.read())
results = data.get('web', {}).get('results', [])
if not results:
return 'No results found.'
parts = []
for r in results:
parts.append(f"**{r.get('title', '')}**\n{r.get('url', '')}\n{r.get('description', '')}")
return '\n\n'.join(parts)
def web_fetch(url: str) -> str:
"""Fetch and return text content from a URL."""
req = urllib.request.Request(
url,
headers={'User-Agent': 'Mozilla/5.0 (compatible; agent-claw/1.0)'},
)
with urllib.request.urlopen(req, timeout=15) as resp:
raw = resp.read(1024 * 1024) # cap at 1MB
# Basic text extraction (strip HTML tags)
import re
text = raw.decode('utf-8', errors='ignore')
text = re.sub(r'<script[^>]*>.*?</script>', '', text, flags=re.DOTALL | re.IGNORECASE)
text = re.sub(r'<style[^>]*>.*?</style>', '', text, flags=re.DOTALL | re.IGNORECASE)
text = re.sub(r'<[^>]+>', ' ', text)
text = re.sub(r'[ \t]+', ' ', text)
text = re.sub(r'\n{3,}', '\n\n', text)
return text[:8000].strip()

View File

@@ -0,0 +1,48 @@
import os
import boto3
# In-memory cache for workspace files (lives for the duration of the warm session)
_cache: dict[str, str] = {}
_s3 = None
def _get_s3():
global _s3
if _s3 is None:
_s3 = boto3.client('s3')
return _s3
def get_bucket() -> str:
return os.environ['WORKSPACE_BUCKET_NAME']
def read_file(path: str) -> str:
"""Read a workspace file from S3 (cached)."""
if path not in _cache:
resp = _get_s3().get_object(Bucket=get_bucket(), Key=path)
_cache[path] = resp['Body'].read().decode('utf-8')
return _cache[path]
def write_file(path: str, content: str) -> str:
"""Write a workspace file to S3 and update cache."""
_get_s3().put_object(
Bucket=get_bucket(),
Key=path,
Body=content.encode('utf-8'),
ContentType='text/markdown',
)
_cache[path] = content
return f"Written {len(content)} bytes to {path}"
def load_persona_files() -> dict[str, str]:
"""Load all persona files at session start (SOUL.md etc.)"""
files = {}
for fname in ['SOUL.md', 'AGENTS.md', 'IDENTITY.md', 'USER.md']:
try:
files[fname] = read_file(fname)
except Exception:
pass
return files

2441
agentclaw/app/agent_claw_main/uv.lock generated Normal file

File diff suppressed because it is too large Load Diff