arkaos 3.34.0 → 3.36.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/dashboard/app/components/AgentSuggestionsCard.vue +62 -0
- package/dashboard/app/pages/index.vue +3 -0
- package/dashboard/app/pages/personas/index.vue +89 -8
- 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 +116 -7
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
3.
|
|
1
|
+
3.36.0
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// PR91a v3.35.0 — "What's missing?" home page card.
|
|
3
|
+
//
|
|
4
|
+
// Lists empty departments (high severity) and depts missing a Tier 2
|
|
5
|
+
// specialist (medium). Each suggestion is a link to /agents/new.
|
|
6
|
+
|
|
7
|
+
interface Suggestion {
|
|
8
|
+
department: string
|
|
9
|
+
reason: string
|
|
10
|
+
recommended_tier: 1 | 2
|
|
11
|
+
severity: 'high' | 'medium'
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const { fetchApi } = useApi()
|
|
15
|
+
const { data, status } = fetchApi<{ suggestions: Suggestion[], total_gaps: number }>(
|
|
16
|
+
'/api/agents/suggestions?limit=6',
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
const suggestions = computed<Suggestion[]>(() => data.value?.suggestions ?? [])
|
|
20
|
+
|
|
21
|
+
function severityColor(s: string): 'error' | 'warning' | 'neutral' {
|
|
22
|
+
return s === 'high' ? 'error' : s === 'medium' ? 'warning' : 'neutral'
|
|
23
|
+
}
|
|
24
|
+
</script>
|
|
25
|
+
|
|
26
|
+
<template>
|
|
27
|
+
<UCard v-if="status !== 'pending' && suggestions.length > 0">
|
|
28
|
+
<template #header>
|
|
29
|
+
<div class="flex items-center justify-between">
|
|
30
|
+
<div>
|
|
31
|
+
<h3 class="text-lg font-semibold">What's missing?</h3>
|
|
32
|
+
<p class="text-xs text-muted mt-0.5">
|
|
33
|
+
{{ data?.total_gaps }} gap{{ data?.total_gaps === 1 ? '' : 's' }} across departments. Showing top {{ suggestions.length }}.
|
|
34
|
+
</p>
|
|
35
|
+
</div>
|
|
36
|
+
<UIcon name="i-lucide-sparkles" class="size-5 text-primary" />
|
|
37
|
+
</div>
|
|
38
|
+
</template>
|
|
39
|
+
|
|
40
|
+
<div class="grid grid-cols-1 md:grid-cols-2 gap-2">
|
|
41
|
+
<NuxtLink
|
|
42
|
+
v-for="s in suggestions"
|
|
43
|
+
:key="`${s.department}:${s.recommended_tier}`"
|
|
44
|
+
to="/agents/new"
|
|
45
|
+
class="flex items-center gap-3 rounded-lg border border-default p-3 hover:border-primary/40 transition-colors"
|
|
46
|
+
>
|
|
47
|
+
<UBadge
|
|
48
|
+
:label="s.severity"
|
|
49
|
+
:color="severityColor(s.severity)"
|
|
50
|
+
variant="subtle"
|
|
51
|
+
size="xs"
|
|
52
|
+
/>
|
|
53
|
+
<div class="flex-1 min-w-0">
|
|
54
|
+
<p class="text-sm font-semibold capitalize truncate">{{ s.department }}</p>
|
|
55
|
+
<p class="text-xs text-muted truncate">{{ s.reason }}</p>
|
|
56
|
+
</div>
|
|
57
|
+
<span class="text-xs font-mono text-muted shrink-0">T{{ s.recommended_tier }}</span>
|
|
58
|
+
<UIcon name="i-lucide-arrow-right" class="size-4 text-muted shrink-0" />
|
|
59
|
+
</NuxtLink>
|
|
60
|
+
</div>
|
|
61
|
+
</UCard>
|
|
62
|
+
</template>
|
|
@@ -204,6 +204,9 @@ function copyCommand(cmd: string) {
|
|
|
204
204
|
</div>
|
|
205
205
|
</UCard>
|
|
206
206
|
|
|
207
|
+
<!-- PR91a v3.35.0 — Agent gap suggestions -->
|
|
208
|
+
<AgentSuggestionsCard class="mb-6" />
|
|
209
|
+
|
|
207
210
|
<!-- PR84d v3.10.0 — Top departments + Recent personas row -->
|
|
208
211
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
|
|
209
212
|
<div>
|
|
@@ -143,13 +143,55 @@ await favs.load()
|
|
|
143
143
|
const favoritesOnly = ref(false)
|
|
144
144
|
|
|
145
145
|
// PR87b v3.20.0 — import .md persona files.
|
|
146
|
+
// PR91b v3.36.0 — extended with URL import.
|
|
146
147
|
const importInput = ref<HTMLInputElement | null>(null)
|
|
147
148
|
const importing = ref(false)
|
|
149
|
+
const urlImportOpen = ref(false)
|
|
150
|
+
const urlImportText = ref('')
|
|
148
151
|
|
|
149
152
|
function triggerImport() {
|
|
150
153
|
importInput.value?.click()
|
|
151
154
|
}
|
|
152
155
|
|
|
156
|
+
async function runUrlImport() {
|
|
157
|
+
const urls = urlImportText.value
|
|
158
|
+
.split('\n')
|
|
159
|
+
.map((s) => s.trim())
|
|
160
|
+
.filter(Boolean)
|
|
161
|
+
if (urls.length === 0) return
|
|
162
|
+
importing.value = true
|
|
163
|
+
try {
|
|
164
|
+
const res = await $fetch<{
|
|
165
|
+
imported: number
|
|
166
|
+
failed: number
|
|
167
|
+
results: Array<{ filename: string, status: string, id?: string, error?: string }>
|
|
168
|
+
error?: string
|
|
169
|
+
}>(`${apiBase}/api/personas/import`, { method: 'POST', body: { urls } })
|
|
170
|
+
if (res.error) throw new Error(res.error)
|
|
171
|
+
toast.add({
|
|
172
|
+
title: res.imported > 0
|
|
173
|
+
? `Imported ${res.imported} persona${res.imported === 1 ? '' : 's'}`
|
|
174
|
+
: 'Nothing imported',
|
|
175
|
+
description: res.failed > 0 ? `${res.failed} failed` : undefined,
|
|
176
|
+
color: res.imported > 0 && res.failed === 0
|
|
177
|
+
? 'success'
|
|
178
|
+
: res.imported > 0 ? 'warning' : 'error',
|
|
179
|
+
icon: 'i-lucide-globe',
|
|
180
|
+
})
|
|
181
|
+
await refreshAll()
|
|
182
|
+
urlImportText.value = ''
|
|
183
|
+
urlImportOpen.value = false
|
|
184
|
+
} catch (err) {
|
|
185
|
+
toast.add({
|
|
186
|
+
title: 'URL import failed',
|
|
187
|
+
description: err instanceof Error ? err.message : 'unknown error',
|
|
188
|
+
color: 'error',
|
|
189
|
+
})
|
|
190
|
+
} finally {
|
|
191
|
+
importing.value = false
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
153
195
|
async function onImportFiles(event: Event) {
|
|
154
196
|
const target = event.target as HTMLInputElement
|
|
155
197
|
const files = Array.from(target.files ?? [])
|
|
@@ -312,14 +354,21 @@ async function undoTrashIds(ids: string[]) {
|
|
|
312
354
|
/>
|
|
313
355
|
</template>
|
|
314
356
|
<template #right>
|
|
315
|
-
<
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
357
|
+
<UDropdownMenu
|
|
358
|
+
:items="[
|
|
359
|
+
{ label: 'Pick .md files…', icon: 'i-lucide-file-up', onSelect: triggerImport },
|
|
360
|
+
{ label: 'From URLs…', icon: 'i-lucide-globe', onSelect: () => urlImportOpen = true },
|
|
361
|
+
]"
|
|
362
|
+
>
|
|
363
|
+
<UButton
|
|
364
|
+
label="Import"
|
|
365
|
+
icon="i-lucide-file-up"
|
|
366
|
+
variant="soft"
|
|
367
|
+
size="sm"
|
|
368
|
+
:loading="importing"
|
|
369
|
+
trailing-icon="i-lucide-chevron-down"
|
|
370
|
+
/>
|
|
371
|
+
</UDropdownMenu>
|
|
323
372
|
<input
|
|
324
373
|
ref="importInput"
|
|
325
374
|
type="file"
|
|
@@ -328,6 +377,38 @@ async function undoTrashIds(ids: string[]) {
|
|
|
328
377
|
class="hidden"
|
|
329
378
|
@change="onImportFiles"
|
|
330
379
|
/>
|
|
380
|
+
<UModal v-model:open="urlImportOpen" title="Import from URLs">
|
|
381
|
+
<template #content>
|
|
382
|
+
<UCard>
|
|
383
|
+
<template #header>
|
|
384
|
+
<h2 class="text-lg font-bold">Import personas from URLs</h2>
|
|
385
|
+
<p class="text-xs text-muted mt-0.5">
|
|
386
|
+
One raw .md URL per line. Files must have YAML
|
|
387
|
+
frontmatter with <code>type: persona</code>.
|
|
388
|
+
</p>
|
|
389
|
+
</template>
|
|
390
|
+
<UTextarea
|
|
391
|
+
v-model="urlImportText"
|
|
392
|
+
:rows="6"
|
|
393
|
+
placeholder="https://raw.githubusercontent.com/owner/repo/main/personas/alex.md"
|
|
394
|
+
class="w-full font-mono text-sm"
|
|
395
|
+
/>
|
|
396
|
+
<template #footer>
|
|
397
|
+
<div class="flex items-center justify-end gap-2">
|
|
398
|
+
<UButton label="Cancel" variant="ghost" :disabled="importing" @click="urlImportOpen = false" />
|
|
399
|
+
<UButton
|
|
400
|
+
label="Import"
|
|
401
|
+
icon="i-lucide-globe"
|
|
402
|
+
color="primary"
|
|
403
|
+
:loading="importing"
|
|
404
|
+
:disabled="!urlImportText.trim()"
|
|
405
|
+
@click="runUrlImport"
|
|
406
|
+
/>
|
|
407
|
+
</div>
|
|
408
|
+
</template>
|
|
409
|
+
</UCard>
|
|
410
|
+
</template>
|
|
411
|
+
</UModal>
|
|
331
412
|
<UButton
|
|
332
413
|
label="New Persona"
|
|
333
414
|
icon="i-lucide-plus"
|
package/package.json
CHANGED
package/pyproject.toml
CHANGED
|
Binary file
|
package/scripts/dashboard-api.py
CHANGED
|
@@ -1567,6 +1567,63 @@ def agent_export_to_vault(agent_id: str):
|
|
|
1567
1567
|
return {"exported": True, "path": str(res.path), "vault_path": str(res.vault_path)}
|
|
1568
1568
|
|
|
1569
1569
|
|
|
1570
|
+
# --- Agent gap suggestions (PR91a v3.35.0) ---
|
|
1571
|
+
|
|
1572
|
+
_KNOWN_DEPARTMENTS = (
|
|
1573
|
+
"dev", "marketing", "brand", "finance", "strategy", "ecom", "kb",
|
|
1574
|
+
"ops", "pm", "saas", "landing", "content", "community", "sales",
|
|
1575
|
+
"leadership", "org",
|
|
1576
|
+
)
|
|
1577
|
+
|
|
1578
|
+
|
|
1579
|
+
@app.get("/api/agents/suggestions")
|
|
1580
|
+
def agents_suggestions(limit: int = 6):
|
|
1581
|
+
"""PR91a v3.35.0 — recommend agents the operator should consider creating.
|
|
1582
|
+
|
|
1583
|
+
Heuristics:
|
|
1584
|
+
1. Departments with zero agents at all (severity: high).
|
|
1585
|
+
2. Departments missing a Tier 2 specialist (severity: medium).
|
|
1586
|
+
|
|
1587
|
+
Each suggestion includes ``reason`` and ``recommended_tier``. Used
|
|
1588
|
+
by the home page "What's missing?" card.
|
|
1589
|
+
"""
|
|
1590
|
+
agents = _load_agents()
|
|
1591
|
+
by_dept: dict[str, dict] = {}
|
|
1592
|
+
for a in agents:
|
|
1593
|
+
dept = a.get("department") or ""
|
|
1594
|
+
if not dept:
|
|
1595
|
+
continue
|
|
1596
|
+
row = by_dept.setdefault(dept, {"count": 0, "tiers": set()})
|
|
1597
|
+
row["count"] += 1
|
|
1598
|
+
try:
|
|
1599
|
+
row["tiers"].add(int(a.get("tier") or 99))
|
|
1600
|
+
except (TypeError, ValueError):
|
|
1601
|
+
pass
|
|
1602
|
+
|
|
1603
|
+
suggestions: list[dict] = []
|
|
1604
|
+
for dept in _KNOWN_DEPARTMENTS:
|
|
1605
|
+
info = by_dept.get(dept)
|
|
1606
|
+
if info is None or info["count"] == 0:
|
|
1607
|
+
suggestions.append({
|
|
1608
|
+
"department": dept,
|
|
1609
|
+
"reason": "no agents — department is empty",
|
|
1610
|
+
"recommended_tier": 1,
|
|
1611
|
+
"severity": "high",
|
|
1612
|
+
})
|
|
1613
|
+
continue
|
|
1614
|
+
if 2 not in info["tiers"]:
|
|
1615
|
+
suggestions.append({
|
|
1616
|
+
"department": dept,
|
|
1617
|
+
"reason": "no Tier 2 specialist",
|
|
1618
|
+
"recommended_tier": 2,
|
|
1619
|
+
"severity": "medium",
|
|
1620
|
+
})
|
|
1621
|
+
return {
|
|
1622
|
+
"suggestions": suggestions[: max(0, int(limit))],
|
|
1623
|
+
"total_gaps": len(suggestions),
|
|
1624
|
+
}
|
|
1625
|
+
|
|
1626
|
+
|
|
1570
1627
|
# --- Departments (PR89a v3.27.0) ---
|
|
1571
1628
|
|
|
1572
1629
|
@app.get("/api/departments")
|
|
@@ -1947,16 +2004,24 @@ def global_search(q: str = "", limit: int = 20):
|
|
|
1947
2004
|
def personas_import(body: dict):
|
|
1948
2005
|
"""Import persona Markdown files (frontmatter + body) into the store.
|
|
1949
2006
|
|
|
1950
|
-
Body:
|
|
1951
|
-
|
|
2007
|
+
Body:
|
|
2008
|
+
{"files": [{"name": "Alex.md", "content": "..."}]} # local picker
|
|
2009
|
+
{"urls": ["https://example.com/raw.md", ...]} # remote import
|
|
2010
|
+
|
|
2011
|
+
Both keys are accepted; URLs are fetched server-side (PR91b
|
|
2012
|
+
v3.36.0) and converted into the same `{name, content}` shape
|
|
2013
|
+
before processing.
|
|
1952
2014
|
|
|
1953
|
-
|
|
1954
|
-
lacking the frontmatter are flagged as failed without partial
|
|
1955
|
-
side-effects.
|
|
2015
|
+
Returns: {imported, failed, results: [{filename, status, id?, error?}]}
|
|
1956
2016
|
"""
|
|
1957
|
-
|
|
1958
|
-
|
|
2017
|
+
raw_files = body.get("files")
|
|
2018
|
+
raw_urls = body.get("urls")
|
|
2019
|
+
if raw_files is not None and not isinstance(raw_files, list):
|
|
1959
2020
|
return {"error": "files must be a list"}
|
|
2021
|
+
if raw_urls is not None and not isinstance(raw_urls, list):
|
|
2022
|
+
return {"error": "urls must be a list"}
|
|
2023
|
+
files = list(raw_files or [])
|
|
2024
|
+
urls = list(raw_urls or [])
|
|
1960
2025
|
mgr = _get_persona_manager()
|
|
1961
2026
|
if not mgr:
|
|
1962
2027
|
return {"error": "Persona manager unavailable"}
|
|
@@ -1965,6 +2030,10 @@ def personas_import(body: dict):
|
|
|
1965
2030
|
|
|
1966
2031
|
from core.personas.obsidian_store import ObsidianPersonaStore
|
|
1967
2032
|
|
|
2033
|
+
# Resolve URLs into {name, content} entries.
|
|
2034
|
+
if urls:
|
|
2035
|
+
files.extend(_fetch_url_entries(urls))
|
|
2036
|
+
|
|
1968
2037
|
imported = 0
|
|
1969
2038
|
failed = 0
|
|
1970
2039
|
results: list[dict] = []
|
|
@@ -1975,6 +2044,14 @@ def personas_import(body: dict):
|
|
|
1975
2044
|
continue
|
|
1976
2045
|
filename = str(entry.get("name") or "")
|
|
1977
2046
|
content = str(entry.get("content") or "")
|
|
2047
|
+
# Carry forward URL-fetch errors so the operator sees them.
|
|
2048
|
+
if entry.get("fetch_error"):
|
|
2049
|
+
failed += 1
|
|
2050
|
+
results.append({
|
|
2051
|
+
"filename": filename, "status": "failed",
|
|
2052
|
+
"error": str(entry["fetch_error"]),
|
|
2053
|
+
})
|
|
2054
|
+
continue
|
|
1978
2055
|
if not content.strip():
|
|
1979
2056
|
failed += 1
|
|
1980
2057
|
results.append({"filename": filename, "status": "failed", "error": "empty content"})
|
|
@@ -2001,6 +2078,38 @@ def personas_import(body: dict):
|
|
|
2001
2078
|
return {"imported": imported, "failed": failed, "results": results}
|
|
2002
2079
|
|
|
2003
2080
|
|
|
2081
|
+
def _fetch_url_entries(urls: list[str]) -> list[dict]:
|
|
2082
|
+
"""Fetch each URL and return ``{name, content}`` entries. PR91b."""
|
|
2083
|
+
import urllib.error
|
|
2084
|
+
import urllib.parse
|
|
2085
|
+
import urllib.request
|
|
2086
|
+
out: list[dict] = []
|
|
2087
|
+
for raw in urls:
|
|
2088
|
+
url = str(raw or "").strip()
|
|
2089
|
+
if not url:
|
|
2090
|
+
continue
|
|
2091
|
+
parsed = urllib.parse.urlparse(url)
|
|
2092
|
+
if parsed.scheme not in ("http", "https"):
|
|
2093
|
+
out.append({"name": url, "fetch_error": "scheme must be http(s)"})
|
|
2094
|
+
continue
|
|
2095
|
+
try:
|
|
2096
|
+
req = urllib.request.Request(url, headers={"User-Agent": "ArkaOS/persona-import"})
|
|
2097
|
+
with urllib.request.urlopen(req, timeout=10) as resp:
|
|
2098
|
+
content = resp.read().decode("utf-8", errors="replace")
|
|
2099
|
+
except urllib.error.URLError as exc:
|
|
2100
|
+
out.append({"name": url, "fetch_error": f"fetch failed: {exc.reason}"})
|
|
2101
|
+
continue
|
|
2102
|
+
except (TimeoutError, OSError) as exc:
|
|
2103
|
+
out.append({"name": url, "fetch_error": f"fetch failed: {exc}"})
|
|
2104
|
+
continue
|
|
2105
|
+
# Derive a filename from the URL path.
|
|
2106
|
+
name = (parsed.path.rsplit("/", 1)[-1] or "imported.md").strip()
|
|
2107
|
+
if not name.endswith(".md"):
|
|
2108
|
+
name = f"{name or 'imported'}.md"
|
|
2109
|
+
out.append({"name": name, "content": content})
|
|
2110
|
+
return out
|
|
2111
|
+
|
|
2112
|
+
|
|
2004
2113
|
# --- Trash / Undo (PR85b v3.12.0) ---
|
|
2005
2114
|
|
|
2006
2115
|
@app.get("/api/trash")
|