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 CHANGED
@@ -1 +1 @@
1
- 3.34.0
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
- <UButton
316
- label="Import .md"
317
- icon="i-lucide-file-up"
318
- variant="soft"
319
- size="sm"
320
- :loading="importing"
321
- @click="triggerImport"
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arkaos",
3
- "version": "3.34.0",
3
+ "version": "3.36.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.34.0"
3
+ version = "3.36.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"}
@@ -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: {"files": [{"name": "Alex.md", "content": "<full file body>"}]}
1951
- Returns: {imported, failed, results: [{filename, status, id?, error?}]}
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
- Each file MUST have YAML frontmatter with ``type: persona``. Files
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
- files = body.get("files") or []
1958
- if not isinstance(files, list):
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")