From f07aaf8f636de7d1788767d79e90e19e725901dd Mon Sep 17 00:00:00 2001 From: Julian lechner Date: Thu, 17 Sep 2026 11:45:41 +0200 Subject: [PATCH] feat(qmd): add managed extension integration --- adapters/plugins.tsv | 1 + scripts/lib/managed-extensions.py | 31 +++++++++++++++++++++++++++--- scripts/lib/plugins.ps1 | 4 +++- scripts/lib/plugins.sh | 4 +++- scripts/test-managed-extensions.py | 16 ++++++++++++++- 5 files changed, 50 insertions(+), 6 deletions(-) diff --git a/adapters/plugins.tsv b/adapters/plugins.tsv index a920974..f5058ed 100644 --- a/adapters/plugins.tsv +++ b/adapters/plugins.tsv @@ -4,6 +4,7 @@ i-have-adhd ayghri/i-have-adhd i-have-adhd@i-have-adhd ayghri/i-have-adhd i-have Superpowers - superpowers@claude-plugins-official openai-curated-remote superpowers@openai-curated-remote plugin - - Context7 upstash/context7 context7@context7-marketplace upstash/context7 context7@context7-marketplace plugin - - Caveman JuliusBrussee/caveman caveman@caveman thinkhome-org/caveman-codex caveman@thinkhome-caveman plugin - - +QMD tobi/qmd qmd@qmd - - qmd @tobilu/qmd qmd Humanizer blader/humanizer humanizer@humanizer - - skill blader/humanizer humanizer Impeccable pbakaus/impeccable impeccable@impeccable pbakaus/impeccable impeccable skill pbakaus/impeccable impeccable Anthropic Frontend Design anthropics/claude-plugins-official frontend-design@claude-plugins-official - - skill anthropics/skills frontend-design diff --git a/scripts/lib/managed-extensions.py b/scripts/lib/managed-extensions.py index fe67c73..da31f90 100644 --- a/scripts/lib/managed-extensions.py +++ b/scripts/lib/managed-extensions.py @@ -166,7 +166,7 @@ class Manager: raise ValueError('Invalid extension ownership ledger.') identities = set() for resource in self.state['resources']: - if not isinstance(resource, dict) or resource.get('client') not in ('codex', 'claude') or resource.get('kind') not in ('plugin', 'skill'): + if not isinstance(resource, dict) or resource.get('client') not in ('codex', 'claude') or resource.get('kind') not in ('plugin', 'skill', 'qmd'): raise ValueError('Invalid extension ownership record.') if not isinstance(resource.get('name'), str) or not resource['name']: raise ValueError('Invalid extension ownership name.') @@ -179,7 +179,7 @@ class Manager: raise ValueError('Invalid owned plugin selector.') if 'source' in resource and not isinstance(resource['source'], str): raise ValueError('Invalid owned plugin source.') - else: + elif resource['kind'] == 'skill': directory = resource.get('directory', '') if not re.fullmatch(r'\.agents/skills/[A-Za-z0-9_-]+', directory) or resource['client'] != 'codex': raise ValueError('Invalid owned skill directory.') @@ -192,6 +192,8 @@ class Manager: if not relative.startswith(directory + '/') or not re.fullmatch(r'[a-f0-9]{64}', value): raise ValueError('Invalid owned skill file record.') safe_path(self.home, relative) + elif resource.get('package') != '@tobilu/qmd' or resource['client'] != 'codex': + raise ValueError('Invalid owned QMD record.') def save(self): if not self.dry_run: @@ -224,7 +226,7 @@ class Manager: arguments = [client, 'plugin', 'uninstall', selector, '--scope', 'user'] if client == 'claude' else [client, 'plugin', 'remove', selector] self.command(arguments) self.cache[client].pop(selector, None) - else: + elif record['kind'] == 'skill': empty_candidates = set() for relative, expected in list(record['files'].items()): path = safe_path(self.home, relative) @@ -254,10 +256,15 @@ class Manager: pass if record['files']: return + else: + self.command([record['client'], 'mcp', 'remove', 'qmd'] + (['--scope', 'user'] if record['client'] == 'claude' else [])) self.state['resources'].remove(record) self.save() def ensure(self, client, entry): + if entry['codex_method'] == 'qmd': + self.ensure_qmd(client, entry) + return skill = client == 'codex' and entry['codex_method'] != 'plugin' kind = 'skill' if skill else 'plugin' record = next((r for r in self.state['resources'] if r['client'] == client and r['name'] == entry['name']), None) @@ -319,6 +326,24 @@ class Manager: self.save() self.cache[client][selector] = {'scope': 'user'} + def ensure_qmd(self, client, entry): + package = entry['codex_source'] + if package != '@tobilu/qmd': + raise ValueError('Invalid QMD package.') + if not getattr(self, 'qmd_ready', False): + self.command(['npm', 'install', '--global', package]) + self.qmd_ready = True + if client == 'claude': + entry = dict(entry, codex_method='plugin') + self.ensure(client, entry) + return + record = next((r for r in self.state['resources'] if r['client'] == client and r['name'] == entry['name']), None) + if record is not None: + return + self.command(['codex', 'mcp', 'add', 'qmd', '--', 'qmd', 'mcp']) + self.state['resources'].append(dict(client='codex', name=entry['name'], kind='qmd', package=package)) + self.save() + def ensure_skill(self, entry, record): skill = entry['codex_skill'] if not re.fullmatch(r'[A-Za-z0-9_-]+', skill): diff --git a/scripts/lib/plugins.ps1 b/scripts/lib/plugins.ps1 index 531fa3f..1170c69 100644 --- a/scripts/lib/plugins.ps1 +++ b/scripts/lib/plugins.ps1 @@ -97,7 +97,9 @@ function Select-ConfiguredPlugins { $checked = @($toggleable | ForEach-Object { if ($Mode -eq 'Install') { $true; return } $selector = if ($referenceClient -eq 'Claude') { $_.claude_plugin } else { $_.codex_plugin } - if ($referenceClient -eq 'Codex' -and $_.codex_method -ne 'plugin') { + if ($referenceClient -eq 'Codex' -and $_.codex_method -eq 'qmd') { + $null -ne (Get-Command qmd -ErrorAction SilentlyContinue) + } elseif ($referenceClient -eq 'Codex' -and $_.codex_method -ne 'plugin') { (Test-Path -LiteralPath (Join-Path $HomePath ('.agents/skills/' + $_.codex_skill))) -or (Test-Path -LiteralPath (Join-Path $HomePath ('.codex/skills/' + $_.codex_skill))) } else { [bool](@($installed | Where-Object { $_.$idField -eq $selector }).Count) } }) diff --git a/scripts/lib/plugins.sh b/scripts/lib/plugins.sh index 6860345..489e780 100755 --- a/scripts/lib/plugins.sh +++ b/scripts/lib/plugins.sh @@ -109,7 +109,9 @@ select_configured_plugins() { continue fi if [ "$reference_client" = claude ]; then selector=$(plugin_field "$root" "$name" claude_plugin); else selector=$(plugin_field "$root" "$name" codex_plugin); fi - if [ "$reference_client" = codex ] && [ "$(plugin_field "$root" "$name" codex_method)" != plugin ]; then + if [ "$reference_client" = codex ] && [ "$(plugin_field "$root" "$name" codex_method)" = qmd ]; then + if command -v qmd >/dev/null 2>&1; then checked+=(true); else checked+=(false); fi + elif [ "$reference_client" = codex ] && [ "$(plugin_field "$root" "$name" codex_method)" != plugin ]; then local skill_name skill_name=$(plugin_field "$root" "$name" codex_skill) if [ -d "$HOME/.agents/skills/$skill_name" ] || [ -d "$HOME/.codex/skills/$skill_name" ]; then checked+=(true); else checked+=(false); fi diff --git a/scripts/test-managed-extensions.py b/scripts/test-managed-extensions.py index 8d1203e..9b220fd 100644 --- a/scripts/test-managed-extensions.py +++ b/scripts/test-managed-extensions.py @@ -40,7 +40,7 @@ class ManagedExtensionTests(unittest.TestCase): def command(self, args, summary, home=None): self.commands.append(args) - if args[2] in ('add', 'install') and args[1] == 'plugin': + if len(args) > 3 and args[2] in ('add', 'install') and args[1] == 'plugin': self.installed[args[0]][args[3]] = {'scope': 'user'} if args[2] in ('remove', 'uninstall'): self.installed[args[0]].pop(args[3], None) @@ -172,6 +172,20 @@ class ManagedExtensionTests(unittest.TestCase): self.manager(dry_run=True, summary=True).sync([plugin(), skill()], ['codex'], {'Example', 'Humanizer'}) self.assertEqual('EXTENSIONS: reconciliation complete (dry run)\n', output.getvalue()) + def test_qmd_installs_once_and_registers_codex_mcp(self): + entry = dict(name='QMD', codex_method='qmd', codex_source='@tobilu/qmd', codex_skill='qmd', codex_plugin='-', codex_marketplace='-', claude_plugin='qmd@qmd', claude_marketplace='tobi/qmd') + self.manager().sync([entry], ['codex'], {'QMD'}) + self.assertEqual([['npm', 'install', '--global', '@tobilu/qmd'], ['codex', 'mcp', 'add', 'qmd', '--', 'qmd', 'mcp']], self.commands) + self.manager().sync([], ['codex'], set()) + self.assertEqual(['codex', 'mcp', 'remove', 'qmd'], self.commands[-1]) + + def test_qmd_installs_once_for_both_clients(self): + entry = dict(name='QMD', codex_method='qmd', codex_source='@tobilu/qmd', codex_skill='qmd', codex_plugin='-', codex_marketplace='-', claude_plugin='qmd@qmd', claude_marketplace='tobi/qmd') + self.manager().sync([entry], ['claude', 'codex'], {'QMD'}) + self.assertEqual(1, self.commands.count(['npm', 'install', '--global', '@tobilu/qmd'])) + self.assertIn(['claude', 'plugin', 'install', 'qmd@qmd', '--scope', 'user'], self.commands) + self.assertIn(['codex', 'mcp', 'add', 'qmd', '--', 'qmd', 'mcp'], self.commands) + def test_foreign_empty_directory_survives_skill_removal(self): self.manager().sync([skill()], ['codex'], {'Humanizer'}) foreign = self.home / '.agents/skills/humanizer/foreign-empty'