arkaos 3.59.0 → 3.61.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__/favorites.cpython-313.pyc +0 -0
- package/core/favorites.py +33 -0
- package/dashboard/app/composables/useFavorites.ts +30 -1
- package/dashboard/app/pages/agents/index.vue +32 -0
- package/dashboard/app/pages/personas/[id].vue +51 -0
- package/dashboard/app/pages/personas/index.vue +32 -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 +84 -0
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
3.
|
|
1
|
+
3.61.0
|
|
Binary file
|
package/core/favorites.py
CHANGED
|
@@ -89,3 +89,36 @@ def set_favorite(kind: str, item_id: str, favorited: bool) -> dict:
|
|
|
89
89
|
state[kind] = bucket
|
|
90
90
|
_save(state)
|
|
91
91
|
return {"kind": kind, "id": item_id, "favorited": favorited}
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def set_many(kind: str, ids: list[str], favorited: bool) -> dict:
|
|
95
|
+
"""PR97c v3.61.0 — bulk-set favourite state for many ids.
|
|
96
|
+
|
|
97
|
+
Returns ``{kind, favorited, applied: N, total: N}`` where applied
|
|
98
|
+
counts how many ids actually changed state.
|
|
99
|
+
"""
|
|
100
|
+
if kind not in _VALID_KINDS:
|
|
101
|
+
return {"error": f"unknown kind: {kind!r}", "applied": 0, "total": 0}
|
|
102
|
+
if not isinstance(ids, list):
|
|
103
|
+
return {"error": "ids must be a list", "applied": 0, "total": 0}
|
|
104
|
+
state = _load()
|
|
105
|
+
bucket = state.setdefault(kind, [])
|
|
106
|
+
existing = set(bucket)
|
|
107
|
+
applied = 0
|
|
108
|
+
for item_id in ids:
|
|
109
|
+
if not isinstance(item_id, str) or not item_id:
|
|
110
|
+
continue
|
|
111
|
+
if favorited and item_id not in existing:
|
|
112
|
+
existing.add(item_id)
|
|
113
|
+
applied += 1
|
|
114
|
+
elif not favorited and item_id in existing:
|
|
115
|
+
existing.discard(item_id)
|
|
116
|
+
applied += 1
|
|
117
|
+
state[kind] = list(existing)
|
|
118
|
+
_save(state)
|
|
119
|
+
return {
|
|
120
|
+
"kind": kind,
|
|
121
|
+
"favorited": favorited,
|
|
122
|
+
"applied": applied,
|
|
123
|
+
"total": len([i for i in ids if isinstance(i, str) and i]),
|
|
124
|
+
}
|
|
@@ -43,6 +43,35 @@ const _useFavorites = () => {
|
|
|
43
43
|
return state.value.personas.includes(id)
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
// PR97c v3.61.0 — bulk set N ids in one POST.
|
|
47
|
+
async function setMany(
|
|
48
|
+
kind: 'agents' | 'personas',
|
|
49
|
+
ids: string[],
|
|
50
|
+
favorited: boolean,
|
|
51
|
+
) {
|
|
52
|
+
try {
|
|
53
|
+
const res = await $fetch<{
|
|
54
|
+
applied?: number
|
|
55
|
+
total?: number
|
|
56
|
+
error?: string
|
|
57
|
+
}>(`${apiBase}/api/favorites/bulk`, {
|
|
58
|
+
method: 'POST',
|
|
59
|
+
body: { kind, ids, favorited },
|
|
60
|
+
})
|
|
61
|
+
if (res.error) throw new Error(res.error)
|
|
62
|
+
// Sync local state with the new server truth.
|
|
63
|
+
await load(true)
|
|
64
|
+
return res.applied ?? 0
|
|
65
|
+
} catch (err) {
|
|
66
|
+
toast.add({
|
|
67
|
+
title: 'Favorites bulk update failed',
|
|
68
|
+
description: err instanceof Error ? err.message : 'unknown error',
|
|
69
|
+
color: 'error',
|
|
70
|
+
})
|
|
71
|
+
return null
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
46
75
|
async function toggle(kind: 'agents' | 'personas', id: string) {
|
|
47
76
|
try {
|
|
48
77
|
const res = await $fetch<{ favorited?: boolean, error?: string }>(
|
|
@@ -67,7 +96,7 @@ const _useFavorites = () => {
|
|
|
67
96
|
}
|
|
68
97
|
}
|
|
69
98
|
|
|
70
|
-
return { state, load, isAgentFavorite, isPersonaFavorite, toggle, loaded }
|
|
99
|
+
return { state, load, isAgentFavorite, isPersonaFavorite, toggle, setMany, loaded }
|
|
71
100
|
}
|
|
72
101
|
|
|
73
102
|
export const useFavorites = createSharedComposable(_useFavorites)
|
|
@@ -416,6 +416,22 @@ async function bulkDelete() {
|
|
|
416
416
|
await refreshAll()
|
|
417
417
|
}
|
|
418
418
|
|
|
419
|
+
// PR97c v3.61.0 — bulk star / unstar selected agents.
|
|
420
|
+
async function bulkStar(favorited: boolean) {
|
|
421
|
+
if (selected.value.size === 0) return
|
|
422
|
+
const ids = Array.from(selected.value)
|
|
423
|
+
const applied = await favs.setMany('agents', ids, favorited)
|
|
424
|
+
if (applied === null) return
|
|
425
|
+
toast.add({
|
|
426
|
+
title: favorited
|
|
427
|
+
? `Starred ${applied} agent${applied === 1 ? '' : 's'}`
|
|
428
|
+
: `Unstarred ${applied} agent${applied === 1 ? '' : 's'}`,
|
|
429
|
+
description: applied < ids.length ? `${ids.length - applied} already in state` : undefined,
|
|
430
|
+
color: 'success',
|
|
431
|
+
icon: favorited ? 'i-lucide-star' : 'i-lucide-star-off',
|
|
432
|
+
})
|
|
433
|
+
}
|
|
434
|
+
|
|
419
435
|
async function undoTrashIds(ids: string[]) {
|
|
420
436
|
const results = await Promise.allSettled(
|
|
421
437
|
ids.map((tid) =>
|
|
@@ -636,6 +652,22 @@ async function undoTrashIds(ids: string[]) {
|
|
|
636
652
|
@click="clearSelection"
|
|
637
653
|
/>
|
|
638
654
|
<div class="h-5 w-px bg-default" />
|
|
655
|
+
<UButton
|
|
656
|
+
icon="i-lucide-star"
|
|
657
|
+
size="sm"
|
|
658
|
+
variant="soft"
|
|
659
|
+
color="warning"
|
|
660
|
+
aria-label="Star selected"
|
|
661
|
+
@click="bulkStar(true)"
|
|
662
|
+
/>
|
|
663
|
+
<UButton
|
|
664
|
+
icon="i-lucide-star-off"
|
|
665
|
+
size="sm"
|
|
666
|
+
variant="ghost"
|
|
667
|
+
color="neutral"
|
|
668
|
+
aria-label="Unstar selected"
|
|
669
|
+
@click="bulkStar(false)"
|
|
670
|
+
/>
|
|
639
671
|
<UButton
|
|
640
672
|
label="Compare"
|
|
641
673
|
icon="i-lucide-columns-2"
|
|
@@ -226,6 +226,19 @@ function csvToList(value: string): string[] {
|
|
|
226
226
|
type SuggestField = 'mental_models' | 'frameworks' | 'expertise_domains' | 'communication_avoid' | 'key_quotes'
|
|
227
227
|
const suggestingField = ref<SuggestField | null>(null)
|
|
228
228
|
|
|
229
|
+
// PR97b v3.60.0 — weekly usage timeline (when did agents clone from here).
|
|
230
|
+
interface UsageWeek { week_start: string, count: number }
|
|
231
|
+
const { data: usageTimelineData } = fetchApi<{
|
|
232
|
+
weeks: UsageWeek[]
|
|
233
|
+
total_agents: number
|
|
234
|
+
period_weeks: number
|
|
235
|
+
}>(`/api/personas/${personaId}/usage-timeline?weeks=12`)
|
|
236
|
+
const usageWeeks = computed<UsageWeek[]>(() => usageTimelineData.value?.weeks ?? [])
|
|
237
|
+
const usageMaxCount = computed(() =>
|
|
238
|
+
Math.max(1, usageWeeks.value.reduce((acc, w) => Math.max(acc, w.count), 0)),
|
|
239
|
+
)
|
|
240
|
+
const usageTotalLinks = computed(() => usageTimelineData.value?.total_agents ?? 0)
|
|
241
|
+
|
|
229
242
|
// PR86a v3.15.0 — favorites.
|
|
230
243
|
const favs = useFavorites()
|
|
231
244
|
await favs.load()
|
|
@@ -650,6 +663,44 @@ const vocabOptions = [
|
|
|
650
663
|
</div>
|
|
651
664
|
</section>
|
|
652
665
|
|
|
666
|
+
<!-- PR97b v3.60.0 — usage timeline (when agents linked) -->
|
|
667
|
+
<section
|
|
668
|
+
v-if="usageTotalLinks > 0"
|
|
669
|
+
class="rounded-xl border border-default bg-elevated/10 p-5"
|
|
670
|
+
>
|
|
671
|
+
<div class="flex items-center justify-between text-xs mb-2">
|
|
672
|
+
<span class="font-semibold text-muted uppercase tracking-wide">
|
|
673
|
+
Usage timeline (12 weeks)
|
|
674
|
+
</span>
|
|
675
|
+
<span class="font-mono text-muted">
|
|
676
|
+
{{ usageTotalLinks }} agent{{ usageTotalLinks === 1 ? '' : 's' }} linking · peak {{ usageMaxCount }}/wk
|
|
677
|
+
</span>
|
|
678
|
+
</div>
|
|
679
|
+
<svg
|
|
680
|
+
:viewBox="`0 0 ${usageWeeks.length * 16} 48`"
|
|
681
|
+
class="w-full h-12"
|
|
682
|
+
preserveAspectRatio="none"
|
|
683
|
+
>
|
|
684
|
+
<rect
|
|
685
|
+
v-for="(w, idx) in usageWeeks"
|
|
686
|
+
:key="w.week_start"
|
|
687
|
+
:x="idx * 16 + 2"
|
|
688
|
+
:y="48 - (w.count / usageMaxCount) * 46"
|
|
689
|
+
width="12"
|
|
690
|
+
:height="(w.count / usageMaxCount) * 46"
|
|
691
|
+
class="fill-primary"
|
|
692
|
+
:class="w.count === 0 ? 'opacity-20' : 'opacity-90'"
|
|
693
|
+
>
|
|
694
|
+
<title>{{ w.week_start }} · {{ w.count }} agent{{ w.count === 1 ? '' : 's' }} linked</title>
|
|
695
|
+
</rect>
|
|
696
|
+
</svg>
|
|
697
|
+
<p class="text-xs text-muted mt-1.5">
|
|
698
|
+
Buckets reflect the YAML mtime of agents that currently link to
|
|
699
|
+
this persona — approximation of when they were cloned / edited
|
|
700
|
+
to depend on this profile.
|
|
701
|
+
</p>
|
|
702
|
+
</section>
|
|
703
|
+
|
|
653
704
|
<!-- BIO (PR86d) -->
|
|
654
705
|
<section
|
|
655
706
|
v-if="(detail as any).bio_md"
|
|
@@ -418,6 +418,22 @@ async function bulkDelete() {
|
|
|
418
418
|
await refreshAll()
|
|
419
419
|
}
|
|
420
420
|
|
|
421
|
+
// PR97c v3.61.0 — bulk star / unstar selected personas.
|
|
422
|
+
async function bulkStar(favorited: boolean) {
|
|
423
|
+
if (selected.value.size === 0) return
|
|
424
|
+
const ids = Array.from(selected.value)
|
|
425
|
+
const applied = await favs.setMany('personas', ids, favorited)
|
|
426
|
+
if (applied === null) return
|
|
427
|
+
toast.add({
|
|
428
|
+
title: favorited
|
|
429
|
+
? `Starred ${applied} persona${applied === 1 ? '' : 's'}`
|
|
430
|
+
: `Unstarred ${applied} persona${applied === 1 ? '' : 's'}`,
|
|
431
|
+
description: applied < ids.length ? `${ids.length - applied} already in state` : undefined,
|
|
432
|
+
color: 'success',
|
|
433
|
+
icon: favorited ? 'i-lucide-star' : 'i-lucide-star-off',
|
|
434
|
+
})
|
|
435
|
+
}
|
|
436
|
+
|
|
421
437
|
async function undoTrashIds(ids: string[]) {
|
|
422
438
|
const results = await Promise.allSettled(
|
|
423
439
|
ids.map((tid) =>
|
|
@@ -721,6 +737,22 @@ async function undoTrashIds(ids: string[]) {
|
|
|
721
737
|
@click="clearSelection"
|
|
722
738
|
/>
|
|
723
739
|
<div class="h-5 w-px bg-default" />
|
|
740
|
+
<UButton
|
|
741
|
+
icon="i-lucide-star"
|
|
742
|
+
size="sm"
|
|
743
|
+
variant="soft"
|
|
744
|
+
color="warning"
|
|
745
|
+
aria-label="Star selected"
|
|
746
|
+
@click="bulkStar(true)"
|
|
747
|
+
/>
|
|
748
|
+
<UButton
|
|
749
|
+
icon="i-lucide-star-off"
|
|
750
|
+
size="sm"
|
|
751
|
+
variant="ghost"
|
|
752
|
+
color="neutral"
|
|
753
|
+
aria-label="Unstar selected"
|
|
754
|
+
@click="bulkStar(false)"
|
|
755
|
+
/>
|
|
724
756
|
<UButton
|
|
725
757
|
label="Export ZIP"
|
|
726
758
|
icon="i-lucide-archive"
|
package/package.json
CHANGED
package/pyproject.toml
CHANGED
|
Binary file
|
package/scripts/dashboard-api.py
CHANGED
|
@@ -1371,6 +1371,74 @@ def _obsidian_store_available() -> bool:
|
|
|
1371
1371
|
return False
|
|
1372
1372
|
|
|
1373
1373
|
|
|
1374
|
+
@app.get("/api/personas/{persona_id}/usage-timeline")
|
|
1375
|
+
def persona_usage_timeline(persona_id: str, weeks: int = 12):
|
|
1376
|
+
"""PR97b v3.60.0 — histogram of agent YAML mtimes for agents that
|
|
1377
|
+
link to this persona. Approximation of "when did people clone /
|
|
1378
|
+
create agents from this persona over time".
|
|
1379
|
+
|
|
1380
|
+
Returns ``{weeks: [{week_start, count}], total_agents, period_weeks}``
|
|
1381
|
+
bucketed by ISO week start (Monday). Capped at 52 weeks.
|
|
1382
|
+
|
|
1383
|
+
Uses filesystem mtime for the agent YAML — works even without a
|
|
1384
|
+
git history.
|
|
1385
|
+
"""
|
|
1386
|
+
try:
|
|
1387
|
+
weeks_int = int(weeks) if weeks is not None else 12
|
|
1388
|
+
except (TypeError, ValueError):
|
|
1389
|
+
weeks_int = 12
|
|
1390
|
+
capped_weeks = max(1, min(weeks_int, 52))
|
|
1391
|
+
|
|
1392
|
+
try:
|
|
1393
|
+
import yaml as _yaml
|
|
1394
|
+
except ImportError:
|
|
1395
|
+
return {"weeks": [], "total_agents": 0, "period_weeks": capped_weeks}
|
|
1396
|
+
|
|
1397
|
+
dept_root = ARKAOS_ROOT / "departments"
|
|
1398
|
+
if not dept_root.exists():
|
|
1399
|
+
return {"weeks": [], "total_agents": 0, "period_weeks": capped_weeks}
|
|
1400
|
+
|
|
1401
|
+
from datetime import datetime, timedelta, timezone
|
|
1402
|
+
now = datetime.now(timezone.utc)
|
|
1403
|
+
today = now.date()
|
|
1404
|
+
# Monday of current ISO week.
|
|
1405
|
+
current_monday = today - timedelta(days=today.weekday())
|
|
1406
|
+
buckets: dict[str, int] = {}
|
|
1407
|
+
for offset in range(capped_weeks):
|
|
1408
|
+
m = current_monday - timedelta(weeks=capped_weeks - 1 - offset)
|
|
1409
|
+
buckets[m.isoformat()] = 0
|
|
1410
|
+
cutoff_monday = current_monday - timedelta(weeks=capped_weeks - 1)
|
|
1411
|
+
|
|
1412
|
+
linked_total = 0
|
|
1413
|
+
for path in dept_root.glob("*/agents/*.yaml"):
|
|
1414
|
+
try:
|
|
1415
|
+
raw = _yaml.safe_load(path.read_text(encoding="utf-8")) or {}
|
|
1416
|
+
except Exception: # noqa: BLE001
|
|
1417
|
+
continue
|
|
1418
|
+
if not isinstance(raw, dict):
|
|
1419
|
+
continue
|
|
1420
|
+
linked = raw.get("linked_personas") or []
|
|
1421
|
+
if not isinstance(linked, list) or persona_id not in linked:
|
|
1422
|
+
continue
|
|
1423
|
+
linked_total += 1
|
|
1424
|
+
try:
|
|
1425
|
+
mtime = datetime.fromtimestamp(path.stat().st_mtime, tz=timezone.utc).date()
|
|
1426
|
+
except OSError:
|
|
1427
|
+
continue
|
|
1428
|
+
if mtime < cutoff_monday:
|
|
1429
|
+
continue
|
|
1430
|
+
monday = mtime - timedelta(days=mtime.weekday())
|
|
1431
|
+
key = monday.isoformat()
|
|
1432
|
+
if key in buckets:
|
|
1433
|
+
buckets[key] += 1
|
|
1434
|
+
|
|
1435
|
+
out = [
|
|
1436
|
+
{"week_start": k, "count": buckets[k]}
|
|
1437
|
+
for k in sorted(buckets.keys())
|
|
1438
|
+
]
|
|
1439
|
+
return {"weeks": out, "total_agents": linked_total, "period_weeks": capped_weeks}
|
|
1440
|
+
|
|
1441
|
+
|
|
1374
1442
|
@app.get("/api/personas/usage")
|
|
1375
1443
|
def personas_usage():
|
|
1376
1444
|
"""PR77 v2.95.0 — reverse lookup: how many agents link to each
|
|
@@ -2539,6 +2607,22 @@ def favorites_toggle(kind: str, item_id: str):
|
|
|
2539
2607
|
return _fav.toggle(kind, item_id)
|
|
2540
2608
|
|
|
2541
2609
|
|
|
2610
|
+
@app.post("/api/favorites/bulk")
|
|
2611
|
+
def favorites_bulk(body: dict):
|
|
2612
|
+
"""PR97c v3.61.0 — bulk star/unstar many ids in one shot.
|
|
2613
|
+
|
|
2614
|
+
Body: ``{"kind": "agents"|"personas", "ids": [...], "favorited": bool}``
|
|
2615
|
+
Returns ``{kind, favorited, applied, total}``.
|
|
2616
|
+
"""
|
|
2617
|
+
if not isinstance(body, dict):
|
|
2618
|
+
return {"error": "body must be an object"}
|
|
2619
|
+
kind = (body.get("kind") or "").strip()
|
|
2620
|
+
ids = body.get("ids") or []
|
|
2621
|
+
favorited = bool(body.get("favorited"))
|
|
2622
|
+
from core import favorites as _fav
|
|
2623
|
+
return _fav.set_many(kind, ids if isinstance(ids, list) else [], favorited)
|
|
2624
|
+
|
|
2625
|
+
|
|
2542
2626
|
# --- Global search (PR85d v3.14.0) ---
|
|
2543
2627
|
|
|
2544
2628
|
@app.get("/api/search")
|