answer42 0.2.0__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.
@@ -0,0 +1,127 @@
1
+ """Install packaged Answer42 skills into popular agent skill directories."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import importlib.resources
7
+ import shutil
8
+ from pathlib import Path
9
+
10
+ PACKAGED_SKILLS = ("answer42", "answer42-rag")
11
+
12
+ AGENT_SKILL_DIRS = {
13
+ # OpenClaw workspace skills.
14
+ "openclaw": Path("~/.openclaw/workspace/skills"),
15
+ # Claude Code / Claude Desktop style user skills directory.
16
+ "claude": Path("~/.claude/skills"),
17
+ # Best-effort conventional locations for agents that do not expose a single
18
+ # portable skill standard yet. Users can always pass --target-dir explicitly.
19
+ "codex": Path("~/.codex/skills"),
20
+ "opencode": Path("~/.config/opencode/skills"),
21
+ "pi": Path("~/.pi/skills"),
22
+ "hermes": Path("~/.hermes/skills"),
23
+ }
24
+
25
+
26
+ def _copy_tree(src: Path, dst: Path, *, force: bool, dry_run: bool) -> str:
27
+ existed = dst.exists()
28
+ if existed:
29
+ if not force:
30
+ return "skipped-exists"
31
+ if not dry_run:
32
+ shutil.rmtree(dst)
33
+ if not dry_run:
34
+ dst.parent.mkdir(parents=True, exist_ok=True)
35
+ shutil.copytree(src, dst)
36
+ return "updated" if existed else "installed"
37
+
38
+
39
+ def install_skills(
40
+ *,
41
+ agents: list[str],
42
+ target_dir: Path | None,
43
+ skills: list[str],
44
+ force: bool,
45
+ dry_run: bool,
46
+ ) -> list[dict[str, str]]:
47
+ selected_skills = skills or list(PACKAGED_SKILLS)
48
+ unknown_skills = sorted(set(selected_skills) - set(PACKAGED_SKILLS))
49
+ if unknown_skills:
50
+ raise ValueError(f"Unknown packaged skill(s): {', '.join(unknown_skills)}")
51
+
52
+ targets: list[tuple[str, Path]] = []
53
+ if target_dir is not None:
54
+ targets.append(("custom", target_dir.expanduser()))
55
+ for agent in agents:
56
+ if agent == "all":
57
+ targets.extend((name, path.expanduser()) for name, path in AGENT_SKILL_DIRS.items())
58
+ continue
59
+ path = AGENT_SKILL_DIRS.get(agent)
60
+ if path is None:
61
+ raise ValueError(
62
+ f"Unknown agent {agent!r}. Known: {', '.join(sorted(AGENT_SKILL_DIRS))}, all"
63
+ )
64
+ targets.append((agent, path.expanduser()))
65
+ if not targets:
66
+ targets.append(("openclaw", AGENT_SKILL_DIRS["openclaw"].expanduser()))
67
+
68
+ results: list[dict[str, str]] = []
69
+ root = importlib.resources.files("mcp_1c.assets").joinpath("skills")
70
+ with importlib.resources.as_file(root) as root_path:
71
+ for skill in selected_skills:
72
+ src = root_path / skill
73
+ if not src.exists():
74
+ raise FileNotFoundError(f"Packaged skill not found: {skill}")
75
+ for agent, base_dir in targets:
76
+ dst = base_dir / skill
77
+ status = _copy_tree(src, dst, force=force, dry_run=dry_run)
78
+ results.append(
79
+ {
80
+ "agent": agent,
81
+ "skill": skill,
82
+ "path": str(dst),
83
+ "status": "would-" + status if dry_run else status,
84
+ }
85
+ )
86
+ return results
87
+
88
+
89
+ def main(argv: list[str] | None = None) -> int:
90
+ parser = argparse.ArgumentParser(description="Install packaged Answer42 skills")
91
+ parser.add_argument(
92
+ "--agent",
93
+ action="append",
94
+ choices=[*sorted(AGENT_SKILL_DIRS), "all"],
95
+ help="Agent preset to install into. Can be passed multiple times. Default: openclaw.",
96
+ )
97
+ parser.add_argument(
98
+ "--target-dir",
99
+ type=Path,
100
+ help="Custom skills directory. Use this for agents with non-standard or project-local paths.",
101
+ )
102
+ parser.add_argument(
103
+ "--skill",
104
+ action="append",
105
+ choices=list(PACKAGED_SKILLS),
106
+ help="Packaged skill to install. Can be passed multiple times. Default: all packaged skills.",
107
+ )
108
+ parser.add_argument("--no-overwrite", action="store_true", help="Skip skills that already exist.")
109
+ parser.add_argument("--dry-run", action="store_true", help="Print planned changes without writing files.")
110
+ parser.add_argument("--list-agents", action="store_true", help="Print known agent presets and exit.")
111
+ args = parser.parse_args(argv)
112
+
113
+ if args.list_agents:
114
+ for name, path in sorted(AGENT_SKILL_DIRS.items()):
115
+ print(f"{name}: {path.expanduser()}")
116
+ return 0
117
+
118
+ results = install_skills(
119
+ agents=args.agent or [],
120
+ target_dir=args.target_dir,
121
+ skills=args.skill or [],
122
+ force=not args.no_overwrite,
123
+ dry_run=args.dry_run,
124
+ )
125
+ for item in results:
126
+ print(f"{item['status']}: {item['agent']}:{item['skill']} -> {item['path']}")
127
+ return 0
@@ -0,0 +1,276 @@
1
+ from __future__ import annotations
2
+
3
+ import ctypes
4
+ import ctypes.wintypes
5
+ import os
6
+ import shutil
7
+ import subprocess
8
+ import time
9
+ from dataclasses import dataclass
10
+ from typing import Any
11
+
12
+
13
+ @dataclass
14
+ class WindowControlResult:
15
+ ok: bool
16
+ method: str
17
+ title: str = ""
18
+ geometry: dict[str, int] | None = None
19
+ candidates: list[dict[str, Any]] | None = None
20
+ error: str = ""
21
+
22
+ def as_dict(self) -> dict[str, Any]:
23
+ return {
24
+ "ok": self.ok,
25
+ "method": self.method,
26
+ "title": self.title,
27
+ "geometry": self.geometry or {},
28
+ "candidates": self.candidates or [],
29
+ "error": self.error,
30
+ }
31
+
32
+
33
+ def maximize_test_client_window(
34
+ title_contains: str = "",
35
+ display: str | None = None,
36
+ width: int | None = None,
37
+ height: int | None = None,
38
+ ) -> dict[str, Any]:
39
+ """Maximize/resize a 1C test-client window.
40
+
41
+ Priority: Windows WinAPI on Windows; wmctrl -> xdotool -> python-xlib on X11.
42
+ This function controls only window geometry; it doesn't click or type in the application.
43
+ """
44
+ if os.name == "nt":
45
+ return _maximize_with_winapi(title_contains, width, height).as_dict()
46
+
47
+ env = os.environ.copy()
48
+ if display:
49
+ env["DISPLAY"] = display
50
+ display = env.get("DISPLAY", "")
51
+
52
+ for fn in (_maximize_with_wmctrl, _maximize_with_xdotool, _maximize_with_xlib):
53
+ result = fn(title_contains=title_contains, display=display, width=width, height=height, env=env)
54
+ if result.ok:
55
+ return result.as_dict()
56
+ last = result
57
+ return last.as_dict()
58
+
59
+
60
+ def active_1c_window_geometry(title_contains: str = "") -> dict[str, int] | None:
61
+ """Return best visible 1C window geometry on Windows, or None elsewhere."""
62
+ if os.name != "nt":
63
+ return None
64
+ chosen = _find_windows_1c_window(title_contains)
65
+ if not chosen:
66
+ return None
67
+ return chosen.get("geometry")
68
+
69
+
70
+ def _screen_size(display: str, env: dict[str, str]) -> tuple[int, int]:
71
+ try:
72
+ out = subprocess.check_output(["xdpyinfo", "-display", display], env=env, text=True, stderr=subprocess.DEVNULL)
73
+ for line in out.splitlines():
74
+ if "dimensions:" in line:
75
+ part = line.split("dimensions:", 1)[1].strip().split()[0]
76
+ w, h = part.split("x")
77
+ return int(w), int(h)
78
+ except Exception:
79
+ pass
80
+ return 1920, 1080
81
+
82
+
83
+ def _maximize_with_wmctrl(title_contains: str, display: str, width: int | None, height: int | None, env: dict[str, str]) -> WindowControlResult:
84
+ if not shutil.which("wmctrl"):
85
+ return WindowControlResult(False, "wmctrl", error="wmctrl not installed")
86
+ try:
87
+ sw, sh = (width, height) if width and height else _screen_size(display, env)
88
+ out = subprocess.check_output(["wmctrl", "-l"], env=env, text=True)
89
+ candidates=[]
90
+ chosen=None
91
+ for line in out.splitlines():
92
+ parts=line.split(None, 3)
93
+ if len(parts) < 4:
94
+ continue
95
+ wid, _, _, title = parts
96
+ item={"id": wid, "title": title}
97
+ candidates.append(item)
98
+ if _title_matches(title, title_contains):
99
+ chosen=item
100
+ if chosen is None:
101
+ return WindowControlResult(False, "wmctrl", candidates=candidates, error="window not found")
102
+ wid=chosen["id"]
103
+ subprocess.check_call(["wmctrl", "-i", "-r", wid, "-b", "remove,maximized_vert,maximized_horz"], env=env)
104
+ subprocess.check_call(["wmctrl", "-i", "-r", wid, "-e", f"0,0,0,{sw},{sh}"], env=env)
105
+ subprocess.check_call(["wmctrl", "-i", "-r", wid, "-b", "add,maximized_vert,maximized_horz"], env=env)
106
+ return WindowControlResult(True, "wmctrl", title=chosen["title"], geometry={"x":0,"y":0,"width":sw,"height":sh}, candidates=candidates)
107
+ except Exception as e:
108
+ return WindowControlResult(False, "wmctrl", error=str(e))
109
+
110
+
111
+ def _maximize_with_xdotool(title_contains: str, display: str, width: int | None, height: int | None, env: dict[str, str]) -> WindowControlResult:
112
+ if not shutil.which("xdotool"):
113
+ return WindowControlResult(False, "xdotool", error="xdotool not installed")
114
+ try:
115
+ sw, sh = (width, height) if width and height else _screen_size(display, env)
116
+ pattern = title_contains or ".*"
117
+ ids = subprocess.check_output(["xdotool", "search", "--name", pattern], env=env, text=True).split()
118
+ if not ids:
119
+ return WindowControlResult(False, "xdotool", error="window not found")
120
+ wid=ids[-1]
121
+ title = subprocess.check_output(["xdotool", "getwindowname", wid], env=env, text=True).strip()
122
+ subprocess.check_call(["xdotool", "windowsize", wid, str(sw), str(sh)], env=env)
123
+ subprocess.check_call(["xdotool", "windowmove", wid, "0", "0"], env=env)
124
+ subprocess.check_call(["xdotool", "windowactivate", wid], env=env)
125
+ return WindowControlResult(True, "xdotool", title=title, geometry={"x":0,"y":0,"width":sw,"height":sh})
126
+ except Exception as e:
127
+ return WindowControlResult(False, "xdotool", error=str(e))
128
+
129
+
130
+ def _title_matches(title: str, title_contains: str) -> bool:
131
+ if title_contains and title_contains.lower() not in title.lower():
132
+ return False
133
+ if "Конфигурация" in title:
134
+ return False
135
+ return any(s in title for s in ["DEV-AREA", "Начальная", "Приложения", "1С:Предприятие", "ЭмФешн", "Время создания"])
136
+
137
+
138
+ def _maximize_with_xlib(title_contains: str, display: str, width: int | None, height: int | None, env: dict[str, str]) -> WindowControlResult:
139
+ try:
140
+ from Xlib import X, display as xdisplay
141
+ disp = xdisplay.Display(display or None)
142
+ root = disp.screen().root
143
+ sw = width or disp.screen().width_in_pixels
144
+ sh = height or disp.screen().height_in_pixels
145
+ candidates=[]
146
+ def walk(win):
147
+ yield win
148
+ try:
149
+ children = win.query_tree().children
150
+ except Exception:
151
+ return
152
+ for child in children:
153
+ yield from walk(child)
154
+ def get_name(win):
155
+ for atom_name in ("_NET_WM_NAME", "WM_NAME"):
156
+ try:
157
+ prop=win.get_full_property(disp.intern_atom(atom_name), X.AnyPropertyType)
158
+ if prop and prop.value is not None:
159
+ value=prop.value
160
+ if isinstance(value, bytes):
161
+ return value.decode("utf-8", "ignore")
162
+ try:
163
+ return bytes(value).decode("utf-8", "ignore")
164
+ except Exception:
165
+ return str(value)
166
+ except Exception:
167
+ pass
168
+ return ""
169
+ chosen=None
170
+ for win in walk(root):
171
+ title=get_name(win)
172
+ if not title:
173
+ continue
174
+ try:
175
+ geo=win.get_geometry()
176
+ except Exception:
177
+ continue
178
+ if geo.width < 300 or geo.height < 200:
179
+ continue
180
+ item={"title": title, "width": geo.width, "height": geo.height}
181
+ candidates.append(item)
182
+ if _title_matches(title, title_contains):
183
+ chosen=(win, item)
184
+ if chosen is None:
185
+ return WindowControlResult(False, "xlib", candidates=candidates, error="window not found")
186
+ win,item=chosen
187
+ win.configure(x=0, y=0, width=sw, height=sh, border_width=0, stack_mode=X.Above)
188
+ try:
189
+ win.set_input_focus(X.RevertToParent, X.CurrentTime)
190
+ except Exception:
191
+ pass
192
+ win.map()
193
+ disp.sync()
194
+ time.sleep(0.2)
195
+ return WindowControlResult(True, "xlib", title=item["title"], geometry={"x":0,"y":0,"width":sw,"height":sh}, candidates=candidates)
196
+ except Exception as e:
197
+ return WindowControlResult(False, "xlib", error=str(e))
198
+
199
+
200
+ def _find_windows_1c_window(title_contains: str = "") -> dict[str, Any] | None:
201
+ user32 = ctypes.windll.user32
202
+ candidates: list[dict[str, Any]] = []
203
+
204
+ EnumWindowsProc = ctypes.WINFUNCTYPE(ctypes.c_bool, ctypes.c_void_p, ctypes.c_void_p)
205
+
206
+ def callback(hwnd: int, _lparam: int) -> bool:
207
+ try:
208
+ if not user32.IsWindowVisible(hwnd):
209
+ return True
210
+ length = user32.GetWindowTextLengthW(hwnd)
211
+ if length <= 0:
212
+ return True
213
+ buf = ctypes.create_unicode_buffer(length + 1)
214
+ user32.GetWindowTextW(hwnd, buf, length + 1)
215
+ title = buf.value
216
+ if not _title_matches(title, title_contains):
217
+ return True
218
+ rect = ctypes.wintypes.RECT() # type: ignore[attr-defined]
219
+ if not user32.GetWindowRect(hwnd, ctypes.byref(rect)):
220
+ return True
221
+ width = int(rect.right - rect.left)
222
+ height = int(rect.bottom - rect.top)
223
+ if width < 300 or height < 150:
224
+ return True
225
+ priority = min(width * height // 10000, 50)
226
+ if "1С:Предприятие" in title:
227
+ priority += 50
228
+ if title and title not in {"1cv8c", "1cv8", "Конфигурация"}:
229
+ priority += 100
230
+ candidates.append({
231
+ "hwnd": hwnd,
232
+ "title": title,
233
+ "priority": priority,
234
+ "geometry": {"left": int(rect.left), "top": int(rect.top), "width": width, "height": height, "title": title},
235
+ })
236
+ except Exception:
237
+ pass
238
+ return True
239
+
240
+ try:
241
+ user32.EnumWindows(EnumWindowsProc(callback), 0)
242
+ except Exception:
243
+ return None
244
+ if not candidates:
245
+ return None
246
+ candidates.sort(key=lambda item: int(item.get("priority") or 0), reverse=True)
247
+ return candidates[0]
248
+
249
+
250
+ def _maximize_with_winapi(title_contains: str, width: int | None, height: int | None) -> WindowControlResult:
251
+ try:
252
+ import ctypes.wintypes # type: ignore # noqa: F401
253
+
254
+ user32 = ctypes.windll.user32
255
+ chosen = _find_windows_1c_window(title_contains)
256
+ if chosen is None:
257
+ return WindowControlResult(False, "winapi", error="window not found")
258
+ hwnd = int(chosen["hwnd"])
259
+ screen_w = width or user32.GetSystemMetrics(0) or 1920
260
+ screen_h = height or user32.GetSystemMetrics(1) or 1080
261
+ user32.ShowWindow(hwnd, 9) # SW_RESTORE
262
+ user32.MoveWindow(hwnd, 0, 0, int(screen_w), int(screen_h), True)
263
+ user32.ShowWindow(hwnd, 3) # SW_MAXIMIZE
264
+ try:
265
+ user32.SetForegroundWindow(hwnd)
266
+ except Exception:
267
+ pass
268
+ return WindowControlResult(
269
+ True,
270
+ "winapi",
271
+ title=str(chosen.get("title") or ""),
272
+ geometry={"x": 0, "y": 0, "width": int(screen_w), "height": int(screen_h)},
273
+ candidates=[{"title": str(chosen.get("title") or ""), "hwnd": hwnd}],
274
+ )
275
+ except Exception as e:
276
+ return WindowControlResult(False, "winapi", error=str(e))