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.
Files changed (309) hide show
  1. opscloud/__init__.py +5 -0
  2. opscloud/__main__.py +6 -0
  3. opscloud/_constants.py +53 -0
  4. opscloud/_debug.py +145 -0
  5. opscloud/_debug_buffer.py +250 -0
  6. opscloud/_textual_patches.py +367 -0
  7. opscloud/_version.py +15 -0
  8. opscloud/agent/__init__.py +27 -0
  9. opscloud/agent/config.py +168 -0
  10. opscloud/agent/factory.py +1103 -0
  11. opscloud/approval_mode.py +44 -0
  12. opscloud/backend/__init__.py +49 -0
  13. opscloud/backend/composite.py +89 -0
  14. opscloud/backend/local.py +183 -0
  15. opscloud/backend/registry.py +79 -0
  16. opscloud/backend/sandbox_config.py +107 -0
  17. opscloud/backend/sandbox_factory.py +432 -0
  18. opscloud/built_in_skills/remember/SKILL.md +124 -0
  19. opscloud/built_in_skills/skill-creator/SKILL.md +348 -0
  20. opscloud/built_in_skills/skill-creator/scripts/init_skill.py +371 -0
  21. opscloud/built_in_skills/skill-creator/scripts/quick_validate.py +165 -0
  22. opscloud/cli/__init__.py +1 -0
  23. opscloud/cli/commands/__init__.py +15 -0
  24. opscloud/cli/commands/auth.py +45 -0
  25. opscloud/cli/commands/doctor.py +100 -0
  26. opscloud/cli/commands/mcp.py +137 -0
  27. opscloud/cli/commands/threads.py +51 -0
  28. opscloud/cli/interactive.py +154 -0
  29. opscloud/cli/main.py +903 -0
  30. opscloud/cli/non_interactive.py +425 -0
  31. opscloud/cli/server_manager.py +58 -0
  32. opscloud/commands/__init__.py +19 -0
  33. opscloud/commands/_base.py +110 -0
  34. opscloud/commands/_guards.py +52 -0
  35. opscloud/commands/_router.py +146 -0
  36. opscloud/commands/_types.py +36 -0
  37. opscloud/commands/core/__init__.py +47 -0
  38. opscloud/commands/core/auth.py +94 -0
  39. opscloud/commands/core/bug.py +46 -0
  40. opscloud/commands/core/clear.py +106 -0
  41. opscloud/commands/core/cloud.py +185 -0
  42. opscloud/commands/core/compact.py +128 -0
  43. opscloud/commands/core/config_cmd.py +120 -0
  44. opscloud/commands/core/context.py +220 -0
  45. opscloud/commands/core/cost.py +44 -0
  46. opscloud/commands/core/doctor.py +535 -0
  47. opscloud/commands/core/effort.py +108 -0
  48. opscloud/commands/core/exit_cmd.py +41 -0
  49. opscloud/commands/core/fast.py +148 -0
  50. opscloud/commands/core/help_cmd.py +94 -0
  51. opscloud/commands/core/mcp.py +134 -0
  52. opscloud/commands/core/model.py +109 -0
  53. opscloud/commands/core/permissions.py +196 -0
  54. opscloud/commands/core/plugins.py +92 -0
  55. opscloud/commands/core/pool.py +108 -0
  56. opscloud/commands/core/resume.py +97 -0
  57. opscloud/commands/core/skills.py +178 -0
  58. opscloud/commands/power/__init__.py +51 -0
  59. opscloud/commands/power/agents.py +98 -0
  60. opscloud/commands/power/btw.py +85 -0
  61. opscloud/commands/power/copy.py +97 -0
  62. opscloud/commands/power/goal.py +548 -0
  63. opscloud/commands/power/loop.py +295 -0
  64. opscloud/commands/power/memory.py +287 -0
  65. opscloud/commands/power/review.py +152 -0
  66. opscloud/commands/power/rubric.py +287 -0
  67. opscloud/commands/power/runtime.py +321 -0
  68. opscloud/commands/power/skill_creator.py +70 -0
  69. opscloud/commands/power/skill_invoke.py +165 -0
  70. opscloud/commands/power/tasks.py +209 -0
  71. opscloud/commands/power/trace.py +117 -0
  72. opscloud/commands/power/ui_toggles.py +135 -0
  73. opscloud/commands/power/version.py +52 -0
  74. opscloud/commands/registry.py +176 -0
  75. opscloud/config/__init__.py +114 -0
  76. opscloud/config/adapters/__init__.py +5 -0
  77. opscloud/config/adapters/postgres.py +776 -0
  78. opscloud/config/adapters/sqlite.py +769 -0
  79. opscloud/config/aws.py +248 -0
  80. opscloud/config/cloud_profiles.py +437 -0
  81. opscloud/config/env_vars.py +81 -0
  82. opscloud/config/langsmith.py +319 -0
  83. opscloud/config/manifest.py +817 -0
  84. opscloud/config/metadata.py +205 -0
  85. opscloud/config/paths.py +604 -0
  86. opscloud/config/plugins.py +130 -0
  87. opscloud/config/settings.py +834 -0
  88. opscloud/config/store.py +453 -0
  89. opscloud/config/store_factory.py +97 -0
  90. opscloud/config/toml_config.py +562 -0
  91. opscloud/editor.py +173 -0
  92. opscloud/exceptions.py +65 -0
  93. opscloud/file_ops.py +217 -0
  94. opscloud/hooks/__init__.py +139 -0
  95. opscloud/hooks/env.py +44 -0
  96. opscloud/hooks/interrupt.py +121 -0
  97. opscloud/hooks/models/__init__.py +109 -0
  98. opscloud/hooks/models/adapters.py +19 -0
  99. opscloud/hooks/models/config.py +79 -0
  100. opscloud/hooks/models/domain.py +434 -0
  101. opscloud/hooks/models/transport.py +37 -0
  102. opscloud/hooks/tools.py +231 -0
  103. opscloud/input.py +48 -0
  104. opscloud/integrations/__init__.py +80 -0
  105. opscloud/integrations/base.py +153 -0
  106. opscloud/integrations/content_filter.py +119 -0
  107. opscloud/integrations/event_bus.py +480 -0
  108. opscloud/integrations/hooks.py +238 -0
  109. opscloud/integrations/identity.py +181 -0
  110. opscloud/integrations/notifications.py +134 -0
  111. opscloud/integrations/stream_bridge.py +397 -0
  112. opscloud/integrations/thread_store.py +81 -0
  113. opscloud/json_types.py +20 -0
  114. opscloud/mcp/__init__.py +57 -0
  115. opscloud/mcp/config.py +203 -0
  116. opscloud/mcp/discovery.py +229 -0
  117. opscloud/mcp/mcp_info.py +109 -0
  118. opscloud/mcp/middleware.py +102 -0
  119. opscloud/mcp/preload.py +505 -0
  120. opscloud/mcp/raw_config.py +149 -0
  121. opscloud/mcp/semantic_profiler.py +305 -0
  122. opscloud/mcp/session_manager.py +727 -0
  123. opscloud/mcp/trust.py +105 -0
  124. opscloud/memory/__init__.py +27 -0
  125. opscloud/memory/branch.py +61 -0
  126. opscloud/memory/guard.py +19 -0
  127. opscloud/memory/onboarding.py +120 -0
  128. opscloud/memory/registry.py +134 -0
  129. opscloud/memory/store.py +223 -0
  130. opscloud/middleware/__init__.py +106 -0
  131. opscloud/middleware/_repository_bounds.py +459 -0
  132. opscloud/middleware/ask_user.py +152 -0
  133. opscloud/middleware/auto_mode.py +60 -0
  134. opscloud/middleware/auto_mode_hitl.py +2434 -0
  135. opscloud/middleware/compaction.py +566 -0
  136. opscloud/middleware/configurable_model.py +233 -0
  137. opscloud/middleware/cost_tracking.py +1021 -0
  138. opscloud/middleware/glm_stall_recovery.py +109 -0
  139. opscloud/middleware/goal_criteria.py +1234 -0
  140. opscloud/middleware/goal_state_notice.py +712 -0
  141. opscloud/middleware/goal_tools.py +407 -0
  142. opscloud/middleware/headless_mcp_guard.py +88 -0
  143. opscloud/middleware/jev_model_router.py +1018 -0
  144. opscloud/middleware/local_context.py +460 -0
  145. opscloud/middleware/mcp_context.py +151 -0
  146. opscloud/middleware/mcp_middleware.py +146 -0
  147. opscloud/middleware/memory_guard.py +393 -0
  148. opscloud/middleware/model_retry.py +205 -0
  149. opscloud/middleware/registry.py +137 -0
  150. opscloud/middleware/reliable_rubric.py +709 -0
  151. opscloud/middleware/resume_state.py +91 -0
  152. opscloud/middleware/server_hooks.py +1189 -0
  153. opscloud/middleware/shell_allow_list.py +90 -0
  154. opscloud/middleware/skills.py +382 -0
  155. opscloud/middleware/subagent_artifacts.py +84 -0
  156. opscloud/middleware/subagent_telemetry.py +261 -0
  157. opscloud/middleware/subagents.py +243 -0
  158. opscloud/middleware/tool_filter.py +384 -0
  159. opscloud/middleware/unified_system_message.py +62 -0
  160. opscloud/model/__init__.py +37 -0
  161. opscloud/model/config.py +2992 -0
  162. opscloud/model/factory.py +477 -0
  163. opscloud/model/pool.py +459 -0
  164. opscloud/model/reasoning.py +282 -0
  165. opscloud/offload.py +173 -0
  166. opscloud/output.py +64 -0
  167. opscloud/plugins/__init__.py +189 -0
  168. opscloud/plugins/_json.py +22 -0
  169. opscloud/plugins/adapters/__init__.py +13 -0
  170. opscloud/plugins/adapters/agents.py +112 -0
  171. opscloud/plugins/adapters/commands.py +190 -0
  172. opscloud/plugins/adapters/mcp.py +363 -0
  173. opscloud/plugins/adapters/skills.py +132 -0
  174. opscloud/plugins/commands_cli.py +541 -0
  175. opscloud/plugins/discovery.py +512 -0
  176. opscloud/plugins/manifest.py +335 -0
  177. opscloud/plugins/marketplace.py +893 -0
  178. opscloud/plugins/models.py +334 -0
  179. opscloud/plugins/project_plugins.py +317 -0
  180. opscloud/plugins/protocol.py +31 -0
  181. opscloud/plugins/store.py +905 -0
  182. opscloud/plugins/substitution.py +122 -0
  183. opscloud/project_utils.py +145 -0
  184. opscloud/prompts/__init__.py +37 -0
  185. opscloud/prompts/system.py +483 -0
  186. opscloud/prompts/templates/system_prompt.md +166 -0
  187. opscloud/prompts/types.py +54 -0
  188. opscloud/rubrics/__init__.py +42 -0
  189. opscloud/rubrics/evaluator.py +404 -0
  190. opscloud/rubrics/evidence_extractor.py +229 -0
  191. opscloud/rubrics/generator.py +239 -0
  192. opscloud/rubrics/jev_compiler.py +175 -0
  193. opscloud/rubrics/jev_grader.py +297 -0
  194. opscloud/rubrics/middleware.py +8 -0
  195. opscloud/schema/__init__.py +107 -0
  196. opscloud/schema/interrupts.py +981 -0
  197. opscloud/security/__init__.py +91 -0
  198. opscloud/security/approval_mode.py +528 -0
  199. opscloud/security/approval_mode_source.py +230 -0
  200. opscloud/security/cli_ast_evaluator.py +552 -0
  201. opscloud/security/jev_classifier.py +651 -0
  202. opscloud/security/shell_safety.py +340 -0
  203. opscloud/security/unicode_security.py +439 -0
  204. opscloud/security/url_validation.py +160 -0
  205. opscloud/server/__init__.py +13 -0
  206. opscloud/server/_server_config.py +158 -0
  207. opscloud/server/server.py +242 -0
  208. opscloud/server/server_graph.py +93 -0
  209. opscloud/skills/__init__.py +91 -0
  210. opscloud/skills/commands.py +1099 -0
  211. opscloud/skills/invocation.py +200 -0
  212. opscloud/skills/load.py +388 -0
  213. opscloud/skills/loader.py +23 -0
  214. opscloud/skills/merge.py +65 -0
  215. opscloud/skills/registry.py +263 -0
  216. opscloud/skills/sources.py +174 -0
  217. opscloud/skills/trust.py +417 -0
  218. opscloud/state/__init__.py +142 -0
  219. opscloud/state/base.py +154 -0
  220. opscloud/state/cloud_context.py +262 -0
  221. opscloud/state/goal_channels.py +124 -0
  222. opscloud/state/goal_state_limits.py +175 -0
  223. opscloud/state/resume_state.py +58 -0
  224. opscloud/state/service_context.py +281 -0
  225. opscloud/state/session.py +957 -0
  226. opscloud/state/state_migration.py +86 -0
  227. opscloud/subagents/__init__.py +30 -0
  228. opscloud/subagents/loader.py +331 -0
  229. opscloud/subagents/subagents_parser.py +117 -0
  230. opscloud/subagents/types.py +25 -0
  231. opscloud/tools/__init__.py +36 -0
  232. opscloud/tools/catalog.py +37 -0
  233. opscloud/tools/display.py +24 -0
  234. opscloud/tools/fetch_url.py +176 -0
  235. opscloud/tools/goal_tools.py +423 -0
  236. opscloud/tools/registry.py +105 -0
  237. opscloud/tools/thread.py +32 -0
  238. opscloud/tools/web_search.py +215 -0
  239. opscloud/ui/__init__.py +119 -0
  240. opscloud/ui/app.py +5400 -0
  241. opscloud/ui/app.tcss +479 -0
  242. opscloud/ui/clipboard.py +147 -0
  243. opscloud/ui/command_registry.py +482 -0
  244. opscloud/ui/message_store.py +152 -0
  245. opscloud/ui/permission_store.py +360 -0
  246. opscloud/ui/preferences.py +113 -0
  247. opscloud/ui/prompt_manager.py +96 -0
  248. opscloud/ui/remote_client.py +505 -0
  249. opscloud/ui/textual_adapter.py +1269 -0
  250. opscloud/ui/theme.py +728 -0
  251. opscloud/ui/ui_help.py +415 -0
  252. opscloud/ui/widgets/__init__.py +383 -0
  253. opscloud/ui/widgets/_ask_user_types.py +81 -0
  254. opscloud/ui/widgets/_copy_spans.py +52 -0
  255. opscloud/ui/widgets/_inline_prompt.py +290 -0
  256. opscloud/ui/widgets/_js_eval_display.py +139 -0
  257. opscloud/ui/widgets/_links.py +175 -0
  258. opscloud/ui/widgets/_tool_stream.py +308 -0
  259. opscloud/ui/widgets/agent_selector.py +297 -0
  260. opscloud/ui/widgets/approval.py +741 -0
  261. opscloud/ui/widgets/ask_user.py +539 -0
  262. opscloud/ui/widgets/auth.py +695 -0
  263. opscloud/ui/widgets/auth_manager.py +214 -0
  264. opscloud/ui/widgets/auto_mode_notice.py +146 -0
  265. opscloud/ui/widgets/autocomplete.py +709 -0
  266. opscloud/ui/widgets/chat_input.py +805 -0
  267. opscloud/ui/widgets/cloud_selector.py +528 -0
  268. opscloud/ui/widgets/config_manager.py +652 -0
  269. opscloud/ui/widgets/debug_console.py +1269 -0
  270. opscloud/ui/widgets/devops_renderers.py +184 -0
  271. opscloud/ui/widgets/diff.py +237 -0
  272. opscloud/ui/widgets/effort_selector.py +166 -0
  273. opscloud/ui/widgets/goal_review.py +612 -0
  274. opscloud/ui/widgets/goal_status.py +57 -0
  275. opscloud/ui/widgets/infra_panel.py +141 -0
  276. opscloud/ui/widgets/install_confirm.py +114 -0
  277. opscloud/ui/widgets/loading.py +170 -0
  278. opscloud/ui/widgets/mcp_viewer.py +1204 -0
  279. opscloud/ui/widgets/messages.py +4461 -0
  280. opscloud/ui/widgets/model_selector.py +863 -0
  281. opscloud/ui/widgets/notification_center.py +108 -0
  282. opscloud/ui/widgets/notification_settings.py +119 -0
  283. opscloud/ui/widgets/operation_card.py +106 -0
  284. opscloud/ui/widgets/permissions_manager.py +667 -0
  285. opscloud/ui/widgets/plugin_manager.py +1212 -0
  286. opscloud/ui/widgets/pool_selector.py +376 -0
  287. opscloud/ui/widgets/preamble.py +53 -0
  288. opscloud/ui/widgets/skills_viewer.py +432 -0
  289. opscloud/ui/widgets/status.py +1037 -0
  290. opscloud/ui/widgets/subagent_panel.py +927 -0
  291. opscloud/ui/widgets/theme_selector.py +232 -0
  292. opscloud/ui/widgets/thread_selector.py +358 -0
  293. opscloud/ui/widgets/toast.py +31 -0
  294. opscloud/ui/widgets/tool_display.py +325 -0
  295. opscloud/ui/widgets/tool_renderers.py +234 -0
  296. opscloud/ui/widgets/tool_widgets.py +278 -0
  297. opscloud/ui/widgets/welcome.py +421 -0
  298. opscloud/ui/widgets/welcome_popup.py +102 -0
  299. opscloud/utils/__init__.py +24 -0
  300. opscloud/utils/cost_estimation.py +169 -0
  301. opscloud/utils/git.py +390 -0
  302. opscloud/utils/logger.py +571 -0
  303. opscloud/utils/session_stats.py +115 -0
  304. opscloud/utils/startup_error.py +37 -0
  305. talkops_opscloud-0.3.0b1.dist-info/METADATA +449 -0
  306. talkops_opscloud-0.3.0b1.dist-info/RECORD +309 -0
  307. talkops_opscloud-0.3.0b1.dist-info/WHEEL +4 -0
  308. talkops_opscloud-0.3.0b1.dist-info/entry_points.txt +2 -0
  309. 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
+ ]
@@ -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
+ ]