arkaos 2.87.0 → 2.89.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 +1 -1
- package/core/__pycache__/keys.cpython-313.pyc +0 -0
- package/dashboard/app/pages/knowledge.vue +103 -6
- package/dashboard/app/pages/settings.vue +256 -5
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/scripts/__pycache__/dashboard-api.cpython-313.pyc +0 -0
- package/scripts/dashboard-api.py +209 -0
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
2.
|
|
1
|
+
2.89.0
|
|
Binary file
|
|
@@ -324,6 +324,90 @@ async function handleSearch() {
|
|
|
324
324
|
function formatScore(score: number): string {
|
|
325
325
|
return `${(score * 100).toFixed(0)}%`
|
|
326
326
|
}
|
|
327
|
+
|
|
328
|
+
// PR71 v2.88.0 — delete all chunks from a given source.
|
|
329
|
+
|
|
330
|
+
const deletingSource = ref<string | null>(null)
|
|
331
|
+
|
|
332
|
+
async function askDeleteSource(source: string) {
|
|
333
|
+
if (!source) return
|
|
334
|
+
if (typeof window === 'undefined') return
|
|
335
|
+
const ok = window.confirm(
|
|
336
|
+
`Delete every indexed chunk from this source?\n\n${source}\n\n`
|
|
337
|
+
+ 'This removes the source from search results but does not delete the original file. '
|
|
338
|
+
+ 'You can re-ingest the source later if needed.',
|
|
339
|
+
)
|
|
340
|
+
if (!ok) return
|
|
341
|
+
await deleteSource(source)
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
async function deleteSource(source: string) {
|
|
345
|
+
deletingSource.value = source
|
|
346
|
+
try {
|
|
347
|
+
const res = await $fetch<{ deleted?: number, source?: string, error?: string }>(
|
|
348
|
+
`${apiBase}/api/knowledge/sources`,
|
|
349
|
+
{ method: 'DELETE', query: { source } },
|
|
350
|
+
)
|
|
351
|
+
if (res.error) {
|
|
352
|
+
toast.add({
|
|
353
|
+
title: 'Delete failed',
|
|
354
|
+
description: res.error,
|
|
355
|
+
color: 'error',
|
|
356
|
+
})
|
|
357
|
+
return
|
|
358
|
+
}
|
|
359
|
+
const deleted = res.deleted ?? 0
|
|
360
|
+
// Drop the matching rows from the in-memory list without a full re-fetch.
|
|
361
|
+
searchResults.value = searchResults.value.filter((r) => r.source !== source)
|
|
362
|
+
searchTotal.value = searchResults.value.length
|
|
363
|
+
// Refresh stats so the chunk count in the header updates.
|
|
364
|
+
if (typeof refresh === 'function') {
|
|
365
|
+
await refresh()
|
|
366
|
+
}
|
|
367
|
+
toast.add({
|
|
368
|
+
title: deleted > 0
|
|
369
|
+
? `Deleted ${deleted} chunk${deleted === 1 ? '' : 's'}`
|
|
370
|
+
: 'Nothing to delete',
|
|
371
|
+
description: source,
|
|
372
|
+
color: 'success',
|
|
373
|
+
})
|
|
374
|
+
} catch (err) {
|
|
375
|
+
toast.add({
|
|
376
|
+
title: 'Delete failed',
|
|
377
|
+
description: err instanceof Error ? err.message : 'unknown error',
|
|
378
|
+
color: 'error',
|
|
379
|
+
})
|
|
380
|
+
} finally {
|
|
381
|
+
deletingSource.value = null
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// PR71 — highlight the search query in the preview text.
|
|
386
|
+
// Tolerates malformed regex (escapes special characters) and HTML-
|
|
387
|
+
// escapes the input so v-html'd output is safe from XSS via DB rows.
|
|
388
|
+
function highlightMatches(text: string, query: string): string {
|
|
389
|
+
const safe = escapeHtml(text || '')
|
|
390
|
+
const q = (query || '').trim()
|
|
391
|
+
if (!q) return safe
|
|
392
|
+
const pattern = new RegExp(`(${escapeRegex(q)})`, 'gi')
|
|
393
|
+
return safe.replace(
|
|
394
|
+
pattern,
|
|
395
|
+
'<mark class="bg-primary/20 text-primary rounded px-0.5">$1</mark>',
|
|
396
|
+
)
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function escapeHtml(value: string): string {
|
|
400
|
+
return value
|
|
401
|
+
.replace(/&/g, '&')
|
|
402
|
+
.replace(/</g, '<')
|
|
403
|
+
.replace(/>/g, '>')
|
|
404
|
+
.replace(/"/g, '"')
|
|
405
|
+
.replace(/'/g, ''')
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function escapeRegex(value: string): string {
|
|
409
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
410
|
+
}
|
|
327
411
|
</script>
|
|
328
412
|
|
|
329
413
|
<template>
|
|
@@ -771,17 +855,30 @@ function formatScore(score: number): string {
|
|
|
771
855
|
{{ result.heading }}
|
|
772
856
|
</span>
|
|
773
857
|
</div>
|
|
774
|
-
<
|
|
775
|
-
|
|
776
|
-
|
|
858
|
+
<div class="flex items-center gap-2 shrink-0">
|
|
859
|
+
<span class="text-xs text-muted whitespace-nowrap">
|
|
860
|
+
Score: {{ formatScore(result.score) }}
|
|
861
|
+
</span>
|
|
862
|
+
<UButton
|
|
863
|
+
v-if="result.source"
|
|
864
|
+
:icon="deletingSource === result.source
|
|
865
|
+
? 'i-lucide-loader-2'
|
|
866
|
+
: 'i-lucide-trash-2'"
|
|
867
|
+
:loading="deletingSource === result.source"
|
|
868
|
+
variant="ghost"
|
|
869
|
+
color="error"
|
|
870
|
+
size="xs"
|
|
871
|
+
aria-label="Delete all chunks from this source"
|
|
872
|
+
@click.stop="askDeleteSource(result.source)"
|
|
873
|
+
/>
|
|
874
|
+
</div>
|
|
777
875
|
</div>
|
|
778
876
|
<p v-if="result.source" class="text-xs text-muted mb-1 truncate">
|
|
779
877
|
<UIcon name="i-lucide-file-text" class="size-3 inline-block mr-1" />
|
|
780
878
|
{{ result.source }}
|
|
781
879
|
</p>
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
</p>
|
|
880
|
+
<!-- PR71 v2.88.0 — highlight query matches in the preview -->
|
|
881
|
+
<p class="text-sm text-muted line-clamp-3" v-html="highlightMatches(result.text || result.content, searchQuery)" />
|
|
785
882
|
</div>
|
|
786
883
|
</div>
|
|
787
884
|
|
|
@@ -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
|
@@ -612,6 +612,32 @@ def knowledge_search(q: str = Query(...), top_k: int = Query(5)):
|
|
|
612
612
|
return {"results": results, "query": q, "total": len(results)}
|
|
613
613
|
|
|
614
614
|
|
|
615
|
+
@app.delete("/api/knowledge/sources")
|
|
616
|
+
def knowledge_delete_source(source: str = Query(...)):
|
|
617
|
+
"""PR71 v2.88.0 — remove all chunks from a given source.
|
|
618
|
+
|
|
619
|
+
Operators sometimes ingest a noisy / wrong source and want to nuke
|
|
620
|
+
every chunk that came from it without rebuilding the whole vector
|
|
621
|
+
DB. The vector store already exposes `remove_file(source)` —
|
|
622
|
+
this endpoint just exposes it on the wire.
|
|
623
|
+
|
|
624
|
+
Returns ``{deleted: N, source: "..."}``. Refuses empty source
|
|
625
|
+
paths so a runaway client doesn't accidentally request "delete
|
|
626
|
+
everything that has no source".
|
|
627
|
+
"""
|
|
628
|
+
clean = (source or "").strip()
|
|
629
|
+
if not clean:
|
|
630
|
+
return {"error": "source query param is required"}
|
|
631
|
+
store = _get_vector_store()
|
|
632
|
+
if not store:
|
|
633
|
+
return {"error": "vector store unavailable", "deleted": 0}
|
|
634
|
+
try:
|
|
635
|
+
deleted = store.remove_file(clean)
|
|
636
|
+
except Exception as exc: # noqa: BLE001 — surface as 200+error
|
|
637
|
+
return {"error": f"delete failed: {exc}", "deleted": 0}
|
|
638
|
+
return {"deleted": int(deleted), "source": clean}
|
|
639
|
+
|
|
640
|
+
|
|
615
641
|
@app.get("/api/health")
|
|
616
642
|
def health():
|
|
617
643
|
"""PR70 v2.87.0 — per-check severity + response timestamp.
|
|
@@ -1084,6 +1110,189 @@ def llm_costs_trend(days: int = 7):
|
|
|
1084
1110
|
return {"days": out_days, "period_days": capped_days}
|
|
1085
1111
|
|
|
1086
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
|
+
|
|
1087
1296
|
# --- Profile (PR63 v2.81.0) ---
|
|
1088
1297
|
|
|
1089
1298
|
@app.get("/api/profile")
|