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 CHANGED
@@ -1 +1 @@
1
- 3.59.0
1
+ 3.61.0
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arkaos",
3
- "version": "3.59.0",
3
+ "version": "3.61.0",
4
4
  "description": "The Operating System for AI Agent Teams",
5
5
  "type": "module",
6
6
  "bin": {
package/pyproject.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "arkaos-core"
3
- version = "3.59.0"
3
+ version = "3.61.0"
4
4
  description = "Core engine for ArkaOS — The Operating System for AI Agent Teams"
5
5
  readme = "README.md"
6
6
  license = {text = "MIT"}
@@ -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")