feat(qmd): add managed extension integration
This commit is contained in:
1 parent
1b64c54fda
commit
f07aaf8f63
5 files changed
+50
-6
No files matched your search
@@ -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
|
||||
|
@@ -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):
|
||||
|
||||
@@ -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) }
|
||||
})
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'
|
||||
|
||||
Reference in new issue
Block a user