arkaos 3.37.0 → 3.39.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.37.0
1
+ 3.39.0
@@ -53,7 +53,8 @@ interface TrendResponse {
53
53
 
54
54
  type Period = 'today' | 'week' | 'month' | 'all'
55
55
 
56
- const { fetchApi } = useApi()
56
+ const { fetchApi, apiBase } = useApi()
57
+ const toast = useToast()
57
58
  const period = ref<Period>('today')
58
59
 
59
60
  const periodOptions: { label: string; value: Period }[] = [
@@ -73,6 +74,36 @@ const {
73
74
  { query: computed(() => ({ period: period.value })) },
74
75
  )
75
76
 
77
+ // PR91d v3.38.0 — CSV export of the telemetry rows for the current period.
78
+ async function exportCsv() {
79
+ try {
80
+ const blob = await $fetch<Blob>(
81
+ `${apiBase}/api/llm-costs/export.csv`,
82
+ { query: { period: period.value }, responseType: 'blob' },
83
+ )
84
+ const url = URL.createObjectURL(blob)
85
+ const a = document.createElement('a')
86
+ a.href = url
87
+ a.download = `arkaos-costs-${period.value}.csv`
88
+ document.body.appendChild(a)
89
+ a.click()
90
+ a.remove()
91
+ URL.revokeObjectURL(url)
92
+ toast.add({
93
+ title: 'CSV downloaded',
94
+ description: `arkaos-costs-${period.value}.csv`,
95
+ color: 'success',
96
+ icon: 'i-lucide-download',
97
+ })
98
+ } catch (err) {
99
+ toast.add({
100
+ title: 'Export failed',
101
+ description: err instanceof Error ? err.message : 'unknown error',
102
+ color: 'error',
103
+ })
104
+ }
105
+ }
106
+
76
107
  // PR90c v3.33.0 — let the operator pick 7d / 14d / 30d.
77
108
  const trendDays = ref<7 | 14 | 30>(7)
78
109
  const trendDaysOptions = [
@@ -204,6 +235,14 @@ async function refreshAll() {
204
235
  size="sm"
205
236
  class="w-32"
206
237
  />
238
+ <UButton
239
+ label="Export CSV"
240
+ icon="i-lucide-download"
241
+ variant="soft"
242
+ size="sm"
243
+ class="ml-2"
244
+ @click="exportCsv"
245
+ />
207
246
  <UButton
208
247
  label="Refresh"
209
248
  variant="ghost"
@@ -142,6 +142,40 @@ const favs = useFavorites()
142
142
  await favs.load()
143
143
  const favoritesOnly = ref(false)
144
144
 
145
+ // PR92a v3.39.0 — bulk export every persona as a zip.
146
+ const exportingZip = ref(false)
147
+ async function exportAllAsZip() {
148
+ exportingZip.value = true
149
+ try {
150
+ const blob = await $fetch<Blob>(
151
+ `${apiBase}/api/personas/export-all.zip`,
152
+ { responseType: 'blob' },
153
+ )
154
+ const url = URL.createObjectURL(blob)
155
+ const a = document.createElement('a')
156
+ a.href = url
157
+ a.download = 'arkaos-personas.zip'
158
+ document.body.appendChild(a)
159
+ a.click()
160
+ a.remove()
161
+ URL.revokeObjectURL(url)
162
+ toast.add({
163
+ title: 'ZIP downloaded',
164
+ description: 'arkaos-personas.zip',
165
+ color: 'success',
166
+ icon: 'i-lucide-archive',
167
+ })
168
+ } catch (err) {
169
+ toast.add({
170
+ title: 'Export failed',
171
+ description: err instanceof Error ? err.message : 'unknown error',
172
+ color: 'error',
173
+ })
174
+ } finally {
175
+ exportingZip.value = false
176
+ }
177
+ }
178
+
145
179
  // PR87b v3.20.0 — import .md persona files.
146
180
  // PR91b v3.36.0 — extended with URL import.
147
181
  const importInput = ref<HTMLInputElement | null>(null)
@@ -354,6 +388,14 @@ async function undoTrashIds(ids: string[]) {
354
388
  />
355
389
  </template>
356
390
  <template #right>
391
+ <UButton
392
+ label="Export ZIP"
393
+ icon="i-lucide-archive"
394
+ variant="ghost"
395
+ size="sm"
396
+ :loading="exportingZip"
397
+ @click="exportAllAsZip"
398
+ />
357
399
  <UDropdownMenu
358
400
  :items="[
359
401
  { label: 'Pick .md files…', icon: 'i-lucide-file-up', onSelect: triggerImport },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arkaos",
3
- "version": "3.37.0",
3
+ "version": "3.39.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.37.0"
3
+ version = "3.39.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"}
@@ -322,6 +322,68 @@ def agent_activity_strip(agent_id: str, period: str = "month"):
322
322
  }
323
323
 
324
324
 
325
+ @app.get("/api/personas/export-all.zip")
326
+ def personas_export_all():
327
+ """PR92a v3.39.0 — stream every persona as Markdown inside a ZIP.
328
+
329
+ Iterates `PersonaManager.list_all()` plus any Obsidian-vault entries
330
+ surfaced via `persona_detail`, renders each through
331
+ `ObsidianPersonaStore._render`, and zips them. Filename uses the
332
+ persona name (sanitised), falling back to id.
333
+ """
334
+ mgr = _get_persona_manager()
335
+ if not mgr:
336
+ return {"error": "Persona manager unavailable"}
337
+ try:
338
+ items = list(mgr.list_all() or [])
339
+ except Exception as exc: # noqa: BLE001
340
+ return {"error": f"list failed: {exc}"}
341
+
342
+ from core.personas.obsidian_store import ObsidianPersonaStore
343
+
344
+ import io
345
+ import zipfile
346
+
347
+ buffer = io.BytesIO()
348
+ seen: set[str] = set()
349
+ written = 0
350
+ with zipfile.ZipFile(buffer, mode="w", compression=zipfile.ZIP_DEFLATED) as zf:
351
+ for p in items:
352
+ persona = p if hasattr(p, "model_dump") else None
353
+ if persona is None:
354
+ continue
355
+ slug = _zip_persona_slug(persona.name or persona.id)
356
+ if slug in seen:
357
+ slug = f"{slug}-{persona.id[:6]}"
358
+ seen.add(slug)
359
+ try:
360
+ body = ObsidianPersonaStore._render(persona)
361
+ except Exception: # noqa: BLE001
362
+ continue
363
+ zf.writestr(f"{slug}.md", body)
364
+ written += 1
365
+
366
+ if written == 0:
367
+ return {"error": "no personas to export"}
368
+
369
+ from fastapi import Response
370
+ return Response(
371
+ content=buffer.getvalue(),
372
+ media_type="application/zip",
373
+ headers={
374
+ "Content-Disposition": 'attachment; filename="arkaos-personas.zip"',
375
+ },
376
+ )
377
+
378
+
379
+ def _zip_persona_slug(name: str) -> str:
380
+ """Sanitise a name for use as a zip member filename."""
381
+ import re
382
+ cleaned = re.sub(r"[^A-Za-z0-9._-]+", "-", str(name or ""))
383
+ cleaned = cleaned.strip("-") or "persona"
384
+ return cleaned[:80]
385
+
386
+
325
387
  @app.get("/api/personas/{persona_id}/markdown")
326
388
  def persona_download_markdown(persona_id: str):
327
389
  """PR90a v3.31.0 — return the persona as a Markdown file.
@@ -2510,6 +2572,56 @@ def llm_costs(period: str = "today"):
2510
2572
  }
2511
2573
 
2512
2574
 
2575
+ @app.get("/api/llm-costs/export.csv")
2576
+ def llm_costs_export(period: str = "month"):
2577
+ """PR91d v3.38.0 — stream the telemetry rows for the period as CSV.
2578
+
2579
+ Returns ``text/csv`` with a header row + every row in the selected
2580
+ period. Period values match `summarise()`: today / week / month / all.
2581
+ """
2582
+ try:
2583
+ from core.runtime.llm_cost_telemetry import (
2584
+ VALID_PERIODS,
2585
+ _load_slice,
2586
+ _period_cutoff,
2587
+ )
2588
+ except Exception:
2589
+ return {"error": "telemetry unavailable"}
2590
+ if period not in VALID_PERIODS:
2591
+ period = "month"
2592
+
2593
+ entries, _ = _load_slice(None, _period_cutoff(period, now=None))
2594
+ import csv
2595
+ import io
2596
+ buffer = io.StringIO()
2597
+ writer = csv.writer(buffer)
2598
+ writer.writerow([
2599
+ "ts", "session_id", "provider", "model", "category",
2600
+ "tokens_in", "tokens_out", "cached_tokens", "estimated_cost_usd",
2601
+ ])
2602
+ for entry in entries:
2603
+ writer.writerow([
2604
+ entry.get("ts", ""),
2605
+ entry.get("session_id", ""),
2606
+ entry.get("provider", ""),
2607
+ entry.get("model", ""),
2608
+ entry.get("category", ""),
2609
+ entry.get("tokens_in", ""),
2610
+ entry.get("tokens_out", ""),
2611
+ entry.get("cached_tokens", ""),
2612
+ entry.get("estimated_cost_usd", ""),
2613
+ ])
2614
+ filename = f"arkaos-costs-{period}.csv"
2615
+ from fastapi import Response
2616
+ return Response(
2617
+ content=buffer.getvalue(),
2618
+ media_type="text/csv",
2619
+ headers={
2620
+ "Content-Disposition": f'attachment; filename="{filename}"',
2621
+ },
2622
+ )
2623
+
2624
+
2513
2625
  @app.get("/api/llm-costs/trend")
2514
2626
  def llm_costs_trend(days: int = 7):
2515
2627
  """Day-by-day rolling totals from the cost telemetry.