arkaos 2.88.0 → 2.90.0
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.
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
2.
|
|
1
|
+
2.90.0
|
|
@@ -84,9 +84,21 @@ const links = [[{
|
|
|
84
84
|
class="bg-elevated/25"
|
|
85
85
|
>
|
|
86
86
|
<template #header="{ collapsed }">
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
87
|
+
<!-- PR72 v2.90.0 — global light/dark switch in the sidebar
|
|
88
|
+
header. Nuxt UI's canonical UColorModeButton (per
|
|
89
|
+
/websites/ui_nuxt docs) flips between sun/moon icons and
|
|
90
|
+
handles SSR via ClientOnly internally. Visible on every
|
|
91
|
+
page; the Settings → Theme section keeps the explicit
|
|
92
|
+
3-way picker (system / light / dark). -->
|
|
93
|
+
<div
|
|
94
|
+
class="flex items-center w-full"
|
|
95
|
+
:class="collapsed ? 'justify-center' : 'justify-between gap-2'"
|
|
96
|
+
>
|
|
97
|
+
<div class="flex items-center gap-2">
|
|
98
|
+
<span class="text-xl font-bold text-primary">A</span>
|
|
99
|
+
<span v-if="!collapsed" class="font-semibold">ArkaOS</span>
|
|
100
|
+
</div>
|
|
101
|
+
<UColorModeButton v-if="!collapsed" size="xs" />
|
|
90
102
|
</div>
|
|
91
103
|
</template>
|
|
92
104
|
|
|
@@ -141,14 +141,87 @@ const keyOptions = [
|
|
|
141
141
|
{ label: 'Custom...', value: 'custom' },
|
|
142
142
|
]
|
|
143
143
|
|
|
144
|
+
// ─── PR63b v2.89.0 — MCPs / Hooks / Plugins / Theme sections ────────────
|
|
145
|
+
|
|
146
|
+
interface McpRow {
|
|
147
|
+
name: string
|
|
148
|
+
source: 'user-global' | 'arkaos-registry' | string
|
|
149
|
+
transport: string
|
|
150
|
+
command: string
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
interface HookCommand {
|
|
154
|
+
command: string
|
|
155
|
+
type: string
|
|
156
|
+
timeout?: number | null
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
interface HookRow {
|
|
160
|
+
hook: string
|
|
161
|
+
count: number
|
|
162
|
+
commands: HookCommand[]
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
interface PluginRow {
|
|
166
|
+
name: string
|
|
167
|
+
marketplace: string
|
|
168
|
+
version: string
|
|
169
|
+
scope: string
|
|
170
|
+
installed_at: string
|
|
171
|
+
last_updated: string
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const { data: mcpsData, refresh: refreshMcps } = fetchApi<{ mcps: McpRow[], total: number }>('/api/settings/mcps')
|
|
175
|
+
const { data: hooksData, refresh: refreshHooks } = fetchApi<{
|
|
176
|
+
hooks: HookRow[]
|
|
177
|
+
settings_path: string
|
|
178
|
+
hard_enforcement: boolean
|
|
179
|
+
}>('/api/settings/hooks')
|
|
180
|
+
const { data: pluginsData, refresh: refreshPlugins } = fetchApi<{
|
|
181
|
+
plugins: PluginRow[]
|
|
182
|
+
total: number
|
|
183
|
+
plugins_path: string
|
|
184
|
+
}>('/api/settings/plugins')
|
|
185
|
+
|
|
186
|
+
const mcps = computed(() => mcpsData.value?.mcps ?? [])
|
|
187
|
+
const hooks = computed(() => hooksData.value?.hooks ?? [])
|
|
188
|
+
const plugins = computed(() => pluginsData.value?.plugins ?? [])
|
|
189
|
+
|
|
190
|
+
// Theme — Nuxt UI ships useColorMode; we just expose a picker.
|
|
191
|
+
const colorMode = useColorMode()
|
|
192
|
+
const themeOptions = [
|
|
193
|
+
{ label: 'System (auto)', value: 'system' },
|
|
194
|
+
{ label: 'Light', value: 'light' },
|
|
195
|
+
{ label: 'Dark', value: 'dark' },
|
|
196
|
+
]
|
|
197
|
+
|
|
198
|
+
function transportColor(transport: string): 'primary' | 'warning' | 'success' | 'neutral' {
|
|
199
|
+
if (transport === 'stdio') return 'primary'
|
|
200
|
+
if (transport === 'http' || transport === 'sse') return 'success'
|
|
201
|
+
return 'neutral'
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function formatInstalledAt(iso: string): string {
|
|
205
|
+
if (!iso) return ''
|
|
206
|
+
try {
|
|
207
|
+
return new Intl.DateTimeFormat('en-US', { month: 'short', day: 'numeric', year: 'numeric' }).format(new Date(iso))
|
|
208
|
+
} catch {
|
|
209
|
+
return iso
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
144
213
|
// ─── Section nav ────────────────────────────────────────────────────────
|
|
145
214
|
|
|
146
|
-
type SectionId = 'profile' | 'projects' | 'keys'
|
|
215
|
+
type SectionId = 'profile' | 'projects' | 'keys' | 'mcps' | 'hooks' | 'plugins' | 'theme'
|
|
147
216
|
|
|
148
217
|
const sections: { id: SectionId; label: string; icon: string }[] = [
|
|
149
|
-
{ id: 'profile',
|
|
150
|
-
{ id: 'projects', label: 'Projects',
|
|
151
|
-
{ id: 'keys',
|
|
218
|
+
{ id: 'profile', label: 'Profile', icon: 'i-lucide-user-circle' },
|
|
219
|
+
{ id: 'projects', label: 'Projects', icon: 'i-lucide-folders' },
|
|
220
|
+
{ id: 'keys', label: 'API Keys', icon: 'i-lucide-key' },
|
|
221
|
+
{ id: 'mcps', label: 'MCPs', icon: 'i-lucide-plug-2' },
|
|
222
|
+
{ id: 'hooks', label: 'Hooks', icon: 'i-lucide-webhook' },
|
|
223
|
+
{ id: 'plugins', label: 'Plugins', icon: 'i-lucide-puzzle' },
|
|
224
|
+
{ id: 'theme', label: 'Theme', icon: 'i-lucide-palette' },
|
|
152
225
|
]
|
|
153
226
|
|
|
154
227
|
const activeSection = ref<SectionId>('profile')
|
|
@@ -182,7 +255,8 @@ const activeSection = ref<SectionId>('profile')
|
|
|
182
255
|
<span>{{ s.label }}</span>
|
|
183
256
|
</button>
|
|
184
257
|
<p class="text-xs text-muted px-3 mt-6">
|
|
185
|
-
|
|
258
|
+
7 sections. Profile + Projects edit data; everything else is
|
|
259
|
+
read-only diagnostics until an explicit edit endpoint lands.
|
|
186
260
|
</p>
|
|
187
261
|
</nav>
|
|
188
262
|
|
|
@@ -409,6 +483,183 @@ const activeSection = ref<SectionId>('profile')
|
|
|
409
483
|
</div>
|
|
410
484
|
</DashboardState>
|
|
411
485
|
</section>
|
|
486
|
+
|
|
487
|
+
<!-- MCPs -->
|
|
488
|
+
<section v-else-if="activeSection === 'mcps'">
|
|
489
|
+
<div class="flex items-baseline justify-between mb-1">
|
|
490
|
+
<h2 class="text-lg font-semibold">MCPs</h2>
|
|
491
|
+
<UButton
|
|
492
|
+
label="Refresh"
|
|
493
|
+
variant="ghost"
|
|
494
|
+
icon="i-lucide-refresh-cw"
|
|
495
|
+
size="xs"
|
|
496
|
+
@click="refreshMcps()"
|
|
497
|
+
/>
|
|
498
|
+
</div>
|
|
499
|
+
<p class="text-sm text-muted mb-6">
|
|
500
|
+
MCP servers configured globally for your Claude Code account.
|
|
501
|
+
Sourced from <code class="font-mono text-xs">~/.claude.json</code>
|
|
502
|
+
and the ArkaOS registry. Read-only.
|
|
503
|
+
</p>
|
|
504
|
+
<div v-if="!mcps.length" class="rounded-lg border border-default p-6 text-center">
|
|
505
|
+
<UIcon name="i-lucide-plug-2" class="size-10 text-muted mx-auto mb-2" />
|
|
506
|
+
<p class="text-sm text-muted">No MCP servers configured.</p>
|
|
507
|
+
</div>
|
|
508
|
+
<div v-else class="space-y-2">
|
|
509
|
+
<div
|
|
510
|
+
v-for="m in mcps"
|
|
511
|
+
:key="`${m.source}:${m.name}`"
|
|
512
|
+
class="flex items-center gap-3 rounded-lg border border-default p-3"
|
|
513
|
+
>
|
|
514
|
+
<UIcon name="i-lucide-plug-2" class="size-4 text-muted shrink-0" />
|
|
515
|
+
<div class="flex-1 min-w-0">
|
|
516
|
+
<div class="flex items-center gap-2 mb-0.5">
|
|
517
|
+
<span class="text-sm font-mono font-medium">{{ m.name }}</span>
|
|
518
|
+
<UBadge :label="m.source" variant="outline" size="xs" />
|
|
519
|
+
<UBadge
|
|
520
|
+
:label="m.transport"
|
|
521
|
+
:color="transportColor(m.transport)"
|
|
522
|
+
variant="subtle"
|
|
523
|
+
size="xs"
|
|
524
|
+
/>
|
|
525
|
+
</div>
|
|
526
|
+
<p v-if="m.command" class="text-xs font-mono text-muted truncate" :title="m.command">
|
|
527
|
+
{{ m.command }}
|
|
528
|
+
</p>
|
|
529
|
+
</div>
|
|
530
|
+
</div>
|
|
531
|
+
</div>
|
|
532
|
+
</section>
|
|
533
|
+
|
|
534
|
+
<!-- Hooks -->
|
|
535
|
+
<section v-else-if="activeSection === 'hooks'">
|
|
536
|
+
<div class="flex items-baseline justify-between mb-1">
|
|
537
|
+
<h2 class="text-lg font-semibold">Hooks</h2>
|
|
538
|
+
<UButton
|
|
539
|
+
label="Refresh"
|
|
540
|
+
variant="ghost"
|
|
541
|
+
icon="i-lucide-refresh-cw"
|
|
542
|
+
size="xs"
|
|
543
|
+
@click="refreshHooks()"
|
|
544
|
+
/>
|
|
545
|
+
</div>
|
|
546
|
+
<p class="text-sm text-muted mb-6">
|
|
547
|
+
Claude Code hooks wired by the ArkaOS installer.
|
|
548
|
+
Sourced from
|
|
549
|
+
<code class="font-mono text-xs">{{ hooksData?.settings_path ?? '~/.claude/settings.json' }}</code>.
|
|
550
|
+
Read-only — re-wire via <code class="font-mono text-xs">npx arkaos@latest update</code>.
|
|
551
|
+
</p>
|
|
552
|
+
<div
|
|
553
|
+
v-if="hooksData?.hard_enforcement"
|
|
554
|
+
class="mb-4 rounded-lg border border-primary/30 bg-primary/5 p-3 text-sm"
|
|
555
|
+
>
|
|
556
|
+
<UIcon name="i-lucide-shield-check" class="size-4 inline text-primary mr-1" />
|
|
557
|
+
Hard enforcement is <strong>ON</strong>. Effect tools require
|
|
558
|
+
<code class="font-mono text-xs">[arka:routing]</code> markers.
|
|
559
|
+
</div>
|
|
560
|
+
<div v-if="!hooks.length" class="rounded-lg border border-default p-6 text-center">
|
|
561
|
+
<UIcon name="i-lucide-webhook" class="size-10 text-muted mx-auto mb-2" />
|
|
562
|
+
<p class="text-sm text-muted">No hooks wired in settings.json.</p>
|
|
563
|
+
</div>
|
|
564
|
+
<div v-else class="space-y-3">
|
|
565
|
+
<div
|
|
566
|
+
v-for="h in hooks"
|
|
567
|
+
:key="h.hook"
|
|
568
|
+
class="rounded-lg border border-default p-3"
|
|
569
|
+
>
|
|
570
|
+
<div class="flex items-center gap-2 mb-2">
|
|
571
|
+
<span class="text-sm font-mono font-semibold">{{ h.hook }}</span>
|
|
572
|
+
<UBadge :label="`${h.count}`" variant="subtle" size="xs" />
|
|
573
|
+
</div>
|
|
574
|
+
<ul class="space-y-1">
|
|
575
|
+
<li
|
|
576
|
+
v-for="(c, idx) in h.commands"
|
|
577
|
+
:key="idx"
|
|
578
|
+
class="flex items-center gap-2 text-xs"
|
|
579
|
+
>
|
|
580
|
+
<UIcon name="i-lucide-terminal" class="size-3 text-muted shrink-0" />
|
|
581
|
+
<code class="font-mono text-xs truncate flex-1" :title="c.command">
|
|
582
|
+
{{ c.command }}
|
|
583
|
+
</code>
|
|
584
|
+
<span v-if="c.timeout" class="text-muted whitespace-nowrap">
|
|
585
|
+
{{ c.timeout }}s
|
|
586
|
+
</span>
|
|
587
|
+
</li>
|
|
588
|
+
</ul>
|
|
589
|
+
</div>
|
|
590
|
+
</div>
|
|
591
|
+
</section>
|
|
592
|
+
|
|
593
|
+
<!-- Plugins -->
|
|
594
|
+
<section v-else-if="activeSection === 'plugins'">
|
|
595
|
+
<div class="flex items-baseline justify-between mb-1">
|
|
596
|
+
<h2 class="text-lg font-semibold">Plugins</h2>
|
|
597
|
+
<UButton
|
|
598
|
+
label="Refresh"
|
|
599
|
+
variant="ghost"
|
|
600
|
+
icon="i-lucide-refresh-cw"
|
|
601
|
+
size="xs"
|
|
602
|
+
@click="refreshPlugins()"
|
|
603
|
+
/>
|
|
604
|
+
</div>
|
|
605
|
+
<p class="text-sm text-muted mb-6">
|
|
606
|
+
Claude Code plugins installed via the marketplace system
|
|
607
|
+
(PR43 auto-install + PR55 ArkaOS marketplace). Sourced
|
|
608
|
+
from <code class="font-mono text-xs">{{ pluginsData?.plugins_path ?? '~/.claude/plugins/installed_plugins.json' }}</code>.
|
|
609
|
+
</p>
|
|
610
|
+
<div v-if="!plugins.length" class="rounded-lg border border-default p-6 text-center">
|
|
611
|
+
<UIcon name="i-lucide-puzzle" class="size-10 text-muted mx-auto mb-2" />
|
|
612
|
+
<p class="text-sm text-muted">No plugins installed.</p>
|
|
613
|
+
<p class="text-xs text-muted mt-2">
|
|
614
|
+
Try <code class="font-mono">/plugin marketplace add andreagroferreira/arka-os</code>
|
|
615
|
+
from Claude Code.
|
|
616
|
+
</p>
|
|
617
|
+
</div>
|
|
618
|
+
<div v-else class="space-y-2">
|
|
619
|
+
<div
|
|
620
|
+
v-for="p in plugins"
|
|
621
|
+
:key="`${p.marketplace}:${p.name}:${p.version}`"
|
|
622
|
+
class="flex items-center gap-3 rounded-lg border border-default p-3"
|
|
623
|
+
>
|
|
624
|
+
<UIcon name="i-lucide-puzzle" class="size-4 text-muted shrink-0" />
|
|
625
|
+
<div class="flex-1 min-w-0">
|
|
626
|
+
<div class="flex items-center gap-2 mb-0.5">
|
|
627
|
+
<span class="text-sm font-semibold">{{ p.name }}</span>
|
|
628
|
+
<UBadge :label="p.marketplace" variant="outline" size="xs" />
|
|
629
|
+
<UBadge v-if="p.scope" :label="p.scope" variant="soft" size="xs" />
|
|
630
|
+
<UBadge v-if="p.version" :label="`v${p.version}`" variant="subtle" size="xs" />
|
|
631
|
+
</div>
|
|
632
|
+
<p v-if="p.installed_at" class="text-xs text-muted">
|
|
633
|
+
Installed {{ formatInstalledAt(p.installed_at) }}
|
|
634
|
+
</p>
|
|
635
|
+
</div>
|
|
636
|
+
</div>
|
|
637
|
+
</div>
|
|
638
|
+
</section>
|
|
639
|
+
|
|
640
|
+
<!-- Theme -->
|
|
641
|
+
<section v-else-if="activeSection === 'theme'">
|
|
642
|
+
<h2 class="text-lg font-semibold mb-1">Theme</h2>
|
|
643
|
+
<p class="text-sm text-muted mb-6">
|
|
644
|
+
Light / dark / system (follows OS preference).
|
|
645
|
+
Stored locally by your browser.
|
|
646
|
+
</p>
|
|
647
|
+
<UCard>
|
|
648
|
+
<div class="space-y-4">
|
|
649
|
+
<UFormField label="Appearance">
|
|
650
|
+
<USelect
|
|
651
|
+
v-model="colorMode.preference"
|
|
652
|
+
:items="themeOptions"
|
|
653
|
+
class="w-full max-w-xs"
|
|
654
|
+
/>
|
|
655
|
+
</UFormField>
|
|
656
|
+
<p class="text-xs text-muted">
|
|
657
|
+
Currently rendering as
|
|
658
|
+
<UBadge :label="colorMode.value" variant="subtle" size="xs" />
|
|
659
|
+
</p>
|
|
660
|
+
</div>
|
|
661
|
+
</UCard>
|
|
662
|
+
</section>
|
|
412
663
|
</div>
|
|
413
664
|
</div>
|
|
414
665
|
</template>
|
package/package.json
CHANGED
package/pyproject.toml
CHANGED
|
Binary file
|
package/scripts/dashboard-api.py
CHANGED
|
@@ -1110,6 +1110,189 @@ def llm_costs_trend(days: int = 7):
|
|
|
1110
1110
|
return {"days": out_days, "period_days": capped_days}
|
|
1111
1111
|
|
|
1112
1112
|
|
|
1113
|
+
# --- Settings sections (PR63b v2.89.0): MCPs / Hooks / Plugins ---
|
|
1114
|
+
|
|
1115
|
+
|
|
1116
|
+
@app.get("/api/settings/mcps")
|
|
1117
|
+
def settings_mcps():
|
|
1118
|
+
"""List MCP servers across user-global config + ArkaOS registry.
|
|
1119
|
+
|
|
1120
|
+
Reads:
|
|
1121
|
+
- ``~/.claude.json::mcpServers`` (Claude Code user-global)
|
|
1122
|
+
- ``~/.claude/skills/arka/mcps/registry.json`` (ArkaOS registry)
|
|
1123
|
+
|
|
1124
|
+
Returns a deduplicated list with each entry's name + source +
|
|
1125
|
+
transport (stdio / http / sse) where the config exposes it.
|
|
1126
|
+
"""
|
|
1127
|
+
out: list[dict] = []
|
|
1128
|
+
seen: set[str] = set()
|
|
1129
|
+
|
|
1130
|
+
user_global = Path.home() / ".claude.json"
|
|
1131
|
+
if user_global.exists():
|
|
1132
|
+
try:
|
|
1133
|
+
data = json.loads(user_global.read_text(encoding="utf-8"))
|
|
1134
|
+
except (json.JSONDecodeError, OSError):
|
|
1135
|
+
data = {}
|
|
1136
|
+
for name, cfg in (data.get("mcpServers") or {}).items():
|
|
1137
|
+
if not isinstance(name, str) or name in seen:
|
|
1138
|
+
continue
|
|
1139
|
+
seen.add(name)
|
|
1140
|
+
out.append({
|
|
1141
|
+
"name": name,
|
|
1142
|
+
"source": "user-global",
|
|
1143
|
+
"transport": _detect_mcp_transport(cfg),
|
|
1144
|
+
"command": (cfg or {}).get("command", "") if isinstance(cfg, dict) else "",
|
|
1145
|
+
})
|
|
1146
|
+
|
|
1147
|
+
registry = Path.home() / ".claude" / "skills" / "arka" / "mcps" / "registry.json"
|
|
1148
|
+
if registry.exists():
|
|
1149
|
+
try:
|
|
1150
|
+
data = json.loads(registry.read_text(encoding="utf-8"))
|
|
1151
|
+
except (json.JSONDecodeError, OSError):
|
|
1152
|
+
data = {}
|
|
1153
|
+
servers = data.get("servers") if isinstance(data, dict) else None
|
|
1154
|
+
if isinstance(servers, dict):
|
|
1155
|
+
for name, cfg in servers.items():
|
|
1156
|
+
if not isinstance(name, str) or name in seen:
|
|
1157
|
+
continue
|
|
1158
|
+
seen.add(name)
|
|
1159
|
+
out.append({
|
|
1160
|
+
"name": name,
|
|
1161
|
+
"source": "arkaos-registry",
|
|
1162
|
+
"transport": _detect_mcp_transport(cfg),
|
|
1163
|
+
"command": (cfg or {}).get("command", "") if isinstance(cfg, dict) else "",
|
|
1164
|
+
})
|
|
1165
|
+
elif isinstance(servers, list):
|
|
1166
|
+
for entry in servers:
|
|
1167
|
+
if not isinstance(entry, dict):
|
|
1168
|
+
continue
|
|
1169
|
+
name = str(entry.get("name") or "")
|
|
1170
|
+
if not name or name in seen:
|
|
1171
|
+
continue
|
|
1172
|
+
seen.add(name)
|
|
1173
|
+
out.append({
|
|
1174
|
+
"name": name,
|
|
1175
|
+
"source": "arkaos-registry",
|
|
1176
|
+
"transport": _detect_mcp_transport(entry),
|
|
1177
|
+
"command": entry.get("command", ""),
|
|
1178
|
+
})
|
|
1179
|
+
|
|
1180
|
+
out.sort(key=lambda r: r["name"])
|
|
1181
|
+
return {"mcps": out, "total": len(out)}
|
|
1182
|
+
|
|
1183
|
+
|
|
1184
|
+
def _detect_mcp_transport(cfg: object) -> str:
|
|
1185
|
+
"""Best-effort transport sniff from an MCP server config dict."""
|
|
1186
|
+
if not isinstance(cfg, dict):
|
|
1187
|
+
return "unknown"
|
|
1188
|
+
if cfg.get("url"):
|
|
1189
|
+
return "http"
|
|
1190
|
+
if cfg.get("transport"):
|
|
1191
|
+
return str(cfg["transport"])
|
|
1192
|
+
if cfg.get("command"):
|
|
1193
|
+
return "stdio"
|
|
1194
|
+
return "unknown"
|
|
1195
|
+
|
|
1196
|
+
|
|
1197
|
+
@app.get("/api/settings/hooks")
|
|
1198
|
+
def settings_hooks():
|
|
1199
|
+
"""Inspect the hooks block of ~/.claude/settings.json.
|
|
1200
|
+
|
|
1201
|
+
Returns one row per hook type with command paths + timeouts so the
|
|
1202
|
+
operator can see at a glance which hooks are wired and which are
|
|
1203
|
+
missing. We never edit the file from here (Hooks ship from the
|
|
1204
|
+
ArkaOS installer); this is purely read-only diagnostics.
|
|
1205
|
+
"""
|
|
1206
|
+
settings_file = Path.home() / ".claude" / "settings.json"
|
|
1207
|
+
if not settings_file.exists():
|
|
1208
|
+
return {"hooks": [], "settings_path": str(settings_file)}
|
|
1209
|
+
try:
|
|
1210
|
+
data = json.loads(settings_file.read_text(encoding="utf-8"))
|
|
1211
|
+
except (json.JSONDecodeError, OSError):
|
|
1212
|
+
return {"hooks": [], "settings_path": str(settings_file)}
|
|
1213
|
+
|
|
1214
|
+
hooks_block = data.get("hooks") if isinstance(data, dict) else None
|
|
1215
|
+
if not isinstance(hooks_block, dict):
|
|
1216
|
+
return {"hooks": [], "settings_path": str(settings_file)}
|
|
1217
|
+
|
|
1218
|
+
rows: list[dict] = []
|
|
1219
|
+
for hook_type, entries in hooks_block.items():
|
|
1220
|
+
if not isinstance(entries, list):
|
|
1221
|
+
continue
|
|
1222
|
+
commands: list[dict] = []
|
|
1223
|
+
for entry in entries:
|
|
1224
|
+
if not isinstance(entry, dict):
|
|
1225
|
+
continue
|
|
1226
|
+
inner = entry.get("hooks") if isinstance(entry, dict) else None
|
|
1227
|
+
if not isinstance(inner, list):
|
|
1228
|
+
continue
|
|
1229
|
+
for h in inner:
|
|
1230
|
+
if not isinstance(h, dict):
|
|
1231
|
+
continue
|
|
1232
|
+
commands.append({
|
|
1233
|
+
"command": str(h.get("command", ""))[:200],
|
|
1234
|
+
"type": str(h.get("type", "command")),
|
|
1235
|
+
"timeout": h.get("timeout"),
|
|
1236
|
+
})
|
|
1237
|
+
rows.append({
|
|
1238
|
+
"hook": hook_type,
|
|
1239
|
+
"count": len(commands),
|
|
1240
|
+
"commands": commands,
|
|
1241
|
+
})
|
|
1242
|
+
rows.sort(key=lambda r: r["hook"])
|
|
1243
|
+
hard_enforcement = bool(
|
|
1244
|
+
isinstance(data.get("hooks"), dict)
|
|
1245
|
+
and data["hooks"].get("hardEnforcement")
|
|
1246
|
+
)
|
|
1247
|
+
return {
|
|
1248
|
+
"hooks": rows,
|
|
1249
|
+
"settings_path": str(settings_file),
|
|
1250
|
+
"hard_enforcement": hard_enforcement,
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
|
|
1254
|
+
@app.get("/api/settings/plugins")
|
|
1255
|
+
def settings_plugins():
|
|
1256
|
+
"""List Claude Code plugins installed via ~/.claude/plugins/installed_plugins.json.
|
|
1257
|
+
|
|
1258
|
+
The PR43 auto-installer + PR55 marketplace flow both touch this
|
|
1259
|
+
file. Format is ``{"plugins": {"<name>@<marketplace>": [entry,...]}}``.
|
|
1260
|
+
We flatten to one row per (name, marketplace, version).
|
|
1261
|
+
"""
|
|
1262
|
+
plugins_file = Path.home() / ".claude" / "plugins" / "installed_plugins.json"
|
|
1263
|
+
if not plugins_file.exists():
|
|
1264
|
+
return {"plugins": [], "total": 0, "plugins_path": str(plugins_file)}
|
|
1265
|
+
try:
|
|
1266
|
+
data = json.loads(plugins_file.read_text(encoding="utf-8"))
|
|
1267
|
+
except (json.JSONDecodeError, OSError):
|
|
1268
|
+
return {"plugins": [], "total": 0, "plugins_path": str(plugins_file)}
|
|
1269
|
+
|
|
1270
|
+
rows: list[dict] = []
|
|
1271
|
+
plugins_map = data.get("plugins") if isinstance(data, dict) else None
|
|
1272
|
+
if isinstance(plugins_map, dict):
|
|
1273
|
+
for key, entries in plugins_map.items():
|
|
1274
|
+
if not isinstance(entries, list):
|
|
1275
|
+
continue
|
|
1276
|
+
name, _, marketplace = str(key).partition("@")
|
|
1277
|
+
for entry in entries:
|
|
1278
|
+
if not isinstance(entry, dict):
|
|
1279
|
+
continue
|
|
1280
|
+
rows.append({
|
|
1281
|
+
"name": name,
|
|
1282
|
+
"marketplace": marketplace,
|
|
1283
|
+
"version": entry.get("version", ""),
|
|
1284
|
+
"scope": entry.get("scope", ""),
|
|
1285
|
+
"installed_at": entry.get("installedAt", ""),
|
|
1286
|
+
"last_updated": entry.get("lastUpdated", ""),
|
|
1287
|
+
})
|
|
1288
|
+
rows.sort(key=lambda r: (r["marketplace"], r["name"]))
|
|
1289
|
+
return {
|
|
1290
|
+
"plugins": rows,
|
|
1291
|
+
"total": len(rows),
|
|
1292
|
+
"plugins_path": str(plugins_file),
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
|
|
1113
1296
|
# --- Profile (PR63 v2.81.0) ---
|
|
1114
1297
|
|
|
1115
1298
|
@app.get("/api/profile")
|