@scemoon/cdh 1.0.4 → 1.0.5
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.
- package/ai-dlc-skill/SKILL.md +4 -2
- package/ai-dlc-skill/agents/master.md +6 -0
- package/ai-dlc-skill/agents/plan-agent.md +2 -2
- package/ai-dlc-skill/agents/understand-agent.md +4 -4
- package/ai-dlc-skill/agents/verify-agent.md +1 -1
- package/ai-dlc-skill/architecture/project-structure.md +10 -10
- package/ai-dlc-skill/brownfield/README.md +2 -2
- package/ai-dlc-skill/brownfield/generate-context.sh +3 -3
- package/ai-dlc-skill/brownfield/scripts/extract-api.sh +5 -5
- package/ai-dlc-skill/components/backend.md +1 -1
- package/ai-dlc-skill/components/web.md +2 -2
- package/ai-dlc-skill/contracts/README.md +3 -3
- package/ai-dlc-skill/cross-tool/cline/export.sh +1 -1
- package/ai-dlc-skill/cross-tool/copilot/export.sh +1 -1
- package/ai-dlc-skill/cross-tool/onecode/export-skills.sh +7 -3
- package/ai-dlc-skill/cross-tool/onecode/export.sh +14 -8
- package/ai-dlc-skill/phases/deliver/lifecycle.md +6 -6
- package/ai-dlc-skill/phases/deliver/prompt.md +2 -2
- package/ai-dlc-skill/phases/deliver/rules.md +1 -1
- package/ai-dlc-skill/phases/plan/lifecycle.md +11 -11
- package/ai-dlc-skill/phases/plan/prompt.md +6 -6
- package/ai-dlc-skill/phases/plan/rules.md +1 -1
- package/ai-dlc-skill/phases/understand/entry.md +1 -1
- package/ai-dlc-skill/phases/understand/lifecycle.md +7 -7
- package/ai-dlc-skill/phases/understand/prompt.md +8 -8
- package/ai-dlc-skill/phases/understand/rules.md +5 -5
- package/ai-dlc-skill/phases/verify/lifecycle.md +24 -24
- package/ai-dlc-skill/phases/verify/prompt.md +8 -8
- package/ai-dlc-skill/phases/verify/rules.md +9 -9
- package/ai-dlc-skill/providers/README.md +2 -2
- package/ai-dlc-skill/providers/aliyun/preview.yaml +2 -2
- package/ai-dlc-skill/providers/tcb/preview.yaml +2 -2
- package/ai-dlc-skill/requirements.md +7 -7
- package/ai-dlc-skill/skill.yaml +4 -4
- package/ai-dlc-skill/templates/integration/CHANGELOG.md +2 -2
- package/ai-dlc-skill/templates/integration/contract-diff.md +7 -7
- package/ai-dlc-skill/templates/integration/contract-spec.md +7 -7
- package/ai-dlc-skill/templates/integration/runtime-contract.yaml +1 -1
- package/ai-dlc-skill/templates/project/README.md +18 -19
- package/ai-dlc-skill/templates/project/template.md +21 -21
- package/ai-dlc-skill/walkthrough/collect.sh +1 -1
- package/ai-dlc-skill/walkthrough/template.md +2 -2
- package/ai-dlc-skill/workflows/ai-dlc.yaml +12 -12
- package/ai-dlc-skill/workflows/deployment.yaml +4 -4
- package/cdh/__pycache__/cdh_mcp_injector.cpython-314.pyc +0 -0
- package/cdh/__pycache__/cdh_mcp_loader.cpython-314.pyc +0 -0
- package/cdh/__pycache__/cdh_mcp_manager.cpython-314.pyc +0 -0
- package/cdh/__pycache__/cdh_session_aggregator.cpython-314.pyc +0 -0
- package/cdh/__pycache__/cdh_skill_loader.cpython-314.pyc +0 -0
- package/cdh/__pycache__/cdh_skill_manager.cpython-314.pyc +0 -0
- package/cdh/__pycache__/cli.cpython-313.pyc +0 -0
- package/cdh/__pycache__/cli.cpython-314.pyc +0 -0
- package/cdh/__pycache__/scaffold.cpython-314.pyc +0 -0
- package/cdh/cdh_mcp_injector.py +115 -0
- package/cdh/cdh_mcp_loader.py +67 -0
- package/cdh/cdh_mcp_manager.py +73 -0
- package/cdh/cdh_session_aggregator.py +258 -0
- package/cdh/cdh_skill_manager.py +105 -0
- package/cdh/cli.py +14 -3
- package/cdh/scaffold.py +192 -41
- package/onecode/__init__.py +1 -1
- package/onecode/__pycache__/cli.cpython-314.pyc +0 -0
- package/onecode/__pycache__/commands.cpython-314.pyc +0 -0
- package/onecode/__pycache__/config.cpython-314.pyc +0 -0
- package/onecode/__pycache__/config_screen.cpython-314.pyc +0 -0
- package/onecode/__pycache__/migrate.cpython-314.pyc +0 -0
- package/onecode/agent/__pycache__/cdh_loader.cpython-314.pyc +0 -0
- package/onecode/agent/__pycache__/context.cpython-314.pyc +0 -0
- package/onecode/agent/__pycache__/engine.cpython-314.pyc +0 -0
- package/onecode/agent/__pycache__/hooks.cpython-314.pyc +0 -0
- package/onecode/agent/__pycache__/onecode_agent_acp.cpython-314.pyc +0 -0
- package/onecode/agent/__pycache__/permissions.cpython-314.pyc +0 -0
- package/onecode/agent/__pycache__/project_doc.cpython-314.pyc +0 -0
- package/onecode/agent/__pycache__/session.cpython-314.pyc +0 -0
- package/onecode/agent/agents/__pycache__/types.cpython-314.pyc +0 -0
- package/onecode/agent/agents/types.py +47 -164
- package/onecode/agent/attachments.py +0 -1
- package/onecode/agent/cdh_loader.py +17 -25
- package/onecode/agent/context.py +8 -3
- package/onecode/agent/engine.py +835 -220
- package/onecode/agent/hooks.py +1 -1
- package/onecode/agent/onecode_agent_acp.py +460 -47
- package/onecode/agent/onecode_agent_acp.py.bak +2447 -0
- package/onecode/agent/permissions.py +0 -1
- package/onecode/agent/project_doc.py +19 -0
- package/onecode/agent/session.py +17 -3
- package/onecode/agent/snapshot.py +3 -4
- package/onecode/agent/tools/__pycache__/agent_tools.cpython-314.pyc +0 -0
- package/onecode/agent/tools/__pycache__/apply_patch_tool.cpython-314.pyc +0 -0
- package/onecode/agent/tools/__pycache__/bash_tool.cpython-314.pyc +0 -0
- package/onecode/agent/tools/__pycache__/communication_tools.cpython-314.pyc +0 -0
- package/onecode/agent/tools/__pycache__/config_tool.cpython-314.pyc +0 -0
- package/onecode/agent/tools/__pycache__/cron_tools.cpython-314.pyc +0 -0
- package/onecode/agent/tools/__pycache__/file_ops.cpython-314.pyc +0 -0
- package/onecode/agent/tools/__pycache__/file_tools.cpython-314.pyc +0 -0
- package/onecode/agent/tools/__pycache__/git_tools.cpython-314.pyc +0 -0
- package/onecode/agent/tools/__pycache__/lsp_tools.cpython-314.pyc +0 -0
- package/onecode/agent/tools/__pycache__/mcp_tools.cpython-314.pyc +0 -0
- package/onecode/agent/tools/__pycache__/permission_handler.cpython-314.pyc +0 -0
- package/onecode/agent/tools/__pycache__/protocol.cpython-314.pyc +0 -0
- package/onecode/agent/tools/__pycache__/registry.cpython-314.pyc +0 -0
- package/onecode/agent/tools/__pycache__/sandbox.cpython-314.pyc +0 -0
- package/onecode/agent/tools/__pycache__/skill_tools.cpython-314.pyc +0 -0
- package/onecode/agent/tools/__pycache__/todo_tools.cpython-314.pyc +0 -0
- package/onecode/agent/tools/__pycache__/web_tools.cpython-314.pyc +0 -0
- package/onecode/agent/tools/agent_tools.py +4 -6
- package/onecode/agent/tools/apply_patch_tool.py +2 -8
- package/onecode/agent/tools/bash_tool.py +32 -12
- package/onecode/agent/tools/communication_tools.py +4 -5
- package/onecode/agent/tools/config_tool.py +3 -3
- package/onecode/agent/tools/cron_tools.py +4 -5
- package/onecode/agent/tools/file_ops.py +37 -3
- package/onecode/agent/tools/file_tools.py +10 -11
- package/onecode/agent/tools/git_tools.py +2 -2
- package/onecode/agent/tools/lsp_tools.py +2 -2
- package/onecode/agent/tools/mcp_tools.py +3 -5
- package/onecode/agent/tools/permission_handler.py +1 -1
- package/onecode/agent/tools/protocol.py +1 -1
- package/onecode/agent/tools/registry.py +69 -2
- package/onecode/agent/tools/sandbox.py +110 -16
- package/onecode/agent/tools/skill_tools.py +2 -2
- package/onecode/agent/tools/todo_tools.py +38 -19
- package/onecode/agent/tools/web_tools.py +6 -7
- package/onecode/builtin_skills/agent-browser/SKILL.md +212 -0
- package/onecode/builtin_skills/agent-browser/skill.yaml +8 -0
- package/onecode/builtin_skills/skill-creator/SKILL.md +106 -0
- package/onecode/builtin_skills/skill-creator/skill.yaml +8 -0
- package/onecode/cli.py +186 -6
- package/onecode/codebase/__pycache__/chunker.cpython-314.pyc +0 -0
- package/onecode/codebase/__pycache__/indexer.cpython-314.pyc +0 -0
- package/onecode/codebase/__pycache__/retriever.cpython-314.pyc +0 -0
- package/onecode/codebase/__pycache__/storage.cpython-314.pyc +0 -0
- package/onecode/codebase/chunker.py +0 -5
- package/onecode/codebase/indexer.py +14 -1
- package/onecode/codebase/retriever.py +13 -2
- package/onecode/codebase/storage.py +5 -4
- package/onecode/commands.py +81 -6
- package/onecode/config.py +41 -12
- package/onecode/config_screen.py +7 -3
- package/onecode/mcp/__pycache__/manager.cpython-314.pyc +0 -0
- package/onecode/mcp/manager.py +2 -3
- package/onecode/memory/__init__.py +7 -53
- package/onecode/memory/__pycache__/__init__.cpython-314.pyc +0 -0
- package/onecode/memory/__pycache__/backend.cpython-314.pyc +0 -0
- package/onecode/memory/__pycache__/offload.cpython-314.pyc +0 -0
- package/onecode/memory/__pycache__/pyramid.cpython-314.pyc +0 -0
- package/onecode/memory/__pycache__/recall.cpython-314.pyc +0 -0
- package/onecode/memory/backend.py +13 -39
- package/onecode/memory/pyramid.py +2 -71
- package/onecode/memory/recall.py +38 -3
- package/onecode/migrate.py +122 -0
- package/onecode/models/__pycache__/messages.cpython-314.pyc +0 -0
- package/onecode/models/__pycache__/provider.cpython-314.pyc +0 -0
- package/onecode/models/messages.py +58 -0
- package/onecode/models/provider.py +20 -2
- package/onecode/models/providers/__pycache__/anthropic_provider.cpython-314.pyc +0 -0
- package/onecode/models/providers/__pycache__/glm_provider.cpython-314.pyc +0 -0
- package/onecode/models/providers/__pycache__/minimax_provider.cpython-314.pyc +0 -0
- package/onecode/models/providers/__pycache__/minimaxi_provider.cpython-314.pyc +0 -0
- package/onecode/models/providers/__pycache__/ollama_provider.cpython-314.pyc +0 -0
- package/onecode/models/providers/anthropic_provider.py +59 -12
- package/onecode/models/providers/glm_provider.py +40 -0
- package/onecode/models/providers/minimax_provider.py +3 -0
- package/onecode/models/providers/minimaxi_provider.py +3 -0
- package/onecode/models/providers/ollama_provider.py +3 -0
- package/onecode/server/app.py +1 -1
- package/onecode/skills/__pycache__/argument_substitution.cpython-314.pyc +0 -0
- package/onecode/skills/__pycache__/bootstrap.cpython-314.pyc +0 -0
- package/onecode/skills/__pycache__/loader.cpython-314.pyc +0 -0
- package/onecode/skills/__pycache__/manager.cpython-314.pyc +0 -0
- package/onecode/skills/argument_substitution.py +0 -1
- package/onecode/skills/bootstrap.py +124 -0
- package/onecode/skills/loader.py +35 -53
- package/onecode/skills/manager.py +2 -2
- package/onecode/storage/activity.py +3 -4
- package/onecode/storage/session.py +3 -4
- package/onecode/tasks/manager.py +23 -10
- package/onecode/trace/tracer.py +2 -3
- package/package.json +1 -1
- package/pyproject.toml +5 -1
- package/tui/__pycache__/about.cpython-314.pyc +0 -0
- package/tui/__pycache__/app.cpython-313.pyc +0 -0
- package/tui/__pycache__/app.cpython-314.pyc +0 -0
- package/tui/__pycache__/danger.cpython-314.pyc +0 -0
- package/tui/__pycache__/directory_suggester.cpython-314.pyc +0 -0
- package/tui/__pycache__/messages.cpython-314.pyc +0 -0
- package/tui/__pycache__/path_complete.cpython-314.pyc +0 -0
- package/tui/__pycache__/paths.cpython-314.pyc +0 -0
- package/tui/__pycache__/session_tracker.cpython-314.pyc +0 -0
- package/tui/__pycache__/settings_schema.cpython-314.pyc +0 -0
- package/tui/acp/__pycache__/agent.cpython-314.pyc +0 -0
- package/tui/acp/__pycache__/api.cpython-314.pyc +0 -0
- package/tui/acp/__pycache__/messages.cpython-314.pyc +0 -0
- package/tui/acp/__pycache__/prompt.cpython-314.pyc +0 -0
- package/tui/acp/__pycache__/protocol.cpython-314.pyc +0 -0
- package/tui/acp/agent.py +125 -58
- package/tui/acp/api.py +12 -0
- package/tui/acp/messages.py +3 -1
- package/tui/acp/prompt.py +0 -57
- package/tui/acp/protocol.py +6 -0
- package/tui/app.py +11 -33
- package/tui/messages.py +2 -0
- package/tui/path_complete.py +1 -1
- package/tui/paths.py +16 -0
- package/tui/screens/__pycache__/agent_modal.cpython-314.pyc +0 -0
- package/tui/screens/__pycache__/component_picker.cpython-314.pyc +0 -0
- package/tui/screens/__pycache__/diff_screen.cpython-314.pyc +0 -0
- package/tui/screens/__pycache__/log.cpython-314.pyc +0 -0
- package/tui/screens/__pycache__/main.cpython-313.pyc +0 -0
- package/tui/screens/__pycache__/main.cpython-314.pyc +0 -0
- package/tui/screens/__pycache__/permissions.cpython-314.pyc +0 -0
- package/tui/screens/__pycache__/projects_app.cpython-314.pyc +0 -0
- package/tui/screens/__pycache__/projects_screen.cpython-314.pyc +0 -0
- package/tui/screens/__pycache__/sessions.cpython-314.pyc +0 -0
- package/tui/screens/__pycache__/store.cpython-314.pyc +0 -0
- package/tui/screens/__pycache__/tool_content_screen.cpython-314.pyc +0 -0
- package/tui/screens/component_picker.py +35 -15
- package/tui/screens/diff_screen.py +84 -0
- package/tui/screens/log.py +25 -25
- package/tui/screens/main.py +48 -26
- package/tui/screens/projects.tcss +5 -1
- package/tui/screens/projects_app.py +6 -6
- package/tui/screens/projects_screen.py +19 -39
- package/tui/screens/sessions.py +3 -5
- package/tui/session_tracker.py +3 -0
- package/tui/settings_schema.py +1 -1
- package/tui/visuals/__pycache__/columns.cpython-314.pyc +0 -0
- package/tui/visuals/columns.py +1 -2
- package/tui/widgets/__pycache__/acp_content.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/agent_response.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/agent_thought.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/conversation.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/diff_view.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/directory_input.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/future_text.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/grid_select.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/load_more.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/mandelbrot.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/markdown_note.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/modified_files.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/plan.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/project_grid_select.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/project_summary.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/session_grid_select.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/session_summary.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/shell_result.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/side_bar.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/subagent.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/subagent_screen.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/terminal.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/tool_call.cpython-314.pyc +0 -0
- package/tui/widgets/agent_response.py +0 -1
- package/tui/widgets/agent_thought.py +0 -1
- package/tui/widgets/conversation.py +157 -14
- package/tui/widgets/diff_view.py +10 -0
- package/tui/widgets/load_more.py +24 -0
- package/tui/widgets/modified_files.py +86 -12
- package/tui/widgets/plan.py +1 -1
- package/tui/widgets/project_grid_select.py +9 -2
- package/tui/widgets/session_grid_select.py +6 -0
- package/tui/widgets/side_bar.py +10 -0
- package/tui/widgets/subagent.py +92 -58
- package/tui/widgets/subagent_screen.py +638 -115
- package/tui/widgets/terminal.py +1 -2
- package/tui/widgets/terminal_tool.py +11 -2
- package/tui/widgets/tool_call.py +35 -2
- package/onecode/memory/offload.py +0 -114
- package/onecode/memory/symbolic.py +0 -164
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
3
4
|
from dataclasses import dataclass
|
|
4
|
-
from typing import Any, Iterable, Mapping
|
|
5
|
+
from typing import Any, Callable, Iterable, Mapping
|
|
5
6
|
|
|
6
7
|
from onecode.agent.tools.errors import ToolInputError, ToolPermissionError
|
|
7
8
|
from onecode.agent.tools.protocol import ToolCall, ToolResult
|
|
@@ -19,11 +20,19 @@ class ToolSpec:
|
|
|
19
20
|
max_result_size_chars: int = 20_000
|
|
20
21
|
|
|
21
22
|
|
|
22
|
-
class Tool(
|
|
23
|
+
class Tool(ABC):
|
|
24
|
+
@abstractmethod
|
|
23
25
|
def spec(self) -> ToolSpec: ...
|
|
24
26
|
|
|
27
|
+
@abstractmethod
|
|
25
28
|
def run(self, tool_input: dict[str, Any]) -> ToolResult: ...
|
|
26
29
|
|
|
30
|
+
async def run_async(
|
|
31
|
+
self, tool_input: dict[str, Any],
|
|
32
|
+
cancel_check: Callable[[], bool] | None = None,
|
|
33
|
+
) -> ToolResult:
|
|
34
|
+
return self.run(tool_input)
|
|
35
|
+
|
|
27
36
|
|
|
28
37
|
class ToolRegistry:
|
|
29
38
|
def __init__(
|
|
@@ -68,6 +77,64 @@ class ToolRegistry:
|
|
|
68
77
|
})
|
|
69
78
|
return schemas
|
|
70
79
|
|
|
80
|
+
async def dispatch_async(
|
|
81
|
+
self,
|
|
82
|
+
call: ToolCall,
|
|
83
|
+
cancel_check: Callable[[], bool] | None = None,
|
|
84
|
+
) -> ToolResult:
|
|
85
|
+
tool = self.get(call.name)
|
|
86
|
+
if tool is None:
|
|
87
|
+
return ToolResult(
|
|
88
|
+
name=call.name,
|
|
89
|
+
output={"error": f"unknown tool: {call.name}"},
|
|
90
|
+
is_error=True,
|
|
91
|
+
tool_use_id=call.tool_use_id,
|
|
92
|
+
)
|
|
93
|
+
try:
|
|
94
|
+
spec = tool.spec()
|
|
95
|
+
validate_json_schema(call.input, spec.input_schema, tool_name=call.name)
|
|
96
|
+
result = await tool.run_async(call.input, cancel_check=cancel_check)
|
|
97
|
+
max_chars = spec.max_result_size_chars
|
|
98
|
+
if max_chars > 0 and isinstance(result.output, str) and len(result.output) > max_chars:
|
|
99
|
+
result = ToolResult(
|
|
100
|
+
name=result.name,
|
|
101
|
+
output=result.output[:max_chars]
|
|
102
|
+
+ f"\n\n[Output truncated to {max_chars} characters]",
|
|
103
|
+
is_error=result.is_error,
|
|
104
|
+
tool_use_id=result.tool_use_id or call.tool_use_id,
|
|
105
|
+
content_type=result.content_type,
|
|
106
|
+
)
|
|
107
|
+
if result.tool_use_id is None:
|
|
108
|
+
return ToolResult(
|
|
109
|
+
name=result.name,
|
|
110
|
+
output=result.output,
|
|
111
|
+
is_error=result.is_error,
|
|
112
|
+
tool_use_id=call.tool_use_id,
|
|
113
|
+
content_type=result.content_type,
|
|
114
|
+
)
|
|
115
|
+
return result
|
|
116
|
+
except ToolInputError as e:
|
|
117
|
+
return ToolResult(
|
|
118
|
+
name=call.name,
|
|
119
|
+
output={"error": str(e)},
|
|
120
|
+
is_error=True,
|
|
121
|
+
tool_use_id=call.tool_use_id,
|
|
122
|
+
)
|
|
123
|
+
except ToolPermissionError as e:
|
|
124
|
+
return ToolResult(
|
|
125
|
+
name=call.name,
|
|
126
|
+
output={"error": str(e)},
|
|
127
|
+
is_error=True,
|
|
128
|
+
tool_use_id=call.tool_use_id,
|
|
129
|
+
)
|
|
130
|
+
except Exception as e:
|
|
131
|
+
return ToolResult(
|
|
132
|
+
name=call.name,
|
|
133
|
+
output={"error": str(e)},
|
|
134
|
+
is_error=True,
|
|
135
|
+
tool_use_id=call.tool_use_id,
|
|
136
|
+
)
|
|
137
|
+
|
|
71
138
|
def dispatch(
|
|
72
139
|
self,
|
|
73
140
|
call: ToolCall,
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import asyncio
|
|
3
4
|
import enum
|
|
4
5
|
import os
|
|
5
6
|
import resource
|
|
6
7
|
import subprocess
|
|
7
8
|
from dataclasses import dataclass, field
|
|
8
9
|
from pathlib import Path
|
|
9
|
-
from typing import Optional
|
|
10
|
+
from typing import Callable, Optional
|
|
10
11
|
|
|
11
12
|
|
|
12
13
|
class SandboxMode(enum.Enum):
|
|
@@ -77,7 +78,6 @@ class Sandbox:
|
|
|
77
78
|
bwrap_args.extend(["--uid", str(os.getuid())])
|
|
78
79
|
bwrap_args.extend(["--gid", str(os.getgid())])
|
|
79
80
|
|
|
80
|
-
env = " ".join(f"{k}={v}" for k, v in os.environ.items() if k in self.config.env_whitelist or k.startswith("LC_"))
|
|
81
81
|
bwrap_args.extend(["--setenv", "HOME", "/workspace"])
|
|
82
82
|
bwrap_args.extend(["--setenv", "PATH", "/usr/local/sbin:/usr/local/bin:/usr/bin:/bin"])
|
|
83
83
|
|
|
@@ -109,6 +109,24 @@ class Sandbox:
|
|
|
109
109
|
return self._exec_docker(cmd, timeout)
|
|
110
110
|
return self._exec_direct(cmd, timeout)
|
|
111
111
|
|
|
112
|
+
async def exec_async(
|
|
113
|
+
self, cmd: str, timeout: int = 60,
|
|
114
|
+
cancel_check: Callable[[], bool] | None = None,
|
|
115
|
+
) -> dict:
|
|
116
|
+
"""Async public entry point — supports cancellation mid-execution."""
|
|
117
|
+
mode = self.config.mode
|
|
118
|
+
|
|
119
|
+
if mode == SandboxMode.NONE:
|
|
120
|
+
return await self._exec_direct_async(cmd, timeout, cancel_check)
|
|
121
|
+
if mode == SandboxMode.BUBBLEWRAP:
|
|
122
|
+
if self._check_bwrap_available():
|
|
123
|
+
return await self._exec_bwrap_async(cmd, timeout, cancel_check)
|
|
124
|
+
return await self._exec_direct_async(cmd, timeout, cancel_check)
|
|
125
|
+
if mode == SandboxMode.DOCKER:
|
|
126
|
+
# Docker exec is async-adaptable via docker-py, but keep sync for now
|
|
127
|
+
return self._exec_docker(cmd, timeout)
|
|
128
|
+
return await self._exec_direct_async(cmd, timeout, cancel_check)
|
|
129
|
+
|
|
112
130
|
def _exec_direct(self, cmd: str, timeout: int) -> dict:
|
|
113
131
|
# Apply resource limits in the *child* process only — calling
|
|
114
132
|
# ``setrlimit`` on the parent (this Python interpreter) shrinks
|
|
@@ -130,20 +148,6 @@ class Sandbox:
|
|
|
130
148
|
)
|
|
131
149
|
except (ValueError, OSError, resource.error):
|
|
132
150
|
pass
|
|
133
|
-
try:
|
|
134
|
-
resource.setrlimit(
|
|
135
|
-
resource.RLIMIT_NPROC, (limits.max_procs, limits.max_procs + 5)
|
|
136
|
-
)
|
|
137
|
-
except (ValueError, OSError, resource.error):
|
|
138
|
-
pass
|
|
139
|
-
try:
|
|
140
|
-
resource.setrlimit(
|
|
141
|
-
resource.RLIMIT_NOFILE,
|
|
142
|
-
(limits.max_open_files, limits.max_open_files + 10),
|
|
143
|
-
)
|
|
144
|
-
except (ValueError, OSError, resource.error):
|
|
145
|
-
pass
|
|
146
|
-
|
|
147
151
|
try:
|
|
148
152
|
result = subprocess.run(
|
|
149
153
|
cmd,
|
|
@@ -166,6 +170,96 @@ class Sandbox:
|
|
|
166
170
|
except Exception as e:
|
|
167
171
|
return {"success": False, "error": str(e), "sandbox": "none"}
|
|
168
172
|
|
|
173
|
+
async def _exec_direct_async(
|
|
174
|
+
self, cmd: str, timeout: int,
|
|
175
|
+
cancel_check: Callable[[], bool] | None = None,
|
|
176
|
+
) -> dict:
|
|
177
|
+
limits = self.config.resource_limits
|
|
178
|
+
|
|
179
|
+
def _preexec_apply_rlimits() -> None:
|
|
180
|
+
try:
|
|
181
|
+
resource.setrlimit(
|
|
182
|
+
resource.RLIMIT_CPU, (limits.cpu_time, limits.cpu_time + 5)
|
|
183
|
+
)
|
|
184
|
+
except (ValueError, OSError, resource.error):
|
|
185
|
+
pass
|
|
186
|
+
try:
|
|
187
|
+
mem_bytes = limits.memory_mb * 1024 * 1024
|
|
188
|
+
resource.setrlimit(
|
|
189
|
+
resource.RLIMIT_AS, (mem_bytes, mem_bytes)
|
|
190
|
+
)
|
|
191
|
+
except (ValueError, OSError, resource.error):
|
|
192
|
+
pass
|
|
193
|
+
process = await asyncio.create_subprocess_shell(
|
|
194
|
+
cmd,
|
|
195
|
+
stdout=asyncio.subprocess.PIPE,
|
|
196
|
+
stderr=asyncio.subprocess.PIPE,
|
|
197
|
+
cwd=str(self.config.workspace_root),
|
|
198
|
+
preexec_fn=_preexec_apply_rlimits,
|
|
199
|
+
)
|
|
200
|
+
try:
|
|
201
|
+
if cancel_check is not None and cancel_check():
|
|
202
|
+
process.terminate()
|
|
203
|
+
await process.wait()
|
|
204
|
+
return {"success": False, "error": "Cancelled", "sandbox": "none"}
|
|
205
|
+
stdout, stderr = await asyncio.wait_for(
|
|
206
|
+
process.communicate(), timeout=timeout,
|
|
207
|
+
)
|
|
208
|
+
stdout = stdout.decode("utf-8", errors="replace") if isinstance(stdout, bytes) else stdout
|
|
209
|
+
stderr = stderr.decode("utf-8", errors="replace") if isinstance(stderr, bytes) else stderr
|
|
210
|
+
return {
|
|
211
|
+
"success": process.returncode == 0,
|
|
212
|
+
"stdout": stdout,
|
|
213
|
+
"stderr": stderr,
|
|
214
|
+
"returncode": process.returncode,
|
|
215
|
+
"sandbox": "none",
|
|
216
|
+
}
|
|
217
|
+
except asyncio.TimeoutError:
|
|
218
|
+
process.terminate()
|
|
219
|
+
await process.wait()
|
|
220
|
+
return {"success": False, "error": "Command timed out", "sandbox": "none"}
|
|
221
|
+
except Exception as e:
|
|
222
|
+
process.terminate()
|
|
223
|
+
await process.wait()
|
|
224
|
+
return {"success": False, "error": str(e), "sandbox": "none"}
|
|
225
|
+
|
|
226
|
+
async def _exec_bwrap_async(
|
|
227
|
+
self, cmd: str, timeout: int,
|
|
228
|
+
cancel_check: Callable[[], bool] | None = None,
|
|
229
|
+
) -> dict:
|
|
230
|
+
bwrap_cmd = self._build_bwrap_cmd(cmd)
|
|
231
|
+
process = await asyncio.create_subprocess_exec(
|
|
232
|
+
*bwrap_cmd,
|
|
233
|
+
stdout=asyncio.subprocess.PIPE,
|
|
234
|
+
stderr=asyncio.subprocess.PIPE,
|
|
235
|
+
cwd=str(self.config.workspace_root),
|
|
236
|
+
)
|
|
237
|
+
try:
|
|
238
|
+
if cancel_check is not None and cancel_check():
|
|
239
|
+
process.terminate()
|
|
240
|
+
await process.wait()
|
|
241
|
+
return {"success": False, "error": "Cancelled", "sandbox": "bwrap"}
|
|
242
|
+
stdout, stderr = await asyncio.wait_for(
|
|
243
|
+
process.communicate(), timeout=timeout,
|
|
244
|
+
)
|
|
245
|
+
stdout = stdout.decode("utf-8", errors="replace") if isinstance(stdout, bytes) else stdout
|
|
246
|
+
stderr = stderr.decode("utf-8", errors="replace") if isinstance(stderr, bytes) else stderr
|
|
247
|
+
return {
|
|
248
|
+
"success": process.returncode == 0,
|
|
249
|
+
"stdout": stdout,
|
|
250
|
+
"stderr": stderr,
|
|
251
|
+
"returncode": process.returncode,
|
|
252
|
+
"sandbox": "bwrap",
|
|
253
|
+
}
|
|
254
|
+
except asyncio.TimeoutError:
|
|
255
|
+
process.terminate()
|
|
256
|
+
await process.wait()
|
|
257
|
+
return {"success": False, "error": "Command timed out", "sandbox": "bwrap"}
|
|
258
|
+
except Exception as e:
|
|
259
|
+
process.terminate()
|
|
260
|
+
await process.wait()
|
|
261
|
+
return {"success": False, "error": str(e), "sandbox": "bwrap"}
|
|
262
|
+
|
|
169
263
|
def _exec_bwrap(self, cmd: str, timeout: int) -> dict:
|
|
170
264
|
bwrap_cmd = self._build_bwrap_cmd(cmd)
|
|
171
265
|
|
|
@@ -3,12 +3,12 @@ from __future__ import annotations
|
|
|
3
3
|
from typing import Any
|
|
4
4
|
|
|
5
5
|
from onecode.agent.tools.protocol import ToolResult
|
|
6
|
-
from onecode.agent.tools.registry import ToolSpec
|
|
6
|
+
from onecode.agent.tools.registry import Tool, ToolSpec
|
|
7
7
|
from onecode.skills.loader import SkillLoader
|
|
8
8
|
from onecode.skills.argument_substitution import substitute_arguments
|
|
9
9
|
|
|
10
10
|
|
|
11
|
-
class SkillTool:
|
|
11
|
+
class SkillTool(Tool):
|
|
12
12
|
"""Run a SKILL.md skill (Clawd-Code pattern)."""
|
|
13
13
|
|
|
14
14
|
def __init__(self, loader: SkillLoader):
|
|
@@ -3,10 +3,10 @@ from __future__ import annotations
|
|
|
3
3
|
from typing import Any
|
|
4
4
|
|
|
5
5
|
from onecode.agent.tools.protocol import ToolResult
|
|
6
|
-
from onecode.agent.tools.registry import ToolSpec
|
|
6
|
+
from onecode.agent.tools.registry import Tool, ToolSpec
|
|
7
7
|
|
|
8
8
|
|
|
9
|
-
class TodoCreateTool:
|
|
9
|
+
class TodoCreateTool(Tool):
|
|
10
10
|
def __init__(self, todo_manager):
|
|
11
11
|
self._tm = todo_manager
|
|
12
12
|
|
|
@@ -14,10 +14,12 @@ class TodoCreateTool:
|
|
|
14
14
|
return ToolSpec(
|
|
15
15
|
name="TodoCreate",
|
|
16
16
|
description=(
|
|
17
|
-
"Create a todo (
|
|
18
|
-
"
|
|
19
|
-
"
|
|
20
|
-
"
|
|
17
|
+
"Create a todo (plan item) for tracking progress. ALL tasks — simple "
|
|
18
|
+
"or complex — are created via TodoCreate, persisted to .cdh/todos.json, "
|
|
19
|
+
"and mirrored to the sidebar Plan widget. At execution time, route by "
|
|
20
|
+
"complexity: simple todos are executed directly; complex todos are "
|
|
21
|
+
"delegated via `Spawn` (mark with metadata={\"delegate_to\": \"general\"}). "
|
|
22
|
+
"Returns todo id."
|
|
21
23
|
),
|
|
22
24
|
input_schema={
|
|
23
25
|
"type": "object",
|
|
@@ -42,7 +44,7 @@ class TodoCreateTool:
|
|
|
42
44
|
|
|
43
45
|
|
|
44
46
|
|
|
45
|
-
class TodoGetTool:
|
|
47
|
+
class TodoGetTool(Tool):
|
|
46
48
|
def __init__(self, todo_manager):
|
|
47
49
|
self._tm = todo_manager
|
|
48
50
|
|
|
@@ -69,7 +71,7 @@ class TodoGetTool:
|
|
|
69
71
|
|
|
70
72
|
|
|
71
73
|
|
|
72
|
-
class TodoListTool:
|
|
74
|
+
class TodoListTool(Tool):
|
|
73
75
|
def __init__(self, todo_manager):
|
|
74
76
|
self._tm = todo_manager
|
|
75
77
|
|
|
@@ -95,7 +97,7 @@ class TodoListTool:
|
|
|
95
97
|
|
|
96
98
|
|
|
97
99
|
|
|
98
|
-
class TodoUpdateTool:
|
|
100
|
+
class TodoUpdateTool(Tool):
|
|
99
101
|
def __init__(self, todo_manager):
|
|
100
102
|
self._tm = todo_manager
|
|
101
103
|
|
|
@@ -137,7 +139,7 @@ class TodoUpdateTool:
|
|
|
137
139
|
|
|
138
140
|
|
|
139
141
|
|
|
140
|
-
class TodoOutputTool:
|
|
142
|
+
class TodoOutputTool(Tool):
|
|
141
143
|
def __init__(self, todo_manager):
|
|
142
144
|
self._tm = todo_manager
|
|
143
145
|
|
|
@@ -148,20 +150,20 @@ class TodoOutputTool:
|
|
|
148
150
|
input_schema={
|
|
149
151
|
"type": "object",
|
|
150
152
|
"properties": {
|
|
151
|
-
"
|
|
153
|
+
"taskId": {"type": "string", "description": "Todo id to get output for"},
|
|
152
154
|
},
|
|
153
|
-
"required": ["
|
|
155
|
+
"required": ["taskId"],
|
|
154
156
|
},
|
|
155
157
|
)
|
|
156
158
|
|
|
157
159
|
def run(self, tool_input: dict[str, Any]) -> ToolResult:
|
|
158
|
-
todo_id = tool_input.get("
|
|
160
|
+
todo_id = tool_input.get("taskId", "")
|
|
159
161
|
result = self._tm.get_todo_output(todo_id)
|
|
160
162
|
return ToolResult(name="TodoOutput", output=result)
|
|
161
163
|
|
|
162
164
|
|
|
163
165
|
|
|
164
|
-
class TodoStopTool:
|
|
166
|
+
class TodoStopTool(Tool):
|
|
165
167
|
def __init__(self, todo_manager):
|
|
166
168
|
self._tm = todo_manager
|
|
167
169
|
|
|
@@ -172,17 +174,34 @@ class TodoStopTool:
|
|
|
172
174
|
input_schema={
|
|
173
175
|
"type": "object",
|
|
174
176
|
"properties": {
|
|
175
|
-
"
|
|
177
|
+
"taskId": {"type": "string", "description": "Todo id to stop"},
|
|
176
178
|
},
|
|
177
|
-
"required": ["
|
|
179
|
+
"required": ["taskId"],
|
|
178
180
|
},
|
|
179
181
|
)
|
|
180
182
|
|
|
181
183
|
def run(self, tool_input: dict[str, Any]) -> ToolResult:
|
|
182
|
-
todo_id = tool_input.get("
|
|
184
|
+
todo_id = tool_input.get("taskId", "")
|
|
183
185
|
result = self._tm.update_todo(todo_id, status="completed")
|
|
184
186
|
if result is None:
|
|
185
187
|
return ToolResult(name="TodoStop", output={"success": False, "stopped": False, "error": "Todo not found"}, is_error=True)
|
|
186
188
|
if result.get("deleted"):
|
|
187
|
-
return ToolResult(name="TodoStop", output={"success": True, "stopped": True, "
|
|
188
|
-
return ToolResult(name="TodoStop", output={"success": True, "stopped": True, "
|
|
189
|
+
return ToolResult(name="TodoStop", output={"success": True, "stopped": True, "taskId": todo_id})
|
|
190
|
+
return ToolResult(name="TodoStop", output={"success": True, "stopped": True, "taskId": todo_id})
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
class TodoClearTool(Tool):
|
|
194
|
+
def __init__(self, todo_manager):
|
|
195
|
+
self._tm = todo_manager
|
|
196
|
+
|
|
197
|
+
def spec(self) -> ToolSpec:
|
|
198
|
+
return ToolSpec(
|
|
199
|
+
name="TodoClear",
|
|
200
|
+
description="Clear all todos. Resets the entire plan — all pending, in-progress, and completed todos are removed and the plan starts fresh.",
|
|
201
|
+
input_schema={"type": "object", "properties": {}, "required": []},
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
def run(self, tool_input: dict[str, Any]) -> ToolResult:
|
|
205
|
+
count = len(self._tm.list_todos())
|
|
206
|
+
self._tm.clear_todos()
|
|
207
|
+
return ToolResult(name="TodoClear", output={"cleared": count})
|
|
@@ -8,7 +8,7 @@ from typing import Any, Optional
|
|
|
8
8
|
import httpx
|
|
9
9
|
|
|
10
10
|
from onecode.agent.tools.protocol import ToolResult
|
|
11
|
-
from onecode.agent.tools.registry import ToolSpec
|
|
11
|
+
from onecode.agent.tools.registry import Tool, ToolSpec
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
@dataclass
|
|
@@ -68,7 +68,7 @@ class WebSearch:
|
|
|
68
68
|
|
|
69
69
|
for url in search_urls:
|
|
70
70
|
try:
|
|
71
|
-
results = self._search_duckduckgo(url, num_results)
|
|
71
|
+
results = self._search_duckduckgo(url, num_results, query)
|
|
72
72
|
if results:
|
|
73
73
|
return results
|
|
74
74
|
except Exception:
|
|
@@ -76,7 +76,7 @@ class WebSearch:
|
|
|
76
76
|
|
|
77
77
|
return self._search_fallback(query, num_results)
|
|
78
78
|
|
|
79
|
-
def _search_duckduckgo(self, url: str, num_results: int) -> str:
|
|
79
|
+
def _search_duckduckgo(self, url: str, num_results: int, query: str) -> str:
|
|
80
80
|
with httpx.Client(timeout=self.timeout, follow_redirects=True) as client:
|
|
81
81
|
resp = client.get(url)
|
|
82
82
|
resp.raise_for_status()
|
|
@@ -84,9 +84,7 @@ class WebSearch:
|
|
|
84
84
|
html_content = resp.text
|
|
85
85
|
results = []
|
|
86
86
|
|
|
87
|
-
result_pattern = re.compile(r'<a class="result__a" href="([^"]+)">([^<]+)</a>.*?<a class="result__snippet"[^>]*>([^<]+)</a>', re.DOTALL)
|
|
88
87
|
snippets = re.findall(r'<a class="result__a" href="([^"]+)">([^<]+)</a>', html_content)
|
|
89
|
-
snippet_pattern = re.compile(r'<a class="result__a" href="[^"]+">[^<]+</a>.*?(?:<p class="result__snippet">([^<]+)</p>)?', re.DOTALL)
|
|
90
88
|
|
|
91
89
|
for i, (url, title) in enumerate(snippets[:num_results]):
|
|
92
90
|
snippet_match = re.search(rf'<a class="result__a" href="{re.escape(url)}"[^>]*>[^<]+</a>.*?(?:<p class="result__snippet">([^<]+)</p>|(?:<a class="result__snippet"[^>]*>([^<]+)</a>))?', html_content, re.DOTALL)
|
|
@@ -104,6 +102,7 @@ class WebSearch:
|
|
|
104
102
|
return f"No results found for '{query}'"
|
|
105
103
|
|
|
106
104
|
def _search_fallback(self, query: str, num_results: int) -> str:
|
|
105
|
+
"""Fallback search using a different method."""
|
|
107
106
|
return f"Web search for '{query}' returned no results. Try using WebFetch to directly access a URL."
|
|
108
107
|
|
|
109
108
|
def _quote(self, query: str) -> str:
|
|
@@ -121,7 +120,7 @@ def websearch(query: str, num_results: int = 5) -> str:
|
|
|
121
120
|
return searcher.search(query, num_results)
|
|
122
121
|
|
|
123
122
|
|
|
124
|
-
class WebFetchTool:
|
|
123
|
+
class WebFetchTool(Tool):
|
|
125
124
|
def spec(self) -> ToolSpec:
|
|
126
125
|
return ToolSpec(
|
|
127
126
|
name="WebFetch",
|
|
@@ -146,7 +145,7 @@ class WebFetchTool:
|
|
|
146
145
|
|
|
147
146
|
|
|
148
147
|
|
|
149
|
-
class WebSearchTool:
|
|
148
|
+
class WebSearchTool(Tool):
|
|
150
149
|
def spec(self) -> ToolSpec:
|
|
151
150
|
return ToolSpec(
|
|
152
151
|
name="WebSearch",
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: agent-browser
|
|
3
|
+
description: Browser automation via Playwright for web interaction and testing
|
|
4
|
+
enabled: true
|
|
5
|
+
triggers:
|
|
6
|
+
- browser
|
|
7
|
+
- playwrith
|
|
8
|
+
- screenshot
|
|
9
|
+
- web page
|
|
10
|
+
- navigate
|
|
11
|
+
- open url
|
|
12
|
+
- click element
|
|
13
|
+
- fill form
|
|
14
|
+
- scrape
|
|
15
|
+
- headless
|
|
16
|
+
allowed_tools:
|
|
17
|
+
- Bash
|
|
18
|
+
- Read
|
|
19
|
+
- Write
|
|
20
|
+
phases:
|
|
21
|
+
- verify
|
|
22
|
+
- deliver
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
# Agent Browser
|
|
26
|
+
|
|
27
|
+
## When to Use
|
|
28
|
+
|
|
29
|
+
- Navigating to a web page and interacting with it
|
|
30
|
+
- Taking screenshots of pages
|
|
31
|
+
- Filling and submitting forms
|
|
32
|
+
- Clicking buttons, links, and other elements
|
|
33
|
+
- Extracting dynamic content that requires JS execution
|
|
34
|
+
- End-to-end testing of web applications
|
|
35
|
+
- Scraping SPAs (Single Page Applications)
|
|
36
|
+
|
|
37
|
+
Use **WebFetch** instead for simple GET requests that don't need interaction.
|
|
38
|
+
|
|
39
|
+
## Setup
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
pip install playwright
|
|
43
|
+
playwright install chromium
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Check if already installed:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
python -c "import playwright; print(playwright.__file__)"
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Basic Usage
|
|
53
|
+
|
|
54
|
+
### Navigate and Screenshot
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
from playwright.sync_api import sync_playwright
|
|
58
|
+
|
|
59
|
+
with sync_playwright() as p:
|
|
60
|
+
browser = p.chromium.launch(headless=True)
|
|
61
|
+
page = browser.new_page()
|
|
62
|
+
page.goto("https://example.com")
|
|
63
|
+
page.screenshot(path="screenshot.png", full_page=True)
|
|
64
|
+
browser.close()
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Fill Form and Submit
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
page.fill("input[name='username']", "myuser")
|
|
71
|
+
page.fill("input[name='password']", "mypass")
|
|
72
|
+
page.click("button[type='submit']")
|
|
73
|
+
page.wait_for_url("**/dashboard")
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Extract Content
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
# Get text content
|
|
80
|
+
title = page.text_content("h1")
|
|
81
|
+
items = page.query_selector_all(".item")
|
|
82
|
+
for item in items:
|
|
83
|
+
print(item.inner_text())
|
|
84
|
+
|
|
85
|
+
# Get HTML
|
|
86
|
+
html = page.content()
|
|
87
|
+
|
|
88
|
+
# Evaluate JavaScript
|
|
89
|
+
result = page.evaluate("() => document.title")
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Wait Strategies
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
page.wait_for_selector(".results") # element appears
|
|
96
|
+
page.wait_for_load_state("networkidle") # no network activity
|
|
97
|
+
page.wait_for_timeout(2000) # sleep 2s
|
|
98
|
+
page.wait_for_url("**/success") # URL pattern
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Selectors
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
# CSS
|
|
105
|
+
page.click(".btn-primary")
|
|
106
|
+
page.click("button[data-id='submit']")
|
|
107
|
+
|
|
108
|
+
# Text
|
|
109
|
+
page.click("text=Submit")
|
|
110
|
+
page.click("button:has-text('Continue')")
|
|
111
|
+
|
|
112
|
+
# XPath
|
|
113
|
+
page.click("xpath=//button[@name='login']")
|
|
114
|
+
|
|
115
|
+
# Role (recommended)
|
|
116
|
+
page.get_by_role("button", name="Submit").click()
|
|
117
|
+
page.get_by_label("Email").fill("user@example.com")
|
|
118
|
+
page.get_by_placeholder("Search...").fill("query")
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Common Patterns
|
|
122
|
+
|
|
123
|
+
### Login Flow
|
|
124
|
+
|
|
125
|
+
```python
|
|
126
|
+
page.goto("https://example.com/login")
|
|
127
|
+
page.fill("#email", email)
|
|
128
|
+
page.fill("#password", password)
|
|
129
|
+
page.click("button[type='submit']")
|
|
130
|
+
page.wait_for_url("**/dashboard")
|
|
131
|
+
print("Logined:", page.url)
|
|
132
|
+
page.screenshot(path="after_login.png")
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### Paginated Scraping
|
|
136
|
+
|
|
137
|
+
```python
|
|
138
|
+
results = []
|
|
139
|
+
while True:
|
|
140
|
+
items = page.query_selector_all(".result-item")
|
|
141
|
+
for item in items:
|
|
142
|
+
results.append(item.inner_text())
|
|
143
|
+
next_btn = page.query_selector(".pagination .next")
|
|
144
|
+
if not next_btn or next_btn.is_disabled():
|
|
145
|
+
break
|
|
146
|
+
next_btn.click()
|
|
147
|
+
page.wait_for_load_state("networkidle")
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### Multi-page Navigation
|
|
151
|
+
|
|
152
|
+
```python
|
|
153
|
+
pages = ["https://example.com/page1", "https://example.com/page2"]
|
|
154
|
+
for url in pages:
|
|
155
|
+
page.goto(url, wait_until="domcontentloaded")
|
|
156
|
+
page.screenshot(path=f"screenshot-{url.split('/')[-1]}.png")
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
## Executing via Bash
|
|
160
|
+
|
|
161
|
+
For quick one-off tasks, use `python -c`:
|
|
162
|
+
|
|
163
|
+
```bash
|
|
164
|
+
python -c "
|
|
165
|
+
from playwright.sync_api import sync_playwright
|
|
166
|
+
with sync_playwright() as p:
|
|
167
|
+
browser = p.chromium.launch(headless=True)
|
|
168
|
+
page = browser.new_page()
|
|
169
|
+
page.goto('https://example.com')
|
|
170
|
+
print(page.title())
|
|
171
|
+
browser.close()
|
|
172
|
+
"
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
Or save a script and execute:
|
|
176
|
+
|
|
177
|
+
```bash
|
|
178
|
+
cat > /tmp/browser_script.py << 'EOF'
|
|
179
|
+
from playwright.sync_api import sync_playwright
|
|
180
|
+
with sync_playwright() as p:
|
|
181
|
+
browser = p.chromium.launch(headless=True)
|
|
182
|
+
page = browser.new_page()
|
|
183
|
+
...
|
|
184
|
+
browser.close()
|
|
185
|
+
EOF
|
|
186
|
+
python /tmp/browser_script.py
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
## Error Handling
|
|
190
|
+
|
|
191
|
+
```python
|
|
192
|
+
try:
|
|
193
|
+
page.goto(url, timeout=30000)
|
|
194
|
+
except Exception as e:
|
|
195
|
+
print(f"Navigation failed: {e}")
|
|
196
|
+
page.screenshot(path="error.png")
|
|
197
|
+
|
|
198
|
+
try:
|
|
199
|
+
page.click(".missing-button", timeout=5000)
|
|
200
|
+
except Exception:
|
|
201
|
+
print("Button not found, trying alternative...")
|
|
202
|
+
page.click(".fallback-button")
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
## Best Practices
|
|
206
|
+
|
|
207
|
+
- Always use `headless=True` unless user asks for visible browser
|
|
208
|
+
- Set reasonable timeouts (10-30s for navigation)
|
|
209
|
+
- Close browser after use to free resources
|
|
210
|
+
- Screenshot on errors for debugging
|
|
211
|
+
- Prefer `get_by_role/get_by_label` over fragile CSS selectors
|
|
212
|
+
- Wait for network idle before extracting content
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
name: agent-browser
|
|
2
|
+
description: >-
|
|
3
|
+
Browser automation skill via Playwright. Enables the agent to navigate pages,
|
|
4
|
+
take screenshots, click elements, fill forms, extract content, and evaluate
|
|
5
|
+
JavaScript in a headless or headed browser.
|
|
6
|
+
version: 1.0.0
|
|
7
|
+
enabled: true
|
|
8
|
+
entry: SKILL.md
|