arkaos 3.8.0 → 3.10.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.8.0
1
+ 3.10.0
@@ -29,6 +29,23 @@ interface QuickAction {
29
29
  description: string
30
30
  }
31
31
 
32
+ interface TopDeptRow {
33
+ department: string
34
+ calls: number
35
+ cost_usd: number | null
36
+ tokens_in: number
37
+ tokens_out: number
38
+ }
39
+
40
+ interface RecentPersonaRow {
41
+ id: string
42
+ name: string
43
+ title: string
44
+ mbti: string
45
+ source_store: string
46
+ created_at: string | null
47
+ }
48
+
32
49
  interface CommandCenterPayload {
33
50
  greeting: {
34
51
  name: string
@@ -45,6 +62,8 @@ interface CommandCenterPayload {
45
62
  }
46
63
  projects: ProjectRow[]
47
64
  recent_incidents: IncidentRow[]
65
+ top_departments_30d: TopDeptRow[]
66
+ recent_personas: RecentPersonaRow[]
48
67
  quick_actions: QuickAction[]
49
68
  }
50
69
 
@@ -185,6 +204,82 @@ function copyCommand(cmd: string) {
185
204
  </div>
186
205
  </UCard>
187
206
 
207
+ <!-- PR84d v3.10.0 — Top departments + Recent personas row -->
208
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
209
+ <div>
210
+ <h2 class="text-sm font-semibold uppercase tracking-wider text-muted mb-3">
211
+ Top departments (30d)
212
+ </h2>
213
+ <DashboardState
214
+ :status="status"
215
+ :empty="!data?.top_departments_30d?.length"
216
+ empty-title="No telemetry yet"
217
+ empty-description="Department spend will appear once agents start running."
218
+ empty-icon="i-lucide-bar-chart"
219
+ >
220
+ <div class="space-y-2">
221
+ <div
222
+ v-for="(d, idx) in data?.top_departments_30d"
223
+ :key="d.department"
224
+ class="rounded-lg border border-default p-3 flex items-center gap-3"
225
+ >
226
+ <span class="text-xs font-mono font-semibold text-muted w-6">#{{ idx + 1 }}</span>
227
+ <span class="flex-1 font-semibold capitalize">{{ d.department }}</span>
228
+ <span class="text-xs font-mono text-muted">{{ d.calls }} calls</span>
229
+ <span class="text-sm font-mono font-semibold">
230
+ {{ d.cost_usd === null ? '—' : `$${d.cost_usd.toFixed(2)}` }}
231
+ </span>
232
+ </div>
233
+ </div>
234
+ </DashboardState>
235
+ </div>
236
+
237
+ <div>
238
+ <h2 class="text-sm font-semibold uppercase tracking-wider text-muted mb-3">
239
+ Recent personas
240
+ </h2>
241
+ <DashboardState
242
+ :status="status"
243
+ :empty="!data?.recent_personas?.length"
244
+ empty-title="No personas yet"
245
+ empty-description="Create one from /personas → New Persona."
246
+ empty-icon="i-lucide-user-plus"
247
+ >
248
+ <div class="space-y-2">
249
+ <NuxtLink
250
+ v-for="p in data?.recent_personas"
251
+ :key="p.id"
252
+ :to="`/personas/${p.id}`"
253
+ class="block rounded-lg border border-default p-3 hover:border-primary/40 transition-colors"
254
+ >
255
+ <div class="flex items-center justify-between gap-3">
256
+ <div class="min-w-0">
257
+ <p class="text-sm font-semibold truncate">{{ p.name }}</p>
258
+ <p class="text-xs text-muted truncate">{{ p.title || '—' }}</p>
259
+ </div>
260
+ <div class="flex items-center gap-2 shrink-0">
261
+ <UBadge
262
+ v-if="p.mbti"
263
+ :label="p.mbti"
264
+ variant="subtle"
265
+ size="xs"
266
+ />
267
+ <UBadge
268
+ v-if="p.source_store === 'obsidian'"
269
+ icon="i-lucide-file-text"
270
+ label="Obsidian"
271
+ color="primary"
272
+ variant="soft"
273
+ size="xs"
274
+ />
275
+ </div>
276
+ </div>
277
+ </NuxtLink>
278
+ </div>
279
+ </DashboardState>
280
+ </div>
281
+ </div>
282
+
188
283
  <!-- Two columns: projects + incidents -->
189
284
  <div class="grid grid-cols-1 lg:grid-cols-[2fr_1fr] gap-6">
190
285
  <!-- Projects -->
@@ -226,6 +226,72 @@ 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
+ // PR84c v3.9.0 — Auto-fill empty lists in one go.
230
+ const autofilling = ref(false)
231
+
232
+ async function autofillEmpties() {
233
+ if (!draft.value || !detail.value) return
234
+ type ListKey = 'mental_models' | 'expertise_domains' | 'frameworks' | 'key_quotes' | 'communication_avoid'
235
+ const targets: ListKey[] = []
236
+ if ((draft.value.mental_models ?? []).length === 0) targets.push('mental_models')
237
+ if ((draft.value.expertise_domains ?? []).length === 0) targets.push('expertise_domains')
238
+ if ((draft.value.frameworks ?? []).length === 0) targets.push('frameworks')
239
+ if ((draft.value.key_quotes ?? []).length === 0) targets.push('key_quotes')
240
+ if ((draft.value.communication.avoid ?? []).length === 0) targets.push('communication_avoid')
241
+ if (targets.length === 0) {
242
+ toast.add({ title: 'No empty lists', description: 'Every list already has at least one item.', color: 'info' })
243
+ return
244
+ }
245
+ autofilling.value = true
246
+ const results = await Promise.allSettled(
247
+ targets.map((field) =>
248
+ $fetch<{ suggestions: string[], provider_name: string, error?: string }>(
249
+ `${apiBase}/api/personas/suggest`,
250
+ {
251
+ method: 'POST',
252
+ body: {
253
+ field,
254
+ count: 5,
255
+ context: {
256
+ name: detail.value!.name,
257
+ title: detail.value!.title,
258
+ current: [],
259
+ },
260
+ },
261
+ },
262
+ ),
263
+ ),
264
+ )
265
+ let filledCount = 0
266
+ let providerName = ''
267
+ results.forEach((r, idx) => {
268
+ if (r.status !== 'fulfilled' || r.value.error) return
269
+ const items = r.value.suggestions ?? []
270
+ if (items.length === 0) return
271
+ const field = targets[idx]
272
+ if (!draft.value) return
273
+ if (field === 'communication_avoid') {
274
+ draft.value.communication.avoid = items
275
+ } else {
276
+ ;(draft.value as any)[field] = items
277
+ }
278
+ filledCount += 1
279
+ providerName = r.value.provider_name || providerName
280
+ })
281
+ autofilling.value = false
282
+ if (filledCount > 0) {
283
+ markDirty()
284
+ toast.add({
285
+ title: `Filled ${filledCount} list${filledCount === 1 ? '' : 's'}`,
286
+ description: `via ${providerName}`,
287
+ color: 'success',
288
+ icon: 'i-lucide-sparkles',
289
+ })
290
+ } else {
291
+ toast.add({ title: 'Nothing filled', description: 'LLM returned no items.', color: 'error' })
292
+ }
293
+ }
294
+
229
295
  // PR83c v3.5.0 — single-string suggester (tone for personas).
230
296
  const suggestingString = ref<'tone' | null>(null)
231
297
 
@@ -657,15 +723,26 @@ const vocabOptions = [
657
723
  }"
658
724
  >
659
725
  <template #header>
660
- <div class="flex items-center justify-between">
726
+ <div class="flex items-center justify-between gap-3">
661
727
  <h2 class="text-xl font-bold">Edit {{ draft.name || 'persona' }}</h2>
662
- <UButton
663
- icon="i-lucide-x"
664
- variant="ghost"
665
- size="sm"
666
- aria-label="Close"
667
- @click="tryCloseEdit"
668
- />
728
+ <div class="flex items-center gap-2">
729
+ <UButton
730
+ label="Auto-fill empties"
731
+ icon="i-lucide-sparkles"
732
+ color="primary"
733
+ variant="soft"
734
+ size="sm"
735
+ :loading="autofilling"
736
+ @click="autofillEmpties"
737
+ />
738
+ <UButton
739
+ icon="i-lucide-x"
740
+ variant="ghost"
741
+ size="sm"
742
+ aria-label="Close"
743
+ @click="tryCloseEdit"
744
+ />
745
+ </div>
669
746
  </div>
670
747
  </template>
671
748
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arkaos",
3
- "version": "3.8.0",
3
+ "version": "3.10.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.8.0"
3
+ version = "3.10.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"}
@@ -1361,6 +1361,9 @@ def overview_command_center():
1361
1361
  },
