Coverage for src / library_skills / installer.py: 100%
144 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-08-03 20:19 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-08-03 20:19 +0000
1import json 1gsthuviwx
2import os 1gsthuviwx
3import shutil 1gsthuviwx
4from dataclasses import dataclass 1gsthuviwx
5from importlib import metadata, resources 1gsthuviwx
6from pathlib import Path 1gsthuviwx
8from .scanner import Skill 1gsthuviwx
10UNIVERSAL_SKILLS_DIR = ".agents/skills" 1gsthuviwx
11CLAUDE_SKILLS_DIR = ".claude/skills" 1gsthuviwx
12TOOL_SKILL_NAME = "library-skills" 1gsthuviwx
13TOOL_SKILL_MARKER = ".library-skills.json" 1gsthuviwx
14TOOL_SKILL_KIND = "tool-skill" 1gsthuviwx
17@dataclass(frozen=True) 1gsthuviwx
18class InstallTarget: 1gsthuviwx
19 """A project-level skill installation target."""
21 name: str 1ghi
22 path: Path 1ghi
25@dataclass(frozen=True) 1gsthuviwx
26class InstalledSkill: 1gsthuviwx
27 """A skill directory found in an installation target."""
29 name: str 1ghi
30 type: str 1ghi
31 path: Path 1ghi
32 target: Path | None = None 1gsthuviwx
33 has_skill_md: bool = False 1gsthuviwx
36@dataclass(frozen=True) 1gsthuviwx
37class ToolSkillStatus: 1gsthuviwx
38 """Status for the copied Library Skills tool skill in a target."""
40 target: InstallTarget 1ghi
41 path: Path 1ghi
42 status: str 1ghi
45def get_target_dirs( 1gsthuviwx
46 project_root: Path, *, include_claude: bool = False
47) -> list[InstallTarget]:
48 """Get project-level target directories.
50 The universal .agents/skills target is always included. Claude-compatible
51 installs are additional and never replace the universal target.
52 """
53 targets = [ 29b!b] - #by p d j T : m 6bW X Y LbRb~b^ _ ac` bc; 8 9 ! { ccDca tb5 K B ub^bGcvbwb$b%b| . 'bz q e k U = n 7bZ 0 1 MbSbdc} ~ ecabfc? # $ % bbgcEcb xb6 L C yb_bHczbAb(b)bcb/ *bA r f l V @ o 8b2 3 4 NbTbhcdbebicfbjc[ ' ( ) gbkcFcc Bb7 M D Cb`bIcDbEb
54 InstallTarget(name="universal", path=project_root / UNIVERSAL_SKILLS_DIR)
55 ]
56 if include_claude: 29b!b] - #by p d j T : m 6bW X Y LbRb~b^ _ ac` bc; 8 9 ! { ccDca tb5 K B ub^bGcvbwb$b%b| . 'bz q e k U = n 7bZ 0 1 MbSbdc} ~ ecabfc? # $ % bbgcEcb xb6 L C yb_bHczbAb(b)bcb/ *bA r f l V @ o 8b2 3 4 NbTbhcdbebicfbjc[ ' ( ) gbkcFcc Bb7 M D Cb`bIcDbEb
57 targets.append( 29b!b] - #by p d j T : m 6bW X Y Rb~b^ _ ac` bc; 8 9 ! { ccDca tb5 K B ub^bGcvbwb$b%b| . 'bz q e k U = n 7bZ 0 1 Sbdc} ~ ecabfc? # $ % bbgcEcb xb6 L C yb_bHczbAb(b)bcb/ *bA r f l V @ o 8b2 3 4 Tbhcdbebicfbjc[ ' ( ) gbkcFcc Bb7 M D Cb`bIcDbEb
58 InstallTarget(
59 name="claude-compatible", path=project_root / CLAUDE_SKILLS_DIR
60 )
61 )
62 return targets 29b!b] - #by p d j T : m 6bW X Y LbRb~b^ _ ac` bc; 8 9 ! { ccDca tb5 K B ub^bGcvbwb$b%b| . 'bz q e k U = n 7bZ 0 1 MbSbdc} ~ ecabfc? # $ % bbgcEcb xb6 L C yb_bHczbAb(b)bcb/ *bA r f l V @ o 8b2 3 4 NbTbhcdbebicfbjc[ ' ( ) gbkcFcc Bb7 M D Cb`bIcDbEb
65def get_all_target_dirs(project_root: Path) -> list[InstallTarget]: 1gsthuviwx
66 """Get all supported project-level target directories."""
67 return get_target_dirs(project_root, include_claude=True) 29b!b] - #by p d j T : m W X Y Rb~b^ _ ac` bc; 8 9 ! { ccDca 5 K B ub^bFbGb$b%b| . 'bz q e k U = n Z 0 1 Sbdc} ~ ecabfc? # $ % bbgcEcb 6 L C yb_bHbIb(b)bcb/ *bA r f l V @ o 2 3 4 Tbhcdbebicfbjc[ ' ( ) gbkcFcc 7 M D Cb`bJbKb
70def get_default_install_target_dirs(project_root: Path) -> list[InstallTarget]: 1gsthuviwx
71 """Get interactive install target defaults from project state.
73 Parent directories are enough to signal that the project uses a target. When
74 neither target is present, prefer the universal .agents/skills target.
75 """
76 targets = get_all_target_dirs(project_root) 2y p d j T : m W X Y Rb; a ubE F z q e k U = n Z 0 1 Sb? b ybG H A r f l V @ o 2 3 4 Tb[ c CbI J
77 by_name = {target.name: target for target in targets} 2y p d j T : m W X Y Rb; a ubE F z q e k U = n Z 0 1 Sb? b ybG H A r f l V @ o 2 3 4 Tb[ c CbI J
78 selected: list[InstallTarget] = [] 2y p d j T : m W X Y Rb; a ubE F z q e k U = n Z 0 1 Sb? b ybG H A r f l V @ o 2 3 4 Tb[ c CbI J
80 if (project_root / ".agents").exists(): 2y p d j T : m W X Y Rb; a ubE F z q e k U = n Z 0 1 Sb? b ybG H A r f l V @ o 2 3 4 Tb[ c CbI J
81 selected.append(by_name["universal"]) 2p j m a ubE F q k n b ybG H r l o c CbI J
82 if (project_root / ".claude").exists(): 2y p d j T : m W X Y Rb; a ubE F z q e k U = n Z 0 1 Sb? b ybG H A r f l V @ o 2 3 4 Tb[ c CbI J
83 selected.append(by_name["claude-compatible"]) 2p ubadbdq ybcdddr Cbedfd
85 if not selected: 2y p d j T : m W X Y Rb; a ubE F z q e k U = n Z 0 1 Sb? b ybG H A r f l V @ o 2 3 4 Tb[ c CbI J
86 selected.append(by_name["universal"]) 2y d T : W X Y Rb; a ubE F z e U = Z 0 1 Sb? b ybG H A f V @ 2 3 4 Tb[ c CbI J
87 return selected 2y p d j T : m W X Y Rb; a ubE F z q e k U = n Z 0 1 Sb? b ybG H A r f l V @ o 2 3 4 Tb[ c CbI J
90def get_existing_target_dirs( 1gsthuviwx
91 project_root: Path, *, include_claude: bool = False
92) -> list[InstallTarget]:
93 """Get targets to inspect for existing installed skills.
95 Without explicit Claude compatibility, this only returns concrete skills
96 directories that already exist. With include_claude, preserve the legacy
97 explicit behavior of managing both known targets.
98 """
99 if include_claude: 29b!b] - #by p d j T : m ~b^ _ ac` bc; 8 9 ! { cca 5 K B ^bFbGb$b%b| . 'bz q e k U = n dc} ~ ecabfc? # $ % bbgcb 6 L C _bHbIb(b)bcb/ *bA r f l V @ o hcdbebicfbjc[ ' ( ) gbkcc 7 M D `bJbKb
100 return get_target_dirs(project_root, include_claude=True) 2^bgdhd_bidjd`bkdld
101 return [ 29b!b] - #by p d j T : m ~b^ _ ac` bc; 8 9 ! { cca 5 K B ^bFbGb$b%b| . 'bz q e k U = n dc} ~ ecabfc? # $ % bbgcb 6 L C _bHbIb(b)bcb/ *bA r f l V @ o hcdbebicfbjc[ ' ( ) gbkcc 7 M D `bJbKb
102 target for target in get_all_target_dirs(project_root) if target.path.is_dir()
103 ]
106class InstallError(Exception): 1gsthuviwx
107 """Raised when a skill cannot be installed safely."""
110class ToolSkillError(Exception): 1gsthuviwx
111 """Raised when the tool skill cannot be managed safely."""
114def install_skill( 1gsthuviwx
115 skill: Skill,
116 target_dir: Path,
117 *,
118 copy: bool = False,
119) -> Path:
120 """Install a single skill to the target directory.
122 Returns the path to the installed skill directory.
123 """
124 dest = target_dir / skill.name 2y p d 6bW X Y lcLbtbK B mc* ncucUbXbnbvbwbz q e 7bZ 0 1 ocMbxbL C pc+ qcvcVbYbobzbAbA r f 8b2 3 4 rcNbBbM D sc, tcwcWbZbpbDbEb
125 source = skill.skill_dir.resolve() 2y p d 6bW X Y lcLbtbK B mc* ncucUbXbnbvbwbz q e 7bZ 0 1 ocMbxbL C pc+ qcvcVbYbobzbAbA r f 8b2 3 4 rcNbBbM D sc, tcwcWbZbpbDbEb
127 if dest.exists() or dest.is_symlink(): 2y p d 6bW X Y lcLbtbK B mc* ncucUbXbnbvbwbz q e 7bZ 0 1 ocMbxbL C pc+ qcvcVbYbobzbAbA r f 8b2 3 4 rcNbBbM D sc, tcwcWbZbpbDbEb
128 if dest.is_symlink(): 2lcK B ncucUbFbGbocL C qcvcVbHbIbrcM D tcwcWbJbKb
129 dest.unlink() 2K B UbFbGbL C VbHbIbM D WbJbKb
130 elif dest.is_file(): 2lcncucYcZcocqcvc0c1crctcwc2c3c
131 raise InstallError(f"Cannot overwrite non-symlink file: {dest}") 2ucmdndvcodpdwcqdrd
132 elif dest.is_dir(): 2lcncYcZcocqc0c1crctc2c3c
133 raise InstallError(f"Cannot overwrite non-symlink directory: {dest}") 2lcncYcZcocqc0c1crctc2c3c
135 dest.parent.mkdir(parents=True, exist_ok=True) 2y p d 6bW X Y LbtbK B mc* UbXbnbvbwbz q e 7bZ 0 1 MbxbL C pc+ VbYbobzbAbA r f 8b2 3 4 NbBbM D sc, WbZbpbDbEb
137 if copy: 2y p d 6bW X Y LbtbK B mc* UbXbnbvbwbz q e 7bZ 0 1 MbxbL C pc+ VbYbobzbAbA r f 8b2 3 4 NbBbM D sc, WbZbpbDbEb
138 shutil.copytree(source, dest) 2y 6bmc4c5cz 7bpc6c7cA 8bsc8c9c
139 else:
140 link_target = _get_symlink_target(source=source, dest=dest) 2p d W X Y LbtbK B * UbXbnbvbwbq e Z 0 1 MbxbL C + VbYbobzbAbr f 2 3 4 NbBbM D , WbZbpbDbEb
141 try: 2p d W X Y LbtbK B * UbXbnbvbwbq e Z 0 1 MbxbL C + VbYbobzbAbr f 2 3 4 NbBbM D , WbZbpbDbEb
142 dest.symlink_to(link_target, target_is_directory=True) 2p d W X Y LbtbK B * UbXbnbvbwbq e Z 0 1 MbxbL C + VbYbobzbAbr f 2 3 4 NbBbM D , WbZbpbDbEb
143 except OSError as e: 2Xb!c#cYb$c%cZb'c(c
144 raise InstallError( 2Xb!c#cYb$c%cZb'c(c
145 f"Could not create symlink: {e}. "
146 "Use --copy if your system does not support symlinks."
147 ) from e
149 return dest 2y p d 6bW X Y LbtbK B mc* Ubnb4cvb5cwbz q e 7bZ 0 1 MbxbL C pc+ Vbob6czb7cAbA r f 8b2 3 4 NbBbM D sc, Wbpb8cDb9cEb
152def _get_symlink_target(*, source: Path, dest: Path) -> Path | str: 1gsthuviwx
153 try: 2p d W X Y LbtbK B Jc)c* UbXbnbvbwbq e Z 0 1 MbxbL C Kc*c+ VbYbobzbAbr f 2 3 4 NbBbM D Lc+c, WbZbpbDbEb
154 return os.path.relpath(source, start=dest.parent.resolve()) 2p d W X Y LbtbK B Jc)c* UbXbnbvbwbq e Z 0 1 MbxbL C Kc*c+ VbYbobzbAbr f 2 3 4 NbBbM D Lc+c, WbZbpbDbEb
155 except ValueError: 2Jc,c-cKc.c/cLc:c;c
156 return source 2Jc,c-cKc.c/cLc:c;c
159def uninstall_skill(skill_name: str, target_dir: Path) -> bool: 1gsthuviwx
160 """Remove an installed symlink. Returns True if something was removed."""
161 dest = target_dir / skill_name 28 9 ! 5 nbMcxcyc# $ % 6 obNczcAc' ( ) 7 pbOcBcCc
162 if dest.is_symlink(): 28 9 ! 5 nbMcxcyc# $ % 6 obNczcAc' ( ) 7 pbOcBcCc
163 dest.unlink() 28 9 ! 5 nbMcxcyc# $ % 6 obNczcAc' ( ) 7 pbOcBcCc
164 return True 28 9 ! 5 nbMcxcyc# $ % 6 obNczcAc' ( ) 7 pbOcBcCc
165 return False 2nbsdtdobudvdpbwdxd
168def get_tool_skill_template() -> str: 1gsthuviwx
169 """Return the bundled Library Skills tool skill template."""
170 return ( 1djaQNEFekbROGHflcSPIJ
171 resources.files("library_skills.tool_skill")
172 .joinpath("SKILL.md")
173 .read_text(encoding="utf-8")
174 )
177def get_tool_skill_version() -> str: 1gsthuviwx
178 """Return the installed library-skills package version for marker metadata."""
179 try: 2d j a Q N PcE F e k b R O QcG H f l c S P RcI J
180 return metadata.version("library-skills") 2d j a Q N PcE F e k b R O QcG H f l c S P RcI J
181 except metadata.PackageNotFoundError: 2Pc=c?cQc@c[cRc]c^c
182 return "0.0.0" 2Pc=c?cQc@c[cRc]c^c
185def inspect_tool_skill(target_dir: Path) -> ToolSkillStatus: 1gsthuviwx
186 """Inspect the copied tool skill status for a target directory."""
187 target = _target_for_path(target_dir) 2- y p d j T m a {b.bhbObQ ibqbN /b:b. z q e k U n b |b;bjbPbR kbrbO =b?b/ A r f l V o c }b@blbQbS mbsbP [b]b
188 dest = target_dir / TOOL_SKILL_NAME 2- y p d j T m a {b.bhbObQ ibqbN /b:b. z q e k U n b |b;bjbPbR kbrbO =b?b/ A r f l V o c }b@blbQbS mbsbP [b]b
189 marker = dest / TOOL_SKILL_MARKER 2- y p d j T m a {b.bhbObQ ibqbN /b:b. z q e k U n b |b;bjbPbR kbrbO =b?b/ A r f l V o c }b@blbQbS mbsbP [b]b
190 skill_md = dest / "SKILL.md" 2- y p d j T m a {b.bhbObQ ibqbN /b:b. z q e k U n b |b;bjbPbR kbrbO =b?b/ A r f l V o c }b@blbQbS mbsbP [b]b
192 if not dest.exists() and not dest.is_symlink(): 2- y p d j T m a {b.bhbObQ ibqbN /b:b. z q e k U n b |b;bjbPbR kbrbO =b?b/ A r f l V o c }b@blbQbS mbsbP [b]b
193 return ToolSkillStatus(target=target, path=dest, status="tool skill: missing") 2- y p d j T a {bQ N /b:b. z q e k U b |bR O =b?b/ A r f l V c }bS P [b]b
195 if dest.is_symlink() or not dest.is_dir(): 2m a .bhbObQ ibqbN E F n b ;bjbPbR kbrbO G H o c @blbQbS mbsbP I J
196 return ToolSkillStatus( 2.bydzd;bAdBd@bCdDd
197 target=target,
198 path=dest,
199 status="tool skill: blocked by hand-authored directory",
200 )
202 if not _is_managed_tool_skill_marker(marker): 2m a hbObQ ibqbN E F n b jbPbR kbrbO G H o c lbQbS mbsbP I J
203 return ToolSkillStatus( 2m ibqbScEdTcFdn kbrbUcGdVcHdo mbsbWcIdXcJd
204 target=target,
205 path=dest,
206 status="tool skill: invalid marker"
207 if marker.exists()
208 else "tool skill: blocked by hand-authored directory",
209 )
211 if not skill_md.is_file(): 2a hbObQ N E F b jbPbR O G H c lbQbS P I J
212 return ToolSkillStatus(target=target, path=dest, status="tool skill: stale") 2ObKdLdPbMdNdQbOdPd
214 try: 2a hbQ N E F b jbR O G H c lbS P I J
215 current = skill_md.read_text(encoding="utf-8") 2a hbQ N E F b jbR O G H c lbS P I J
216 except (OSError, UnicodeDecodeError): 2hb_c`cjb{c|clb}c~c
217 return ToolSkillStatus(target=target, path=dest, status="tool skill: stale") 2hb_c`cjb{c|clb}c~c
219 status = ( 1aQNEFbROGHcSPIJ
220 "tool skill: up to date"
221 if current == get_tool_skill_template()
222 else "tool skill: stale"
223 )
224 return ToolSkillStatus(target=target, path=dest, status=status) 1aQNEFbROGHcSPIJ
227def install_tool_skill(target_dir: Path) -> Path: 1gsthuviwx
228 """Install or update the copied Library Skills tool skill in a target."""
229 status = inspect_tool_skill(target_dir) 2d j m a Q ibqbN E F e k n b R kbrbO G H f l o c S mbsbP I J
230 if status.status in { 2d j m a Q ibqbN E F e k n b R kbrbO G H f l o c S mbsbP I J
231 "tool skill: blocked by hand-authored directory",
232 "tool skill: invalid marker",
233 }:
234 raise ToolSkillError(f"Cannot overwrite {status.status}: {status.path}") 2m ibqbScTcn kbrbUcVco mbsbWcXc
236 dest = target_dir / TOOL_SKILL_NAME 1djaQNEFekbROGHflcSPIJ
237 if dest.exists(): 1djaQNEFekbROGHflcSPIJ
238 shutil.rmtree(dest) 1aNEFbOGHcPIJ
239 dest.mkdir(parents=True, exist_ok=True) 1djaQNEFekbROGHflcSPIJ
240 (dest / "SKILL.md").write_text(get_tool_skill_template(), encoding="utf-8") 1djaQNEFekbROGHflcSPIJ
241 (dest / TOOL_SKILL_MARKER).write_text( 1djaQNEFekbROGHflcSPIJ
242 json.dumps(
243 {"kind": TOOL_SKILL_KIND, "version": get_tool_skill_version()},
244 indent=2,
245 )
246 + "\n",
247 encoding="utf-8",
248 )
249 return dest 1djaQNEFekbROGHflcSPIJ
252def _target_for_path(target_dir: Path) -> InstallTarget: 1gsthuviwx
253 if target_dir.as_posix().endswith(CLAUDE_SKILLS_DIR): 2- y p d j T m a {b.bhbObQ ibqbN /b:b. z q e k U n b |b;bjbPbR kbrbO =b?b/ A r f l V o c }b@blbQbS mbsbP [b]b
254 return InstallTarget(name="claude-compatible", path=target_dir) 2- {b/b:b. |b=b?b/ }b[b]b
255 return InstallTarget(name="universal", path=target_dir) 2- y p d j T m a .bhbObQ ibqbN /b:b. z q e k U n b ;bjbPbR kbrbO =b?b/ A r f l V o c @blbQbS mbsbP [b]b
258def _is_managed_tool_skill_marker(marker: Path) -> bool: 1gsthuviwx
259 try: 2m a hbObQ ibqbN E F n b jbPbR kbrbO G H o c lbQbS mbsbP I J
260 data = json.loads(marker.read_text(encoding="utf-8")) 2m a hbObQ ibqbN E F n b jbPbR kbrbO G H o c lbQbS mbsbP I J
261 except (OSError, UnicodeDecodeError, json.JSONDecodeError): 2m ibScTcn kbUcVco mbWcXc
262 return False 2m ibScTcn kbUcVco mbWcXc
263 return isinstance(data, dict) and data.get("kind") == TOOL_SKILL_KIND 2a hbObQ qbN E F b jbPbR rbO G H c lbQbS sbP I J
266def list_installed_skills(target_dir: Path) -> list[InstalledSkill]: 1gsthuviwx
267 """List skills currently installed in the target directory."""
268 results: list[InstalledSkill] = [] 29b!b] - #by p d j T : m 0b^ _ ` ; 8 9 ! { a 5 K B * 1b+bFbGb$b%b| . 'bz q e k U = n 2b} ~ ab? # $ % bbb 6 L C + 3b,bHbIb(b)bcb/ *bA r f l V @ o 4bdbebfb[ ' ( ) gbc 7 M D , 5b-bJbKb
270 if not target_dir.is_dir(): 29b!b] - #by p d j T : m 0b^ _ ` ; 8 9 ! { a 5 K B * 1b+bFbGb$b%b| . 'bz q e k U = n 2b} ~ ab? # $ % bbb 6 L C + 3b,bHbIb(b)bcb/ *bA r f l V @ o 4bdbebfb[ ' ( ) gbc 7 M D , 5b-bJbKb
271 return results 29b!b- #by p d T : ; a B 1bQdRd$b%b. 'bz q e U = ? b C 3bSdTd(b)b/ *bA r f V @ [ c D 5bUdVd
273 for child in sorted(target_dir.iterdir()): 2] j m 0b^ _ ` 8 9 ! { a 5 K B * 1b+bFbGb| k n 2b} ~ ab# $ % bbb 6 L C + 3b,bHbIbcbl o 4bdbebfb' ( ) gbc 7 M D , 5b-bJbKb
274 if not child.is_dir() and not child.is_symlink(): 2] j m 0b^ _ ` 8 9 ! { a 5 K B * 1b+bFbGb| k n 2b} ~ ab# $ % bbb 6 L C + 3b,bHbIbcbl o 4bdbebfb' ( ) gbc 7 M D , 5b-bJbKb
275 continue 21bWdXd3bYdZd5b0d1d
276 skill_md = child / "SKILL.md" 2] j m 0b^ _ ` 8 9 ! { a 5 K B * 1b+bFbGb| k n 2b} ~ ab# $ % bbb 6 L C + 3b,bHbIbcbl o 4bdbebfb' ( ) gbc 7 M D , 5b-bJbKb
277 if child.is_symlink(): 2] j m 0b^ _ ` 8 9 ! { a 5 K B * 1b+bFbGb| k n 2b} ~ ab# $ % bbb 6 L C + 3b,bHbIbcbl o 4bdbebfb' ( ) gbc 7 M D , 5b-bJbKb
278 install_type = "symlink" 2] j 0b^ _ ` 8 9 ! { 5 K B * +bFbGb| k 2b} ~ ab# $ % bb6 L C + ,bHbIbcbl 4bdbebfb' ( ) gb7 M D , -bJbKb
279 target = child.resolve() 2] j 0b^ _ ` 8 9 ! { 5 K B * +bFbGb| k 2b} ~ ab# $ % bb6 L C + ,bHbIbcbl 4bdbebfb' ( ) gb7 M D , -bJbKb
280 else:
281 install_type = "directory" 2m 0ba 5 1bxcycn 2bb 6 3bzcAco 4bc 7 5bBcCc
282 target = None 2m 0ba 5 1bxcycn 2bb 6 3bzcAco 4bc 7 5bBcCc
284 results.append( 2] j m 0b^ _ ` 8 9 ! { a 5 K B * 1b+bFbGb| k n 2b} ~ ab# $ % bbb 6 L C + 3b,bHbIbcbl o 4bdbebfb' ( ) gbc 7 M D , 5b-bJbKb
285 InstalledSkill(
286 name=child.name,
287 type=install_type,
288 path=child,
289 target=target,
290 has_skill_md=skill_md.is_file(),
291 )
292 )
294 return results 2] j m 0b^ _ ` 8 9 ! { a 5 K B * 1b+bFbGb| k n 2b} ~ ab# $ % bbb 6 L C + 3b,bHbIbcbl o 4bdbebfb' ( ) gbc 7 M D , 5b-bJbKb