arkaos 3.23.0 → 3.25.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/knowledge/__pycache__/vector_store.cpython-313.pyc +0 -0
- package/core/knowledge/vector_store.py +13 -0
- package/dashboard/app/components/KnowledgeSourcesList.vue +157 -0
- package/dashboard/app/layouts/default.vue +7 -0
- package/dashboard/app/pages/knowledge.vue +5 -0
- package/dashboard/app/pages/workflows.vue +191 -0
- 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 +57 -0
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
3.
|
|
1
|
+
3.25.0
|
|
Binary file
|
|
@@ -283,6 +283,19 @@ class VectorStore:
|
|
|
283
283
|
"db_path": self._db_path,
|
|
284
284
|
}
|
|
285
285
|
|
|
286
|
+
def list_sources(self) -> list[dict]:
|
|
287
|
+
"""PR88c v3.25.0 — distinct sources with chunk counts.
|
|
288
|
+
|
|
289
|
+
Returns rows sorted by chunk count desc so the noisiest
|
|
290
|
+
sources surface first.
|
|
291
|
+
"""
|
|
292
|
+
rows = self._db.execute(
|
|
293
|
+
"SELECT source, COUNT(*) AS chunks FROM chunks "
|
|
294
|
+
"WHERE source IS NOT NULL AND source != '' "
|
|
295
|
+
"GROUP BY source ORDER BY chunks DESC"
|
|
296
|
+
).fetchall()
|
|
297
|
+
return [{"source": r["source"], "chunks": int(r["chunks"])} for r in rows]
|
|
298
|
+
|
|
286
299
|
def clear(self) -> None:
|
|
287
300
|
"""Remove all data."""
|
|
288
301
|
if self._vec_available:
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// PR88c v3.25.0 — Listing + management of indexed knowledge sources.
|
|
3
|
+
//
|
|
4
|
+
// Sits below the ingest UI on /knowledge. Loads GET /api/knowledge/sources
|
|
5
|
+
// (returns `{sources: [{source, chunks}], total}`), supports search,
|
|
6
|
+
// per-row Delete (DELETE /api/knowledge/sources?source=...). Pagination
|
|
7
|
+
// inline.
|
|
8
|
+
|
|
9
|
+
interface SourceRow {
|
|
10
|
+
source: string
|
|
11
|
+
chunks: number
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const { fetchApi, apiBase } = useApi()
|
|
15
|
+
const toast = useToast()
|
|
16
|
+
const confirmDialog = useConfirmDialog()
|
|
17
|
+
|
|
18
|
+
const { data, status, error, refresh } = await fetchApi<{
|
|
19
|
+
sources: SourceRow[]
|
|
20
|
+
total: number
|
|
21
|
+
}>('/api/knowledge/sources')
|
|
22
|
+
|
|
23
|
+
const sources = computed(() => data.value?.sources ?? [])
|
|
24
|
+
const search = ref('')
|
|
25
|
+
const page = ref(1)
|
|
26
|
+
const pageSize = 15
|
|
27
|
+
|
|
28
|
+
const filtered = computed(() => {
|
|
29
|
+
const q = search.value.toLowerCase().trim()
|
|
30
|
+
if (!q) return sources.value
|
|
31
|
+
return sources.value.filter((s) => s.source.toLowerCase().includes(q))
|
|
32
|
+
})
|
|
33
|
+
const totalPages = computed(() => Math.max(1, Math.ceil(filtered.value.length / pageSize)))
|
|
34
|
+
const paged = computed(() =>
|
|
35
|
+
filtered.value.slice((page.value - 1) * pageSize, page.value * pageSize),
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
watch(search, () => { page.value = 1 })
|
|
39
|
+
|
|
40
|
+
async function remove(row: SourceRow) {
|
|
41
|
+
const ok = await confirmDialog({
|
|
42
|
+
title: 'Delete source?',
|
|
43
|
+
description: `Removes ${row.chunks} chunk${row.chunks === 1 ? '' : 's'} from the vector store. This cannot be undone.`,
|
|
44
|
+
confirmLabel: 'Delete',
|
|
45
|
+
cancelLabel: 'Cancel',
|
|
46
|
+
variant: 'danger',
|
|
47
|
+
})
|
|
48
|
+
if (!ok) return
|
|
49
|
+
try {
|
|
50
|
+
const res = await $fetch<{ deleted?: number, error?: string }>(
|
|
51
|
+
`${apiBase}/api/knowledge/sources`,
|
|
52
|
+
{ method: 'DELETE', query: { source: row.source } },
|
|
53
|
+
)
|
|
54
|
+
if (res.error) throw new Error(res.error)
|
|
55
|
+
toast.add({
|
|
56
|
+
title: `Removed ${res.deleted ?? 0} chunks`,
|
|
57
|
+
description: row.source,
|
|
58
|
+
color: 'success',
|
|
59
|
+
})
|
|
60
|
+
await refresh()
|
|
61
|
+
} catch (err) {
|
|
62
|
+
toast.add({
|
|
63
|
+
title: 'Delete failed',
|
|
64
|
+
description: err instanceof Error ? err.message : 'unknown error',
|
|
65
|
+
color: 'error',
|
|
66
|
+
})
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function sourceLabel(src: string): string {
|
|
71
|
+
if (src.startsWith('http')) {
|
|
72
|
+
try {
|
|
73
|
+
const u = new URL(src)
|
|
74
|
+
return u.hostname + u.pathname
|
|
75
|
+
} catch {
|
|
76
|
+
return src
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return src
|
|
80
|
+
}
|
|
81
|
+
</script>
|
|
82
|
+
|
|
83
|
+
<template>
|
|
84
|
+
<UCard>
|
|
85
|
+
<template #header>
|
|
86
|
+
<div class="flex items-center justify-between gap-3">
|
|
87
|
+
<div>
|
|
88
|
+
<h3 class="text-lg font-bold">Indexed sources</h3>
|
|
89
|
+
<p class="text-xs text-muted mt-0.5">
|
|
90
|
+
Every distinct source contributing chunks to the vector store.
|
|
91
|
+
<span v-if="data?.total">{{ data.total }} total.</span>
|
|
92
|
+
</p>
|
|
93
|
+
</div>
|
|
94
|
+
<UButton
|
|
95
|
+
icon="i-lucide-refresh-cw"
|
|
96
|
+
variant="ghost"
|
|
97
|
+
size="sm"
|
|
98
|
+
aria-label="Refresh"
|
|
99
|
+
:loading="status === 'pending'"
|
|
100
|
+
@click="refresh"
|
|
101
|
+
/>
|
|
102
|
+
</div>
|
|
103
|
+
</template>
|
|
104
|
+
|
|
105
|
+
<div v-if="error" class="py-6 text-center text-sm text-error">
|
|
106
|
+
Failed to load sources.
|
|
107
|
+
</div>
|
|
108
|
+
<div v-else-if="!sources.length" class="py-6 text-center text-sm text-muted">
|
|
109
|
+
<UIcon name="i-lucide-database" class="size-6 mx-auto mb-2" />
|
|
110
|
+
No sources indexed yet. Use the ingest panel above to add content.
|
|
111
|
+
</div>
|
|
112
|
+
<div v-else class="space-y-3">
|
|
113
|
+
<UInput
|
|
114
|
+
v-model="search"
|
|
115
|
+
icon="i-lucide-search"
|
|
116
|
+
placeholder="Filter by source URL or path…"
|
|
117
|
+
class="w-full"
|
|
118
|
+
/>
|
|
119
|
+
|
|
120
|
+
<ul class="space-y-1.5">
|
|
121
|
+
<li
|
|
122
|
+
v-for="row in paged"
|
|
123
|
+
:key="row.source"
|
|
124
|
+
class="flex items-center gap-3 rounded-lg border border-default p-2.5 hover:border-primary/40 transition-colors"
|
|
125
|
+
>
|
|
126
|
+
<UIcon
|
|
127
|
+
:name="row.source.startsWith('http') ? 'i-lucide-link' : 'i-lucide-file-text'"
|
|
128
|
+
class="size-4 text-muted shrink-0"
|
|
129
|
+
/>
|
|
130
|
+
<div class="flex-1 min-w-0">
|
|
131
|
+
<p class="text-sm font-mono truncate" :title="row.source">
|
|
132
|
+
{{ sourceLabel(row.source) }}
|
|
133
|
+
</p>
|
|
134
|
+
</div>
|
|
135
|
+
<UBadge :label="`${row.chunks} chunk${row.chunks === 1 ? '' : 's'}`" variant="subtle" size="xs" />
|
|
136
|
+
<UButton
|
|
137
|
+
icon="i-lucide-trash-2"
|
|
138
|
+
color="error"
|
|
139
|
+
variant="ghost"
|
|
140
|
+
size="xs"
|
|
141
|
+
aria-label="Delete source"
|
|
142
|
+
@click="remove(row)"
|
|
143
|
+
/>
|
|
144
|
+
</li>
|
|
145
|
+
</ul>
|
|
146
|
+
|
|
147
|
+
<div v-if="totalPages > 1" class="flex items-center justify-center pt-2">
|
|
148
|
+
<UPagination
|
|
149
|
+
:page="page"
|
|
150
|
+
:total="filtered.length"
|
|
151
|
+
:items-per-page="pageSize"
|
|
152
|
+
@update:page="(val) => page = val"
|
|
153
|
+
/>
|
|
154
|
+
</div>
|
|
155
|
+
</div>
|
|
156
|
+
</UCard>
|
|
157
|
+
</template>
|
|
@@ -48,6 +48,13 @@ const links = [[{
|
|
|
48
48
|
onSelect: () => {
|
|
49
49
|
open.value = false
|
|
50
50
|
}
|
|
51
|
+
}, {
|
|
52
|
+
label: 'Workflows',
|
|
53
|
+
icon: 'i-lucide-workflow',
|
|
54
|
+
to: '/workflows',
|
|
55
|
+
onSelect: () => {
|
|
56
|
+
open.value = false
|
|
57
|
+
}
|
|
51
58
|
}, {
|
|
52
59
|
label: 'Knowledge',
|
|
53
60
|
icon: 'i-lucide-brain',
|
|
@@ -906,6 +906,11 @@ function escapeRegex(value: string): string {
|
|
|
906
906
|
</div>
|
|
907
907
|
</template>
|
|
908
908
|
</div>
|
|
909
|
+
|
|
910
|
+
<!-- PR88c v3.25.0 — Indexed sources management -->
|
|
911
|
+
<div class="mt-6">
|
|
912
|
+
<KnowledgeSourcesList />
|
|
913
|
+
</div>
|
|
909
914
|
</template>
|
|
910
915
|
</template>
|
|
911
916
|
</template>
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// PR88b v3.24.0 — Workflows browser.
|
|
3
|
+
//
|
|
4
|
+
// Lists every YAML workflow under departments/*/workflows/*.yaml with
|
|
5
|
+
// metadata pulled from the file's frontmatter-ish top keys. Selecting
|
|
6
|
+
// a row opens a side panel with the raw YAML for inspection.
|
|
7
|
+
|
|
8
|
+
import type { TableColumn } from '@nuxt/ui'
|
|
9
|
+
|
|
10
|
+
interface Workflow {
|
|
11
|
+
id: string
|
|
12
|
+
name: string
|
|
13
|
+
description: string
|
|
14
|
+
department: string
|
|
15
|
+
tier: string
|
|
16
|
+
command: string
|
|
17
|
+
phases_count: number
|
|
18
|
+
file: string
|
|
19
|
+
content: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const { fetchApi } = useApi()
|
|
23
|
+
const { data, status, error, refresh } = await fetchApi<{ workflows: Workflow[] }>('/api/workflows')
|
|
24
|
+
|
|
25
|
+
const workflows = computed(() => data.value?.workflows ?? [])
|
|
26
|
+
const search = ref('')
|
|
27
|
+
const deptFilter = ref<'all' | string>('all')
|
|
28
|
+
const selected = ref<Workflow | null>(null)
|
|
29
|
+
|
|
30
|
+
const departments = computed(() => {
|
|
31
|
+
const set = new Set<string>()
|
|
32
|
+
for (const w of workflows.value) if (w.department) set.add(w.department)
|
|
33
|
+
return [
|
|
34
|
+
{ label: 'All departments', value: 'all' },
|
|
35
|
+
...Array.from(set).sort().map((d) => ({ label: d, value: d })),
|
|
36
|
+
]
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
const filtered = computed(() => {
|
|
40
|
+
let result = workflows.value
|
|
41
|
+
if (deptFilter.value !== 'all') {
|
|
42
|
+
result = result.filter((w) => w.department === deptFilter.value)
|
|
43
|
+
}
|
|
44
|
+
const q = search.value.toLowerCase().trim()
|
|
45
|
+
if (q) {
|
|
46
|
+
result = result.filter((w) =>
|
|
47
|
+
w.name.toLowerCase().includes(q)
|
|
48
|
+
|| w.description.toLowerCase().includes(q)
|
|
49
|
+
|| w.command.toLowerCase().includes(q)
|
|
50
|
+
|| w.id.toLowerCase().includes(q),
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
return result
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
const tierColor = (tier: string) => {
|
|
57
|
+
const m: Record<string, 'primary' | 'success' | 'warning' | 'neutral'> = {
|
|
58
|
+
enterprise: 'primary',
|
|
59
|
+
focused: 'success',
|
|
60
|
+
specialist: 'warning',
|
|
61
|
+
}
|
|
62
|
+
return m[tier] ?? 'neutral'
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const columns: TableColumn<Workflow>[] = [
|
|
66
|
+
{ accessorKey: 'name', header: 'Name' },
|
|
67
|
+
{ accessorKey: 'department', header: 'Department' },
|
|
68
|
+
{ accessorKey: 'tier', header: 'Tier' },
|
|
69
|
+
{ accessorKey: 'command', header: 'Command' },
|
|
70
|
+
{ accessorKey: 'phases_count', header: 'Phases' },
|
|
71
|
+
]
|
|
72
|
+
</script>
|
|
73
|
+
|
|
74
|
+
<template>
|
|
75
|
+
<UDashboardPanel id="workflows">
|
|
76
|
+
<template #header>
|
|
77
|
+
<UDashboardNavbar title="Workflows">
|
|
78
|
+
<template #leading>
|
|
79
|
+
<UDashboardSidebarCollapse />
|
|
80
|
+
</template>
|
|
81
|
+
<template #trailing>
|
|
82
|
+
<UBadge v-if="workflows.length" :label="String(workflows.length)" variant="subtle" />
|
|
83
|
+
</template>
|
|
84
|
+
</UDashboardNavbar>
|
|
85
|
+
</template>
|
|
86
|
+
|
|
87
|
+
<template #body>
|
|
88
|
+
<DashboardState
|
|
89
|
+
:status="status"
|
|
90
|
+
:error="error"
|
|
91
|
+
:empty="!workflows.length"
|
|
92
|
+
empty-title="No workflows found"
|
|
93
|
+
empty-description="YAML workflows live under departments/*/workflows/."
|
|
94
|
+
empty-icon="i-lucide-workflow"
|
|
95
|
+
loading-label="Scanning workflows"
|
|
96
|
+
:on-retry="() => refresh()"
|
|
97
|
+
>
|
|
98
|
+
<div class="flex flex-wrap items-center gap-3 mb-4">
|
|
99
|
+
<UInput
|
|
100
|
+
v-model="search"
|
|
101
|
+
class="max-w-sm"
|
|
102
|
+
icon="i-lucide-search"
|
|
103
|
+
placeholder="Search name, command, description…"
|
|
104
|
+
/>
|
|
105
|
+
<USelect
|
|
106
|
+
v-model="deptFilter"
|
|
107
|
+
:items="departments"
|
|
108
|
+
placeholder="Department"
|
|
109
|
+
class="min-w-44"
|
|
110
|
+
/>
|
|
111
|
+
<span class="ml-auto text-xs text-muted">
|
|
112
|
+
{{ filtered.length }} workflow{{ filtered.length === 1 ? '' : 's' }}
|
|
113
|
+
</span>
|
|
114
|
+
</div>
|
|
115
|
+
|
|
116
|
+
<div class="grid grid-cols-1 xl:grid-cols-[1.4fr_1fr] gap-4">
|
|
117
|
+
<UTable
|
|
118
|
+
:data="filtered"
|
|
119
|
+
:columns="columns"
|
|
120
|
+
:loading="status === 'pending'"
|
|
121
|
+
:ui="{
|
|
122
|
+
tbody: '[&>tr]:cursor-pointer [&>tr]:hover:bg-elevated/50 [&>tr]:transition-colors',
|
|
123
|
+
th: 'py-2 first:rounded-l-lg last:rounded-r-lg border-y border-default first:border-l last:border-r',
|
|
124
|
+
td: 'border-b border-default',
|
|
125
|
+
}"
|
|
126
|
+
@select="(row: { original: Workflow }) => selected = row.original"
|
|
127
|
+
>
|
|
128
|
+
<template #name-cell="{ row }">
|
|
129
|
+
<div class="min-w-0">
|
|
130
|
+
<p class="font-medium truncate">{{ row.original.name }}</p>
|
|
131
|
+
<p class="text-xs text-muted font-mono truncate">{{ row.original.id }}</p>
|
|
132
|
+
</div>
|
|
133
|
+
</template>
|
|
134
|
+
<template #department-cell="{ row }">
|
|
135
|
+
<UBadge :label="row.original.department" variant="subtle" size="sm" />
|
|
136
|
+
</template>
|
|
137
|
+
<template #tier-cell="{ row }">
|
|
138
|
+
<UBadge
|
|
139
|
+
v-if="row.original.tier"
|
|
140
|
+
:label="row.original.tier"
|
|
141
|
+
:color="tierColor(row.original.tier)"
|
|
142
|
+
variant="subtle"
|
|
143
|
+
size="sm"
|
|
144
|
+
/>
|
|
145
|
+
<span v-else class="text-xs text-muted">—</span>
|
|
146
|
+
</template>
|
|
147
|
+
<template #command-cell="{ row }">
|
|
148
|
+
<code v-if="row.original.command" class="text-xs font-mono">{{ row.original.command }}</code>
|
|
149
|
+
<span v-else class="text-xs text-muted">—</span>
|
|
150
|
+
</template>
|
|
151
|
+
<template #phases_count-cell="{ row }">
|
|
152
|
+
<span class="font-mono text-sm">{{ row.original.phases_count }}</span>
|
|
153
|
+
</template>
|
|
154
|
+
</UTable>
|
|
155
|
+
|
|
156
|
+
<div
|
|
157
|
+
v-if="selected"
|
|
158
|
+
class="rounded-lg border border-default overflow-hidden flex flex-col"
|
|
159
|
+
>
|
|
160
|
+
<div class="px-4 py-3 border-b border-default bg-elevated/30">
|
|
161
|
+
<div class="flex items-start justify-between gap-3">
|
|
162
|
+
<div class="min-w-0">
|
|
163
|
+
<p class="text-xs text-muted uppercase tracking-wide">YAML preview</p>
|
|
164
|
+
<p class="font-semibold truncate">{{ selected.name }}</p>
|
|
165
|
+
<p class="text-xs text-muted font-mono truncate">{{ selected.file }}</p>
|
|
166
|
+
</div>
|
|
167
|
+
<UButton
|
|
168
|
+
icon="i-lucide-x"
|
|
169
|
+
variant="ghost"
|
|
170
|
+
size="xs"
|
|
171
|
+
aria-label="Close preview"
|
|
172
|
+
@click="selected = null"
|
|
173
|
+
/>
|
|
174
|
+
</div>
|
|
175
|
+
<p v-if="selected.description" class="text-xs text-muted mt-2">
|
|
176
|
+
{{ selected.description }}
|
|
177
|
+
</p>
|
|
178
|
+
</div>
|
|
179
|
+
<pre class="p-4 text-xs font-mono overflow-x-auto whitespace-pre">{{ selected.content }}</pre>
|
|
180
|
+
</div>
|
|
181
|
+
<div
|
|
182
|
+
v-else
|
|
183
|
+
class="rounded-lg border border-dashed border-default p-8 text-center text-sm text-muted self-start"
|
|
184
|
+
>
|
|
185
|
+
Click a row to preview its YAML.
|
|
186
|
+
</div>
|
|
187
|
+
</div>
|
|
188
|
+
</DashboardState>
|
|
189
|
+
</template>
|
|
190
|
+
</UDashboardPanel>
|
|
191
|
+
</template>
|
package/package.json
CHANGED
package/pyproject.toml
CHANGED
|
Binary file
|
package/scripts/dashboard-api.py
CHANGED
|
@@ -839,6 +839,23 @@ def knowledge_search(q: str = Query(...), top_k: int = Query(5)):
|
|
|
839
839
|
return {"results": results, "query": q, "total": len(results)}
|
|
840
840
|
|
|
841
841
|
|
|
842
|
+
@app.get("/api/knowledge/sources")
|
|
843
|
+
def knowledge_list_sources():
|
|
844
|
+
"""PR88c v3.25.0 — list every distinct source + chunk count.
|
|
845
|
+
|
|
846
|
+
Returns ``{sources: [{source, chunks}], total: N}``. Sorted
|
|
847
|
+
descending by chunk count.
|
|
848
|
+
"""
|
|
849
|
+
store = _get_vector_store()
|
|
850
|
+
if not store:
|
|
851
|
+
return {"sources": [], "total": 0, "error": "vector store unavailable"}
|
|
852
|
+
try:
|
|
853
|
+
rows = store.list_sources()
|
|
854
|
+
except Exception as exc: # noqa: BLE001
|
|
855
|
+
return {"sources": [], "total": 0, "error": str(exc)}
|
|
856
|
+
return {"sources": rows, "total": len(rows)}
|
|
857
|
+
|
|
858
|
+
|
|
842
859
|
@app.delete("/api/knowledge/sources")
|
|
843
860
|
def knowledge_delete_source(source: str = Query(...)):
|
|
844
861
|
"""PR71 v2.88.0 — remove all chunks from a given source.
|
|
@@ -1387,6 +1404,46 @@ def agent_export_to_vault(agent_id: str):
|
|
|
1387
1404
|
return {"exported": True, "path": str(res.path), "vault_path": str(res.vault_path)}
|
|
1388
1405
|
|
|
1389
1406
|
|
|
1407
|
+
# --- Workflows (PR88b v3.24.0) ---
|
|
1408
|
+
|
|
1409
|
+
@app.get("/api/workflows")
|
|
1410
|
+
def workflows_list():
|
|
1411
|
+
"""Scan departments/*/workflows/*.yaml and return metadata + content."""
|
|
1412
|
+
out: list[dict] = []
|
|
1413
|
+
dept_root = ARKAOS_ROOT / "departments"
|
|
1414
|
+
if not dept_root.exists():
|
|
1415
|
+
return {"workflows": []}
|
|
1416
|
+
try:
|
|
1417
|
+
import yaml as _yaml
|
|
1418
|
+
except ImportError:
|
|
1419
|
+
return {"workflows": [], "error": "PyYAML unavailable"}
|
|
1420
|
+
for path in sorted(dept_root.glob("*/workflows/*.yaml")):
|
|
1421
|
+
try:
|
|
1422
|
+
content = path.read_text(encoding="utf-8")
|
|
1423
|
+
except OSError:
|
|
1424
|
+
continue
|
|
1425
|
+
try:
|
|
1426
|
+
raw = _yaml.safe_load(content) or {}
|
|
1427
|
+
except Exception: # noqa: BLE001
|
|
1428
|
+
raw = {}
|
|
1429
|
+
if not isinstance(raw, dict):
|
|
1430
|
+
continue
|
|
1431
|
+
phases = raw.get("phases") if isinstance(raw.get("phases"), list) else []
|
|
1432
|
+
rel = path.relative_to(ARKAOS_ROOT).as_posix()
|
|
1433
|
+
out.append({
|
|
1434
|
+
"id": str(raw.get("id") or path.stem),
|
|
1435
|
+
"name": str(raw.get("name") or path.stem),
|
|
1436
|
+
"description": str(raw.get("description") or ""),
|
|
1437
|
+
"department": str(raw.get("department") or path.parent.parent.name),
|
|
1438
|
+
"tier": str(raw.get("tier") or ""),
|
|
1439
|
+
"command": str(raw.get("command") or ""),
|
|
1440
|
+
"phases_count": len(phases),
|
|
1441
|
+
"file": rel,
|
|
1442
|
+
"content": content,
|
|
1443
|
+
})
|
|
1444
|
+
return {"workflows": out}
|
|
1445
|
+
|
|
1446
|
+
|
|
1390
1447
|
# --- Sidebar stats widget (PR87d v3.22.0) ---
|
|
1391
1448
|
|
|
1392
1449
|
@app.get("/api/sidebar-stats")
|