1362
1362
  "projects": _scan_projects(parse_projects_dirs(profile.projectsDir)),
1363
1363
  "recent_incidents": _recent_incidents(limit=8),
1364
+ # PR84d v3.10.0 — extra command-center cards
1365
+ "top_departments_30d": _top_departments_by_cost(period="month", top_n=5),
1366
+ "recent_personas": _recent_personas(limit=5),
1364
1367
  "quick_actions": [
1365
1368
  {"command": "/arka update", "description": "Sync projects + skills"},
1366
1369
  {"command": "/arka costs", "description": "View detailed LLM cost breakdown"},
@@ -1370,6 +1373,62 @@ def overview_command_center():
1370
1373
  }
1371
1374
 
1372
1375
 
1376
+ def _top_departments_by_cost(period: str = "month", top_n: int = 5) -> list[dict]:
1377
+ """Top-N departments ranked by LLM spend over the period."""
1378
+ try:
1379
+ from core.runtime.llm_cost_telemetry import summarise, VALID_PERIODS
1380
+ except Exception:
1381
+ return []
1382
+ if period not in VALID_PERIODS:
1383
+ period = "month"
1384
+ summary = summarise(period=period)
1385
+ rows: list[dict] = []
1386
+ for category, row in (summary.by_category or {}).items():
1387
+ if not isinstance(category, str) or not category.startswith("subagent:"):
1388
+ continue
1389
+ dept = category.split(":", 1)[1] or "unknown"
1390
+ cost = row.get("total_cost_usd")
1391
+ rows.append({
1392
+ "department": dept,
1393
+ "calls": int(row.get("call_count", 0)),
1394
+ "cost_usd": float(cost) if isinstance(cost, (int, float)) else None,
1395
+ "tokens_in": int(row.get("total_tokens_in", 0)),
1396
+ "tokens_out": int(row.get("total_tokens_out", 0)),
1397
+ })
1398
+ rows.sort(key=lambda r: r["cost_usd"] or 0.0, reverse=True)
1399
+ return rows[: max(0, int(top_n))]
1400
+
1401
+
1402
+ def _recent_personas(limit: int = 5) -> list[dict]:
1403
+ """Return the N most recently created/modified personas.
1404
+
1405
+ Reads both the JSON store and the Obsidian vault; returns the
1406
+ union sorted by created_at desc (best-effort — entries without
1407
+ a timestamp sort last).
1408
+ """
1409
+ mgr = _get_persona_manager()
1410
+ if not mgr:
1411
+ return []
1412
+ try:
1413
+ personas = mgr.list() or []
1414
+ except Exception:
1415
+ return []
1416
+ def _ts(p: dict) -> str:
1417
+ return str(p.get("created_at") or p.get("_created_at") or "")
1418
+ sorted_p = sorted(personas, key=_ts, reverse=True)
1419
+ out: list[dict] = []
1420
+ for p in sorted_p[: max(0, int(limit))]:
1421
+ out.append({
1422
+ "id": p.get("id"),
1423
+ "name": p.get("name"),
1424
+ "title": p.get("title"),
1425
+ "mbti": p.get("mbti"),
1426
+ "source_store": (p.get("_source_store") or ""),
1427
+ "created_at": _ts(p) or None,
1428
+ })
1429
+ return out
1430
+
1431
+
1373
1432
  def _scan_projects(projects_dirs: list[str]) -> list[dict]:
1374
1433
  """Read each project descriptor and enrich with last-commit info.
1375
1434