talkops-opscloud 0.3.0b1__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.
- opscloud/__init__.py +5 -0
- opscloud/__main__.py +6 -0
- opscloud/_constants.py +53 -0
- opscloud/_debug.py +145 -0
- opscloud/_debug_buffer.py +250 -0
- opscloud/_textual_patches.py +367 -0
- opscloud/_version.py +15 -0
- opscloud/agent/__init__.py +27 -0
- opscloud/agent/config.py +168 -0
- opscloud/agent/factory.py +1103 -0
- opscloud/approval_mode.py +44 -0
- opscloud/backend/__init__.py +49 -0
- opscloud/backend/composite.py +89 -0
- opscloud/backend/local.py +183 -0
- opscloud/backend/registry.py +79 -0
- opscloud/backend/sandbox_config.py +107 -0
- opscloud/backend/sandbox_factory.py +432 -0
- opscloud/built_in_skills/remember/SKILL.md +124 -0
- opscloud/built_in_skills/skill-creator/SKILL.md +348 -0
- opscloud/built_in_skills/skill-creator/scripts/init_skill.py +371 -0
- opscloud/built_in_skills/skill-creator/scripts/quick_validate.py +165 -0
- opscloud/cli/__init__.py +1 -0
- opscloud/cli/commands/__init__.py +15 -0
- opscloud/cli/commands/auth.py +45 -0
- opscloud/cli/commands/doctor.py +100 -0
- opscloud/cli/commands/mcp.py +137 -0
- opscloud/cli/commands/threads.py +51 -0
- opscloud/cli/interactive.py +154 -0
- opscloud/cli/main.py +903 -0
- opscloud/cli/non_interactive.py +425 -0
- opscloud/cli/server_manager.py +58 -0
- opscloud/commands/__init__.py +19 -0
- opscloud/commands/_base.py +110 -0
- opscloud/commands/_guards.py +52 -0
- opscloud/commands/_router.py +146 -0
- opscloud/commands/_types.py +36 -0
- opscloud/commands/core/__init__.py +47 -0
- opscloud/commands/core/auth.py +94 -0
- opscloud/commands/core/bug.py +46 -0
- opscloud/commands/core/clear.py +106 -0
- opscloud/commands/core/cloud.py +185 -0
- opscloud/commands/core/compact.py +128 -0
- opscloud/commands/core/config_cmd.py +120 -0
- opscloud/commands/core/context.py +220 -0
- opscloud/commands/core/cost.py +44 -0
- opscloud/commands/core/doctor.py +535 -0
- opscloud/commands/core/effort.py +108 -0
- opscloud/commands/core/exit_cmd.py +41 -0
- opscloud/commands/core/fast.py +148 -0
- opscloud/commands/core/help_cmd.py +94 -0
- opscloud/commands/core/mcp.py +134 -0
- opscloud/commands/core/model.py +109 -0
- opscloud/commands/core/permissions.py +196 -0
- opscloud/commands/core/plugins.py +92 -0
- opscloud/commands/core/pool.py +108 -0
- opscloud/commands/core/resume.py +97 -0
- opscloud/commands/core/skills.py +178 -0
- opscloud/commands/power/__init__.py +51 -0
- opscloud/commands/power/agents.py +98 -0
- opscloud/commands/power/btw.py +85 -0
- opscloud/commands/power/copy.py +97 -0
- opscloud/commands/power/goal.py +548 -0
- opscloud/commands/power/loop.py +295 -0
- opscloud/commands/power/memory.py +287 -0
- opscloud/commands/power/review.py +152 -0
- opscloud/commands/power/rubric.py +287 -0
- opscloud/commands/power/runtime.py +321 -0
- opscloud/commands/power/skill_creator.py +70 -0
- opscloud/commands/power/skill_invoke.py +165 -0
- opscloud/commands/power/tasks.py +209 -0
- opscloud/commands/power/trace.py +117 -0
- opscloud/commands/power/ui_toggles.py +135 -0
- opscloud/commands/power/version.py +52 -0
- opscloud/commands/registry.py +176 -0
- opscloud/config/__init__.py +114 -0
- opscloud/config/adapters/__init__.py +5 -0
- opscloud/config/adapters/postgres.py +776 -0
- opscloud/config/adapters/sqlite.py +769 -0
- opscloud/config/aws.py +248 -0
- opscloud/config/cloud_profiles.py +437 -0
- opscloud/config/env_vars.py +81 -0
- opscloud/config/langsmith.py +319 -0
- opscloud/config/manifest.py +817 -0
- opscloud/config/metadata.py +205 -0
- opscloud/config/paths.py +604 -0
- opscloud/config/plugins.py +130 -0
- opscloud/config/settings.py +834 -0
- opscloud/config/store.py +453 -0
- opscloud/config/store_factory.py +97 -0
- opscloud/config/toml_config.py +562 -0
- opscloud/editor.py +173 -0
- opscloud/exceptions.py +65 -0
- opscloud/file_ops.py +217 -0
- opscloud/hooks/__init__.py +139 -0
- opscloud/hooks/env.py +44 -0
- opscloud/hooks/interrupt.py +121 -0
- opscloud/hooks/models/__init__.py +109 -0
- opscloud/hooks/models/adapters.py +19 -0
- opscloud/hooks/models/config.py +79 -0
- opscloud/hooks/models/domain.py +434 -0
- opscloud/hooks/models/transport.py +37 -0
- opscloud/hooks/tools.py +231 -0
- opscloud/input.py +48 -0
- opscloud/integrations/__init__.py +80 -0
- opscloud/integrations/base.py +153 -0
- opscloud/integrations/content_filter.py +119 -0
- opscloud/integrations/event_bus.py +480 -0
- opscloud/integrations/hooks.py +238 -0
- opscloud/integrations/identity.py +181 -0
- opscloud/integrations/notifications.py +134 -0
- opscloud/integrations/stream_bridge.py +397 -0
- opscloud/integrations/thread_store.py +81 -0
- opscloud/json_types.py +20 -0
- opscloud/mcp/__init__.py +57 -0
- opscloud/mcp/config.py +203 -0
- opscloud/mcp/discovery.py +229 -0
- opscloud/mcp/mcp_info.py +109 -0
- opscloud/mcp/middleware.py +102 -0
- opscloud/mcp/preload.py +505 -0
- opscloud/mcp/raw_config.py +149 -0
- opscloud/mcp/semantic_profiler.py +305 -0
- opscloud/mcp/session_manager.py +727 -0
- opscloud/mcp/trust.py +105 -0
- opscloud/memory/__init__.py +27 -0
- opscloud/memory/branch.py +61 -0
- opscloud/memory/guard.py +19 -0
- opscloud/memory/onboarding.py +120 -0
- opscloud/memory/registry.py +134 -0
- opscloud/memory/store.py +223 -0
- opscloud/middleware/__init__.py +106 -0
- opscloud/middleware/_repository_bounds.py +459 -0
- opscloud/middleware/ask_user.py +152 -0
- opscloud/middleware/auto_mode.py +60 -0
- opscloud/middleware/auto_mode_hitl.py +2434 -0
- opscloud/middleware/compaction.py +566 -0
- opscloud/middleware/configurable_model.py +233 -0
- opscloud/middleware/cost_tracking.py +1021 -0
- opscloud/middleware/glm_stall_recovery.py +109 -0
- opscloud/middleware/goal_criteria.py +1234 -0
- opscloud/middleware/goal_state_notice.py +712 -0
- opscloud/middleware/goal_tools.py +407 -0
- opscloud/middleware/headless_mcp_guard.py +88 -0
- opscloud/middleware/jev_model_router.py +1018 -0
- opscloud/middleware/local_context.py +460 -0
- opscloud/middleware/mcp_context.py +151 -0
- opscloud/middleware/mcp_middleware.py +146 -0
- opscloud/middleware/memory_guard.py +393 -0
- opscloud/middleware/model_retry.py +205 -0
- opscloud/middleware/registry.py +137 -0
- opscloud/middleware/reliable_rubric.py +709 -0
- opscloud/middleware/resume_state.py +91 -0
- opscloud/middleware/server_hooks.py +1189 -0
- opscloud/middleware/shell_allow_list.py +90 -0
- opscloud/middleware/skills.py +382 -0
- opscloud/middleware/subagent_artifacts.py +84 -0
- opscloud/middleware/subagent_telemetry.py +261 -0
- opscloud/middleware/subagents.py +243 -0
- opscloud/middleware/tool_filter.py +384 -0
- opscloud/middleware/unified_system_message.py +62 -0
- opscloud/model/__init__.py +37 -0
- opscloud/model/config.py +2992 -0
- opscloud/model/factory.py +477 -0
- opscloud/model/pool.py +459 -0
- opscloud/model/reasoning.py +282 -0
- opscloud/offload.py +173 -0
- opscloud/output.py +64 -0
- opscloud/plugins/__init__.py +189 -0
- opscloud/plugins/_json.py +22 -0
- opscloud/plugins/adapters/__init__.py +13 -0
- opscloud/plugins/adapters/agents.py +112 -0
- opscloud/plugins/adapters/commands.py +190 -0
- opscloud/plugins/adapters/mcp.py +363 -0
- opscloud/plugins/adapters/skills.py +132 -0
- opscloud/plugins/commands_cli.py +541 -0
- opscloud/plugins/discovery.py +512 -0
- opscloud/plugins/manifest.py +335 -0
- opscloud/plugins/marketplace.py +893 -0
- opscloud/plugins/models.py +334 -0
- opscloud/plugins/project_plugins.py +317 -0
- opscloud/plugins/protocol.py +31 -0
- opscloud/plugins/store.py +905 -0
- opscloud/plugins/substitution.py +122 -0
- opscloud/project_utils.py +145 -0
- opscloud/prompts/__init__.py +37 -0
- opscloud/prompts/system.py +483 -0
- opscloud/prompts/templates/system_prompt.md +166 -0
- opscloud/prompts/types.py +54 -0
- opscloud/rubrics/__init__.py +42 -0
- opscloud/rubrics/evaluator.py +404 -0
- opscloud/rubrics/evidence_extractor.py +229 -0
- opscloud/rubrics/generator.py +239 -0
- opscloud/rubrics/jev_compiler.py +175 -0
- opscloud/rubrics/jev_grader.py +297 -0
- opscloud/rubrics/middleware.py +8 -0
- opscloud/schema/__init__.py +107 -0
- opscloud/schema/interrupts.py +981 -0
- opscloud/security/__init__.py +91 -0
- opscloud/security/approval_mode.py +528 -0
- opscloud/security/approval_mode_source.py +230 -0
- opscloud/security/cli_ast_evaluator.py +552 -0
- opscloud/security/jev_classifier.py +651 -0
- opscloud/security/shell_safety.py +340 -0
- opscloud/security/unicode_security.py +439 -0
- opscloud/security/url_validation.py +160 -0
- opscloud/server/__init__.py +13 -0
- opscloud/server/_server_config.py +158 -0
- opscloud/server/server.py +242 -0
- opscloud/server/server_graph.py +93 -0
- opscloud/skills/__init__.py +91 -0
- opscloud/skills/commands.py +1099 -0
- opscloud/skills/invocation.py +200 -0
- opscloud/skills/load.py +388 -0
- opscloud/skills/loader.py +23 -0
- opscloud/skills/merge.py +65 -0
- opscloud/skills/registry.py +263 -0
- opscloud/skills/sources.py +174 -0
- opscloud/skills/trust.py +417 -0
- opscloud/state/__init__.py +142 -0
- opscloud/state/base.py +154 -0
- opscloud/state/cloud_context.py +262 -0
- opscloud/state/goal_channels.py +124 -0
- opscloud/state/goal_state_limits.py +175 -0
- opscloud/state/resume_state.py +58 -0
- opscloud/state/service_context.py +281 -0
- opscloud/state/session.py +957 -0
- opscloud/state/state_migration.py +86 -0
- opscloud/subagents/__init__.py +30 -0
- opscloud/subagents/loader.py +331 -0
- opscloud/subagents/subagents_parser.py +117 -0
- opscloud/subagents/types.py +25 -0
- opscloud/tools/__init__.py +36 -0
- opscloud/tools/catalog.py +37 -0
- opscloud/tools/display.py +24 -0
- opscloud/tools/fetch_url.py +176 -0
- opscloud/tools/goal_tools.py +423 -0
- opscloud/tools/registry.py +105 -0
- opscloud/tools/thread.py +32 -0
- opscloud/tools/web_search.py +215 -0
- opscloud/ui/__init__.py +119 -0
- opscloud/ui/app.py +5400 -0
- opscloud/ui/app.tcss +479 -0
- opscloud/ui/clipboard.py +147 -0
- opscloud/ui/command_registry.py +482 -0
- opscloud/ui/message_store.py +152 -0
- opscloud/ui/permission_store.py +360 -0
- opscloud/ui/preferences.py +113 -0
- opscloud/ui/prompt_manager.py +96 -0
- opscloud/ui/remote_client.py +505 -0
- opscloud/ui/textual_adapter.py +1269 -0
- opscloud/ui/theme.py +728 -0
- opscloud/ui/ui_help.py +415 -0
- opscloud/ui/widgets/__init__.py +383 -0
- opscloud/ui/widgets/_ask_user_types.py +81 -0
- opscloud/ui/widgets/_copy_spans.py +52 -0
- opscloud/ui/widgets/_inline_prompt.py +290 -0
- opscloud/ui/widgets/_js_eval_display.py +139 -0
- opscloud/ui/widgets/_links.py +175 -0
- opscloud/ui/widgets/_tool_stream.py +308 -0
- opscloud/ui/widgets/agent_selector.py +297 -0
- opscloud/ui/widgets/approval.py +741 -0
- opscloud/ui/widgets/ask_user.py +539 -0
- opscloud/ui/widgets/auth.py +695 -0
- opscloud/ui/widgets/auth_manager.py +214 -0
- opscloud/ui/widgets/auto_mode_notice.py +146 -0
- opscloud/ui/widgets/autocomplete.py +709 -0
- opscloud/ui/widgets/chat_input.py +805 -0
- opscloud/ui/widgets/cloud_selector.py +528 -0
- opscloud/ui/widgets/config_manager.py +652 -0
- opscloud/ui/widgets/debug_console.py +1269 -0
- opscloud/ui/widgets/devops_renderers.py +184 -0
- opscloud/ui/widgets/diff.py +237 -0
- opscloud/ui/widgets/effort_selector.py +166 -0
- opscloud/ui/widgets/goal_review.py +612 -0
- opscloud/ui/widgets/goal_status.py +57 -0
- opscloud/ui/widgets/infra_panel.py +141 -0
- opscloud/ui/widgets/install_confirm.py +114 -0
- opscloud/ui/widgets/loading.py +170 -0
- opscloud/ui/widgets/mcp_viewer.py +1204 -0
- opscloud/ui/widgets/messages.py +4461 -0
- opscloud/ui/widgets/model_selector.py +863 -0
- opscloud/ui/widgets/notification_center.py +108 -0
- opscloud/ui/widgets/notification_settings.py +119 -0
- opscloud/ui/widgets/operation_card.py +106 -0
- opscloud/ui/widgets/permissions_manager.py +667 -0
- opscloud/ui/widgets/plugin_manager.py +1212 -0
- opscloud/ui/widgets/pool_selector.py +376 -0
- opscloud/ui/widgets/preamble.py +53 -0
- opscloud/ui/widgets/skills_viewer.py +432 -0
- opscloud/ui/widgets/status.py +1037 -0
- opscloud/ui/widgets/subagent_panel.py +927 -0
- opscloud/ui/widgets/theme_selector.py +232 -0
- opscloud/ui/widgets/thread_selector.py +358 -0
- opscloud/ui/widgets/toast.py +31 -0
- opscloud/ui/widgets/tool_display.py +325 -0
- opscloud/ui/widgets/tool_renderers.py +234 -0
- opscloud/ui/widgets/tool_widgets.py +278 -0
- opscloud/ui/widgets/welcome.py +421 -0
- opscloud/ui/widgets/welcome_popup.py +102 -0
- opscloud/utils/__init__.py +24 -0
- opscloud/utils/cost_estimation.py +169 -0
- opscloud/utils/git.py +390 -0
- opscloud/utils/logger.py +571 -0
- opscloud/utils/session_stats.py +115 -0
- opscloud/utils/startup_error.py +37 -0
- talkops_opscloud-0.3.0b1.dist-info/METADATA +449 -0
- talkops_opscloud-0.3.0b1.dist-info/RECORD +309 -0
- talkops_opscloud-0.3.0b1.dist-info/WHEEL +4 -0
- talkops_opscloud-0.3.0b1.dist-info/entry_points.txt +2 -0
- talkops_opscloud-0.3.0b1.dist-info/licenses/LICENSE +175 -0
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
"""Runtime compatibility patches for Textual internals.
|
|
2
|
+
|
|
3
|
+
Applies targeted enhancements and bug fixes for Textual terminal widgets:
|
|
4
|
+
1. Keyboard parser modifier preservation (handling ESC sequences and modifier bits).
|
|
5
|
+
2. Mouse click-chain word selection and text range resolution.
|
|
6
|
+
3. Detached widget hit detection safety.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from opscloud.utils.logger import get_logger
|
|
12
|
+
import re
|
|
13
|
+
from inspect import isawaitable
|
|
14
|
+
from typing import TYPE_CHECKING, Any, cast
|
|
15
|
+
|
|
16
|
+
from rich.text import Text
|
|
17
|
+
from textual import __version__ as _textual_version
|
|
18
|
+
from textual.content import Content
|
|
19
|
+
from textual.geometry import Offset
|
|
20
|
+
from textual.selection import Selection
|
|
21
|
+
|
|
22
|
+
if TYPE_CHECKING:
|
|
23
|
+
from collections.abc import Iterable
|
|
24
|
+
|
|
25
|
+
from textual.events import Click, Event
|
|
26
|
+
from textual.screen import Screen
|
|
27
|
+
from textual.selection import SelectState
|
|
28
|
+
from textual.widget import Widget
|
|
29
|
+
|
|
30
|
+
logger = get_logger(__name__)
|
|
31
|
+
|
|
32
|
+
_ESC_PREFIX_LEN = 2
|
|
33
|
+
_DOUBLE_CLICK_CHAIN = 2
|
|
34
|
+
_TRIPLE_CLICK_CHAIN = 3
|
|
35
|
+
_OPSCODE_WORD_SELECT_ACTIVE = "_opscloud_word_select_active"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# ── 1. Keyboard Parser Patch ──────────────────────────────────
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _install_keyboard_parser_patch() -> None:
|
|
42
|
+
"""Install terminal keyboard sequence decoding fixes."""
|
|
43
|
+
try:
|
|
44
|
+
from textual import events
|
|
45
|
+
from textual._ansi_sequences import ( # noqa: PLC2701
|
|
46
|
+
ANSI_SEQUENCES_KEYS,
|
|
47
|
+
IGNORE_SEQUENCE,
|
|
48
|
+
)
|
|
49
|
+
from textual._xterm_parser import XTermParser # noqa: PLC2701
|
|
50
|
+
|
|
51
|
+
original_sequence_to_key_events = XTermParser._sequence_to_key_events
|
|
52
|
+
except (ImportError, AttributeError) as exc:
|
|
53
|
+
logger.warning("Textual keyboard parser patch skipped: %s", exc)
|
|
54
|
+
return
|
|
55
|
+
|
|
56
|
+
kitty_lock_codes = frozenset({"57358", "57359", "57360"})
|
|
57
|
+
kitty_lock_names = {
|
|
58
|
+
"57358": "caps_lock",
|
|
59
|
+
"57359": "scroll_lock",
|
|
60
|
+
"57360": "num_lock",
|
|
61
|
+
}
|
|
62
|
+
kitty_key_sequence = re.compile(r"\x1b\[(\d+)[\d;:]*u")
|
|
63
|
+
kitty_subfield_key = re.compile(r"\x1b\[[\d;:]*:[\d;:]*[u~ABCDEFHPQRS]")
|
|
64
|
+
kitty_csi_u = re.compile(
|
|
65
|
+
r"\x1b\[(\d+)(?::\d+)*(?:;(\d+)[\d:]*)?(?:;(\d+)[\d:]*)?u"
|
|
66
|
+
)
|
|
67
|
+
ascii_upper_a = 65
|
|
68
|
+
ascii_upper_z = 90
|
|
69
|
+
real_modifier_mask = 0b111111
|
|
70
|
+
|
|
71
|
+
def is_spurious_caps_lock(sequence: str) -> bool:
|
|
72
|
+
match = kitty_csi_u.fullmatch(sequence)
|
|
73
|
+
if match is None:
|
|
74
|
+
return False
|
|
75
|
+
code = int(match.group(1))
|
|
76
|
+
if not ascii_upper_a <= code <= ascii_upper_z:
|
|
77
|
+
return False
|
|
78
|
+
modifier_bits = (int(match.group(2)) - 1) if match.group(2) else 0
|
|
79
|
+
has_text = match.group(3) is not None
|
|
80
|
+
return modifier_bits & real_modifier_mask == 0 and not has_text
|
|
81
|
+
|
|
82
|
+
def clean_kitty_subfields(sequence: str) -> str:
|
|
83
|
+
body, terminator = sequence[2:-1], sequence[-1]
|
|
84
|
+
fields = body.split(";")
|
|
85
|
+
fields[:2] = [field.split(":", 1)[0] for field in fields[:2]]
|
|
86
|
+
return f"\x1b[{';'.join(fields)}{terminator}"
|
|
87
|
+
|
|
88
|
+
def parse_lock_key_event(sequence: str) -> events.Key | None:
|
|
89
|
+
match = kitty_key_sequence.fullmatch(sequence)
|
|
90
|
+
if match is None or match.group(1) not in kitty_lock_codes:
|
|
91
|
+
return None
|
|
92
|
+
return events.Key(kitty_lock_names[match.group(1)], None)
|
|
93
|
+
|
|
94
|
+
def emit_alt_keys(keys: tuple, character: str | None) -> Iterable[events.Key]:
|
|
95
|
+
for key in keys:
|
|
96
|
+
yield events.Key(f"alt+{key.value}", character)
|
|
97
|
+
|
|
98
|
+
def sequence_to_key_events_patched(
|
|
99
|
+
parser_self: XTermParser, sequence: str, alt: bool = False
|
|
100
|
+
) -> Iterable[events.Key]:
|
|
101
|
+
if (lock_event := parse_lock_key_event(sequence)) is not None:
|
|
102
|
+
yield lock_event
|
|
103
|
+
return
|
|
104
|
+
if is_spurious_caps_lock(sequence):
|
|
105
|
+
yield events.Key("caps_lock", None)
|
|
106
|
+
return
|
|
107
|
+
if kitty_subfield_key.fullmatch(sequence):
|
|
108
|
+
sequence = clean_kitty_subfields(sequence)
|
|
109
|
+
|
|
110
|
+
# Fast path: \x1b<byte> on first pass
|
|
111
|
+
if not alt and len(sequence) == _ESC_PREFIX_LEN and sequence[0] == "\x1b":
|
|
112
|
+
inner = ANSI_SEQUENCES_KEYS.get(sequence[1])
|
|
113
|
+
if inner is not IGNORE_SEQUENCE and isinstance(inner, tuple):
|
|
114
|
+
yield from emit_alt_keys(inner, None)
|
|
115
|
+
return
|
|
116
|
+
|
|
117
|
+
# Preserve `alt` on reissue path for single-byte tuple mappings
|
|
118
|
+
if alt:
|
|
119
|
+
keys = ANSI_SEQUENCES_KEYS.get(sequence)
|
|
120
|
+
if keys is not IGNORE_SEQUENCE and isinstance(keys, tuple):
|
|
121
|
+
character = sequence if len(sequence) == 1 else None
|
|
122
|
+
yield from emit_alt_keys(keys, character)
|
|
123
|
+
return
|
|
124
|
+
|
|
125
|
+
yield from original_sequence_to_key_events(parser_self, sequence, alt=alt)
|
|
126
|
+
|
|
127
|
+
try:
|
|
128
|
+
setattr(XTermParser, "_sequence_to_key_events", sequence_to_key_events_patched)
|
|
129
|
+
except (AttributeError, TypeError) as exc:
|
|
130
|
+
logger.warning("Textual keyboard parser patch assignment rejected: %s", exc)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
# ── 2. Word Selection & Mouse Event Utilities ─────────────────
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _extract_rendered_text(widget: Widget) -> str | None:
|
|
137
|
+
visual = widget._render()
|
|
138
|
+
if isinstance(visual, (Content, Text)):
|
|
139
|
+
return str(visual)
|
|
140
|
+
return None
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _calculate_word_bounds(text: str, offset: Offset) -> tuple[Offset, Offset] | None:
|
|
144
|
+
lines = text.splitlines()
|
|
145
|
+
if not lines:
|
|
146
|
+
return None
|
|
147
|
+
|
|
148
|
+
y = min(max(offset.y, 0), len(lines) - 1)
|
|
149
|
+
line = lines[y]
|
|
150
|
+
if not line:
|
|
151
|
+
return None
|
|
152
|
+
|
|
153
|
+
x = min(max(offset.x, 0), len(line))
|
|
154
|
+
index = min(x, len(line) - 1)
|
|
155
|
+
if line[index].isspace():
|
|
156
|
+
if x == len(line) and x > 0 and not line[x - 1].isspace():
|
|
157
|
+
index = x - 1
|
|
158
|
+
else:
|
|
159
|
+
return None
|
|
160
|
+
|
|
161
|
+
start = index
|
|
162
|
+
while start > 0 and not line[start - 1].isspace():
|
|
163
|
+
start -= 1
|
|
164
|
+
|
|
165
|
+
end = index + 1
|
|
166
|
+
while end < len(line) and not line[end].isspace():
|
|
167
|
+
end += 1
|
|
168
|
+
|
|
169
|
+
return Offset(start, y), Offset(end, y)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _compute_word_selection(widget: Widget, selection: Selection) -> Selection | None:
|
|
173
|
+
if selection.start is None or selection.end is None:
|
|
174
|
+
return None
|
|
175
|
+
|
|
176
|
+
text = _extract_rendered_text(widget)
|
|
177
|
+
if text is None:
|
|
178
|
+
return None
|
|
179
|
+
|
|
180
|
+
start, end = selection.start, selection.end
|
|
181
|
+
if end.transpose < start.transpose:
|
|
182
|
+
start, end = end, start
|
|
183
|
+
|
|
184
|
+
start_bounds = _calculate_word_bounds(text, start)
|
|
185
|
+
end_bounds = _calculate_word_bounds(text, end)
|
|
186
|
+
if start_bounds is None and end_bounds is None:
|
|
187
|
+
return None
|
|
188
|
+
|
|
189
|
+
return Selection(
|
|
190
|
+
start_bounds[0] if start_bounds is not None else start,
|
|
191
|
+
end_bounds[1] if end_bounds is not None else end,
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _apply_word_selection_at_click(widget: Widget, event: Click) -> bool:
|
|
196
|
+
offset = event.get_content_offset(widget)
|
|
197
|
+
if offset is None:
|
|
198
|
+
return False
|
|
199
|
+
|
|
200
|
+
text = _extract_rendered_text(widget)
|
|
201
|
+
if text is None:
|
|
202
|
+
return False
|
|
203
|
+
|
|
204
|
+
bounds = _calculate_word_bounds(text, offset)
|
|
205
|
+
if bounds is None:
|
|
206
|
+
widget.screen.clear_selection()
|
|
207
|
+
return True
|
|
208
|
+
|
|
209
|
+
widget.screen.selections = {widget: Selection(*bounds)}
|
|
210
|
+
return True
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def _install_word_selection_patch() -> None:
|
|
214
|
+
"""Install mouse click-chain selection handling."""
|
|
215
|
+
try:
|
|
216
|
+
from textual import events as _events
|
|
217
|
+
from textual.screen import Screen as _Screen
|
|
218
|
+
from textual.widget import Widget as _Widget
|
|
219
|
+
|
|
220
|
+
original_forward_event = _Screen._forward_event
|
|
221
|
+
original_watch_select_state = _Screen._watch__select_state
|
|
222
|
+
original_widget_on_click = _Widget._on_click
|
|
223
|
+
except (ImportError, AttributeError) as exc:
|
|
224
|
+
logger.warning(
|
|
225
|
+
"Textual word-selection patch skipped (textual %s): %s",
|
|
226
|
+
_textual_version,
|
|
227
|
+
exc,
|
|
228
|
+
)
|
|
229
|
+
return
|
|
230
|
+
|
|
231
|
+
def is_word_select_initiator(screen: Screen, event: Event) -> bool:
|
|
232
|
+
if not isinstance(event, _events.MouseDown) or screen.app.mouse_captured:
|
|
233
|
+
return False
|
|
234
|
+
|
|
235
|
+
last_offset = getattr(screen.app, "_click_chain_last_offset", None)
|
|
236
|
+
last_time = getattr(screen.app, "_click_chain_last_time", None)
|
|
237
|
+
if last_offset != event.screen_offset or last_time is None:
|
|
238
|
+
return False
|
|
239
|
+
|
|
240
|
+
if event.time - last_time > screen.app.CLICK_CHAIN_TIME_THRESHOLD:
|
|
241
|
+
return False
|
|
242
|
+
|
|
243
|
+
select_widget, select_offset = screen.get_widget_and_offset_at(event.x, event.y)
|
|
244
|
+
return (
|
|
245
|
+
select_widget is not None
|
|
246
|
+
and select_widget.allow_select
|
|
247
|
+
and screen.allow_select
|
|
248
|
+
and screen.app.ALLOW_SELECT
|
|
249
|
+
and select_offset is not None
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
def forward_event_patched(self: Screen, event: Event) -> None:
|
|
253
|
+
if isinstance(event, _events.MouseDown):
|
|
254
|
+
setattr(
|
|
255
|
+
self,
|
|
256
|
+
_OPSCODE_WORD_SELECT_ACTIVE,
|
|
257
|
+
is_word_select_initiator(self, event),
|
|
258
|
+
)
|
|
259
|
+
try:
|
|
260
|
+
original_forward_event(self, event)
|
|
261
|
+
finally:
|
|
262
|
+
if isinstance(event, _events.MouseUp):
|
|
263
|
+
setattr(self, _OPSCODE_WORD_SELECT_ACTIVE, False)
|
|
264
|
+
|
|
265
|
+
async def watch_select_state_patched(
|
|
266
|
+
self: Screen,
|
|
267
|
+
select_state: SelectState | None,
|
|
268
|
+
) -> None:
|
|
269
|
+
result: Any = original_watch_select_state(self, select_state)
|
|
270
|
+
if isawaitable(result):
|
|
271
|
+
await cast(Any, result)
|
|
272
|
+
if not getattr(self, _OPSCODE_WORD_SELECT_ACTIVE, False):
|
|
273
|
+
return
|
|
274
|
+
|
|
275
|
+
selections = dict(self.selections)
|
|
276
|
+
changed = False
|
|
277
|
+
for widget, selection in selections.items():
|
|
278
|
+
word_selection = _compute_word_selection(widget, selection)
|
|
279
|
+
if word_selection is None or word_selection == selection:
|
|
280
|
+
continue
|
|
281
|
+
selections[widget] = word_selection
|
|
282
|
+
changed = True
|
|
283
|
+
|
|
284
|
+
if changed:
|
|
285
|
+
self.selections = selections
|
|
286
|
+
|
|
287
|
+
async def widget_on_click_patched(self: Widget, event: Click) -> None:
|
|
288
|
+
if (
|
|
289
|
+
event.widget is self
|
|
290
|
+
and self.allow_select
|
|
291
|
+
and self.screen.allow_select
|
|
292
|
+
and self.app.ALLOW_SELECT
|
|
293
|
+
):
|
|
294
|
+
if event.chain == _DOUBLE_CLICK_CHAIN and _apply_word_selection_at_click(
|
|
295
|
+
self, event
|
|
296
|
+
):
|
|
297
|
+
await self.broker_event("click", event)
|
|
298
|
+
return
|
|
299
|
+
if event.chain == _TRIPLE_CLICK_CHAIN:
|
|
300
|
+
self.text_select_all()
|
|
301
|
+
await self.broker_event("click", event)
|
|
302
|
+
return
|
|
303
|
+
|
|
304
|
+
await original_widget_on_click(self, event)
|
|
305
|
+
|
|
306
|
+
try:
|
|
307
|
+
setattr(_Screen, "_forward_event", forward_event_patched)
|
|
308
|
+
setattr(_Screen, "_watch__select_state", watch_select_state_patched)
|
|
309
|
+
setattr(_Widget, "_on_click", widget_on_click_patched)
|
|
310
|
+
except (AttributeError, TypeError) as exc:
|
|
311
|
+
logger.warning(
|
|
312
|
+
"Textual word-selection patch assignment rejected (textual %s): %s",
|
|
313
|
+
_textual_version,
|
|
314
|
+
exc,
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
# ── 3. Detached Widget Hit Testing Patch ──────────────────────
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def _install_detached_hit_patch() -> None:
|
|
322
|
+
"""Install guard against hit-testing unattached / unmounted widgets."""
|
|
323
|
+
try:
|
|
324
|
+
from textual.screen import Screen as _HitScreen
|
|
325
|
+
|
|
326
|
+
original_get_widget_and_offset_at = _HitScreen.get_widget_and_offset_at
|
|
327
|
+
except (ImportError, AttributeError) as exc:
|
|
328
|
+
logger.warning(
|
|
329
|
+
"Textual detached-hit patch skipped (textual %s): %s",
|
|
330
|
+
_textual_version,
|
|
331
|
+
exc,
|
|
332
|
+
)
|
|
333
|
+
return
|
|
334
|
+
|
|
335
|
+
def get_widget_and_offset_at_safe(
|
|
336
|
+
self: Screen,
|
|
337
|
+
x: int,
|
|
338
|
+
y: int,
|
|
339
|
+
) -> tuple[Widget | None, Offset | None]:
|
|
340
|
+
widget, offset = original_get_widget_and_offset_at(self, x, y)
|
|
341
|
+
if (
|
|
342
|
+
widget is not None
|
|
343
|
+
and not isinstance(widget, _HitScreen)
|
|
344
|
+
and (widget.parent is None or not widget.is_attached)
|
|
345
|
+
):
|
|
346
|
+
return None, None
|
|
347
|
+
return widget, offset
|
|
348
|
+
|
|
349
|
+
try:
|
|
350
|
+
setattr(_HitScreen, "get_widget_and_offset_at", get_widget_and_offset_at_safe)
|
|
351
|
+
except (AttributeError, TypeError) as exc:
|
|
352
|
+
logger.warning(
|
|
353
|
+
"Textual detached-hit patch assignment rejected (textual %s): %s",
|
|
354
|
+
_textual_version,
|
|
355
|
+
exc,
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def apply_textual_patches() -> None:
|
|
360
|
+
"""Apply all Textual compatibility enhancements."""
|
|
361
|
+
_install_keyboard_parser_patch()
|
|
362
|
+
_install_word_selection_patch()
|
|
363
|
+
_install_detached_hit_patch()
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
# Automatically apply patches when module is imported
|
|
367
|
+
apply_textual_patches()
|
opscloud/_version.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Version information and lightweight constants for opscloud."""
|
|
2
|
+
|
|
3
|
+
from importlib.metadata import PackageNotFoundError, version as _pkg_version
|
|
4
|
+
|
|
5
|
+
try:
|
|
6
|
+
__version__: str = _pkg_version("talkops-opscloud")
|
|
7
|
+
except PackageNotFoundError:
|
|
8
|
+
try:
|
|
9
|
+
__version__ = _pkg_version("opscloud")
|
|
10
|
+
except PackageNotFoundError:
|
|
11
|
+
__version__ = "0.1.0-dev"
|
|
12
|
+
|
|
13
|
+
DOCS_URL = "https://github.com/talkops-ai/opscloud"
|
|
14
|
+
CHANGELOG_URL = "https://github.com/talkops-ai/opscloud/blob/main/CHANGELOG.md"
|
|
15
|
+
USER_AGENT = f"opscloud/{__version__}"
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Agent construction package for OpsCloud."""
|
|
2
|
+
|
|
3
|
+
from opscloud.agent.config import (
|
|
4
|
+
AVAILABLE_AGENTS,
|
|
5
|
+
AgentContext,
|
|
6
|
+
AgentContextSchema,
|
|
7
|
+
CLIContextSchema,
|
|
8
|
+
get_available_agent_names,
|
|
9
|
+
load_default_agent,
|
|
10
|
+
save_default_agent,
|
|
11
|
+
)
|
|
12
|
+
from opscloud.agent.factory import (
|
|
13
|
+
create_agent,
|
|
14
|
+
create_opscloud_agent,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"AVAILABLE_AGENTS",
|
|
19
|
+
"AgentContext",
|
|
20
|
+
"AgentContextSchema",
|
|
21
|
+
"CLIContextSchema",
|
|
22
|
+
"create_agent",
|
|
23
|
+
"create_opscloud_agent",
|
|
24
|
+
"get_available_agent_names",
|
|
25
|
+
"load_default_agent",
|
|
26
|
+
"save_default_agent",
|
|
27
|
+
]
|
opscloud/agent/config.py
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"""Agent mode discovery, configuration, and default persistence for OpsCloud."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
import json
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from opscloud.config.paths import STATE_DIR as _STATE_DIR
|
|
10
|
+
from opscloud.config.toml_config import (
|
|
11
|
+
clear_default_agent as clear_default_agent,
|
|
12
|
+
load_default_agent as _toml_load_default_agent,
|
|
13
|
+
load_recent_agent as load_recent_agent,
|
|
14
|
+
save_default_agent as save_default_agent,
|
|
15
|
+
save_recent_agent as save_recent_agent,
|
|
16
|
+
)
|
|
17
|
+
from opscloud.utils.logger import get_logger
|
|
18
|
+
|
|
19
|
+
logger = get_logger(__name__)
|
|
20
|
+
|
|
21
|
+
_DEFAULT_AGENT_FILE = _STATE_DIR / "default_agent.json"
|
|
22
|
+
|
|
23
|
+
AVAILABLE_AGENTS: list[str] = [
|
|
24
|
+
"agent",
|
|
25
|
+
"conversation_history",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def get_available_agent_names() -> list[str]:
|
|
30
|
+
"""Return the list of available agent modes."""
|
|
31
|
+
return list(AVAILABLE_AGENTS)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def load_default_agent() -> str | None:
|
|
35
|
+
"""Load the persisted default agent name from config.toml or fallbacks."""
|
|
36
|
+
default_agent = _toml_load_default_agent()
|
|
37
|
+
if default_agent:
|
|
38
|
+
return default_agent
|
|
39
|
+
recent_agent = load_recent_agent()
|
|
40
|
+
if recent_agent:
|
|
41
|
+
return recent_agent
|
|
42
|
+
try:
|
|
43
|
+
if _DEFAULT_AGENT_FILE.exists():
|
|
44
|
+
data = json.loads(_DEFAULT_AGENT_FILE.read_text(encoding="utf-8"))
|
|
45
|
+
if isinstance(data, dict) and "agent" in data:
|
|
46
|
+
return str(data["agent"])
|
|
47
|
+
except Exception:
|
|
48
|
+
pass
|
|
49
|
+
return None
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass
|
|
53
|
+
class AgentContext:
|
|
54
|
+
"""Runtime context passed through the agent lifecycle."""
|
|
55
|
+
|
|
56
|
+
model: str = ""
|
|
57
|
+
thread_id: str = ""
|
|
58
|
+
turn_id: str = ""
|
|
59
|
+
approval_mode: str = "manual"
|
|
60
|
+
working_dir: str = "."
|
|
61
|
+
interactive: bool = True
|
|
62
|
+
auto_approve: bool = False
|
|
63
|
+
extra: dict[str, Any] = field(default_factory=dict)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@dataclass
|
|
67
|
+
class CLIContextSchema:
|
|
68
|
+
"""Standard context schema for the OpsCloud LangGraph execution graph."""
|
|
69
|
+
|
|
70
|
+
model: str | None = None
|
|
71
|
+
model_params: dict[str, Any] = field(default_factory=dict)
|
|
72
|
+
profile_overrides: dict[str, Any] = field(default_factory=dict)
|
|
73
|
+
model_context_limit: int | None = None
|
|
74
|
+
approval_mode: str = "manual"
|
|
75
|
+
auto_approve: bool = False
|
|
76
|
+
smart: bool = False
|
|
77
|
+
approval_mode_key: str | None = None
|
|
78
|
+
thread_id: str | None = None
|
|
79
|
+
turn_id: str | None = None
|
|
80
|
+
offload_tool_call_id: str | None = None
|
|
81
|
+
hooks_snapshot_id: str | None = None
|
|
82
|
+
hooks_server_events: list[str] = field(default_factory=list)
|
|
83
|
+
prompt_id: str | None = None
|
|
84
|
+
reasoning_effort: str | None = None
|
|
85
|
+
cwd: str | None = None
|
|
86
|
+
workspace: dict[str, Any] = field(default_factory=dict)
|
|
87
|
+
aws_profile: str | None = None
|
|
88
|
+
aws_region: str | None = None
|
|
89
|
+
|
|
90
|
+
@classmethod
|
|
91
|
+
def from_payload(cls, payload: object) -> CLIContextSchema | None:
|
|
92
|
+
"""Coerce a run's `context=` payload into this schema."""
|
|
93
|
+
if isinstance(payload, cls):
|
|
94
|
+
return payload
|
|
95
|
+
if not isinstance(payload, dict):
|
|
96
|
+
return None
|
|
97
|
+
data = dict(payload)
|
|
98
|
+
|
|
99
|
+
def _str(key: str) -> str | None:
|
|
100
|
+
val = data.get(key)
|
|
101
|
+
return val if isinstance(val, str) else None
|
|
102
|
+
|
|
103
|
+
def _mapping(key: str) -> dict[str, Any]:
|
|
104
|
+
val = data.get(key)
|
|
105
|
+
return dict(val) if isinstance(val, dict) else {}
|
|
106
|
+
|
|
107
|
+
raw_limit = data.get("model_context_limit")
|
|
108
|
+
limit = raw_limit if isinstance(raw_limit, int) and not isinstance(raw_limit, bool) else None
|
|
109
|
+
|
|
110
|
+
raw_events = data.get("hooks_server_events")
|
|
111
|
+
events = [e for e in raw_events if isinstance(e, str)] if isinstance(raw_events, list) else []
|
|
112
|
+
|
|
113
|
+
raw_auto = data.get("auto_approve")
|
|
114
|
+
auto_approve = raw_auto if isinstance(raw_auto, bool) else False
|
|
115
|
+
|
|
116
|
+
raw_mode = _str("approval_mode")
|
|
117
|
+
raw_smart = data.get("smart")
|
|
118
|
+
smart = raw_smart if isinstance(raw_smart, bool) else (raw_mode == "smart" if raw_mode else False)
|
|
119
|
+
if not raw_mode:
|
|
120
|
+
if smart:
|
|
121
|
+
raw_mode = "smart"
|
|
122
|
+
elif auto_approve:
|
|
123
|
+
raw_mode = "auto"
|
|
124
|
+
else:
|
|
125
|
+
raw_mode = "manual"
|
|
126
|
+
|
|
127
|
+
cwd = _str("cwd") or _str("working_dir")
|
|
128
|
+
workspace = _mapping("workspace")
|
|
129
|
+
if not cwd and workspace and isinstance(workspace.get("cwd"), str):
|
|
130
|
+
cwd = str(workspace["cwd"])
|
|
131
|
+
|
|
132
|
+
return cls(
|
|
133
|
+
model=_str("model"),
|
|
134
|
+
model_params=_mapping("model_params"),
|
|
135
|
+
profile_overrides=_mapping("profile_overrides"),
|
|
136
|
+
model_context_limit=limit,
|
|
137
|
+
approval_mode=raw_mode,
|
|
138
|
+
auto_approve=auto_approve,
|
|
139
|
+
smart=smart,
|
|
140
|
+
approval_mode_key=_str("approval_mode_key"),
|
|
141
|
+
thread_id=_str("thread_id"),
|
|
142
|
+
turn_id=_str("turn_id"),
|
|
143
|
+
offload_tool_call_id=_str("offload_tool_call_id"),
|
|
144
|
+
hooks_snapshot_id=_str("hooks_snapshot_id"),
|
|
145
|
+
hooks_server_events=events,
|
|
146
|
+
prompt_id=_str("prompt_id"),
|
|
147
|
+
reasoning_effort=_str("reasoning_effort"),
|
|
148
|
+
cwd=cwd,
|
|
149
|
+
workspace=workspace,
|
|
150
|
+
aws_profile=_str("aws_profile"),
|
|
151
|
+
aws_region=_str("aws_region"),
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
AgentContextSchema = CLIContextSchema
|
|
156
|
+
|
|
157
|
+
__all__ = [
|
|
158
|
+
"AVAILABLE_AGENTS",
|
|
159
|
+
"AgentContext",
|
|
160
|
+
"AgentContextSchema",
|
|
161
|
+
"CLIContextSchema",
|
|
162
|
+
"clear_default_agent",
|
|
163
|
+
"get_available_agent_names",
|
|
164
|
+
"load_default_agent",
|
|
165
|
+
"load_recent_agent",
|
|
166
|
+
"save_default_agent",
|
|
167
|
+
"save_recent_agent",
|
|
168
|
+
]
|