arkaos 3.16.0 → 3.17.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.16.0
1
+ 3.17.0
@@ -0,0 +1,171 @@
1
+ """Export agent profiles to the Obsidian vault (PR86c v3.17.0).
2
+
3
+ Writes a Markdown file with YAML frontmatter + readable sections to
4
+ ``<vault>/Agents/<id>.md``. Sibling to ``core/personas/obsidian_store``
5
+ but write-only — agents are still source-of-truth in their YAML files.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from dataclasses import dataclass
11
+ from pathlib import Path
12
+
13
+ from core.profile import ProfileManager
14
+
15
+
16
+ _AGENTS_SUBDIR = "Agents"
17
+
18
+
19
+ class AgentExportError(RuntimeError):
20
+ """Raised when the vault is missing or the write fails."""
21
+
22
+
23
+ @dataclass(frozen=True)
24
+ class AgentExportResult:
25
+ path: Path
26
+ vault_path: Path
27
+
28
+
29
+ def export_agent_to_vault(agent: dict) -> AgentExportResult:
30
+ """Render `agent` as Markdown and write it to the configured vault.
31
+
32
+ `agent` is the YAML-loaded dict for the agent (the same shape
33
+ returned by ``/api/agents/{id}``).
34
+ """
35
+ if not isinstance(agent, dict) or not agent.get("id"):
36
+ raise AgentExportError("agent payload must include 'id'")
37
+ vault = _resolve_vault_path()
38
+ if vault is None:
39
+ raise AgentExportError(
40
+ "Obsidian vault is not configured (set vaultPath in profile)"
41
+ )
42
+ target_dir = vault / _AGENTS_SUBDIR
43
+ target_dir.mkdir(parents=True, exist_ok=True)
44
+ target = target_dir / f"{agent['id']}.md"
45
+ md = _render(agent)
46
+ tmp = target.with_suffix(target.suffix + ".tmp")
47
+ try:
48
+ tmp.write_text(md, encoding="utf-8")
49
+ tmp.replace(target)
50
+ except OSError as exc:
51
+ raise AgentExportError(f"write failed: {exc}") from exc
52
+ return AgentExportResult(path=target, vault_path=vault)
53
+
54
+
55
+ def _resolve_vault_path() -> Path | None:
56
+ try:
57
+ profile = ProfileManager().read()
58
+ except Exception: # noqa: BLE001
59
+ return None
60
+ if not profile.vaultPath:
61
+ return None
62
+ path = Path(profile.vaultPath).expanduser()
63
+ return path if path.exists() else None
64
+
65
+
66
+ def _render(agent: dict) -> str:
67
+ """Compose the Markdown body — YAML frontmatter + readable sections."""
68
+ fm_lines = [
69
+ "---",
70
+ "type: agent",
71
+ f"id: {agent.get('id', '')}",
72
+ f"name: {_yaml_str(agent.get('name', ''))}",
73
+ f"role: {_yaml_str(agent.get('role', ''))}",
74
+ f"department: {agent.get('department', '')}",
75
+ f"tier: {agent.get('tier', '')}",
76
+ f"model: {agent.get('model', '')}",
77
+ "---",
78
+ "",
79
+ ]
80
+ sections: list[str] = []
81
+ sections.append(f"# {agent.get('name') or agent.get('id', '(unnamed)')}")
82
+ if agent.get("role"):
83
+ sections.append(f"*{agent['role']}* · `{agent.get('department', '')}`")
84
+ sections.append("")
85
+
86
+ dna = agent.get("behavioral_dna") or {}
87
+ if dna:
88
+ sections.append("## Behavioural DNA")
89
+ disc = dna.get("disc") or {}
90
+ enn = dna.get("enneagram") or {}
91
+ bf = dna.get("big_five") or {}
92
+ mbti = dna.get("mbti")
93
+ mbti_type = mbti.get("type") if isinstance(mbti, dict) else mbti
94
+ sections.append(f"- **DISC:** {disc.get('primary', '?')}/{disc.get('secondary', '?')}")
95
+ sections.append(f"- **Enneagram:** {enn.get('type', '?')}w{enn.get('wing', '?')}")
96
+ sections.append(f"- **MBTI:** {mbti_type or '—'}")
97
+ if bf:
98
+ ocean = " · ".join(
99
+ f"{k[:1].upper()}{int(v)}"
100
+ for k, v in bf.items()
101
+ if isinstance(v, (int, float))
102
+ )
103
+ if ocean:
104
+ sections.append(f"- **OCEAN:** {ocean}")
105
+ sections.append("")
106
+
107
+ exp = agent.get("expertise") or {}
108
+ if exp:
109
+ sections.append("## Expertise")
110
+ domains = exp.get("domains") or []
111
+ frameworks = exp.get("frameworks") or []
112
+ if domains:
113
+ sections.append("**Domains**")
114
+ sections.extend(f"- {d}" for d in domains)
115
+ sections.append("")
116
+ if frameworks:
117
+ sections.append("**Frameworks**")
118
+ sections.extend(f"- {f}" for f in frameworks)
119
+ sections.append("")
120
+ sections.append(f"*Depth: {exp.get('depth', '—')} · {exp.get('years_equivalent', '—')}y equivalent*")
121
+ sections.append("")
122
+
123
+ mm = agent.get("mental_models") or {}
124
+ if mm:
125
+ primary = mm.get("primary") or []
126
+ secondary = mm.get("secondary") or []
127
+ if primary or secondary:
128
+ sections.append("## Mental Models")
129
+ if primary:
130
+ sections.append("**Primary**")
131
+ sections.extend(f"- {m}" for m in primary)
132
+ sections.append("")
133
+ if secondary:
134
+ sections.append("**Secondary**")
135
+ sections.extend(f"- {m}" for m in secondary)
136
+ sections.append("")
137
+
138
+ comm = agent.get("communication") or {}
139
+ if comm and any(comm.values()):
140
+ sections.append("## Communication")
141
+ if comm.get("tone"):
142
+ sections.append(f"- **Tone:** {comm['tone']}")
143
+ if comm.get("vocabulary_level"):
144
+ sections.append(f"- **Vocabulary:** {comm['vocabulary_level']}")
145
+ if comm.get("preferred_format"):
146
+ sections.append(f"- **Preferred format:** {comm['preferred_format']}")
147
+ if comm.get("language"):
148
+ sections.append(f"- **Language:** {comm['language']}")
149
+ avoid = comm.get("avoid") or []
150
+ if avoid:
151
+ sections.append(f"- **Avoid:** {', '.join(avoid)}")
152
+ sections.append("")
153
+
154
+ linked = agent.get("linked_personas") or []
155
+ if linked:
156
+ sections.append("## Linked Personas")
157
+ sections.extend(f"- [[{p}]]" for p in linked)
158
+ sections.append("")
159
+
160
+ sections.append("")
161
+ sections.append("*Generated by ArkaOS — source of truth lives in the YAML file.*")
162
+ return "\n".join(fm_lines + sections)
163
+
164
+
165
+ def _yaml_str(value: object) -> str:
166
+ """Quote a YAML scalar if it contains special characters."""
167
+ s = "" if value is None else str(value)
168
+ if any(c in s for c in ":#'\"\n"):
169
+ escaped = s.replace('"', '\\"')
170
+ return f'"{escaped}"'
171
+ return s
@@ -9,7 +9,8 @@
9
9
  const route = useRoute()
10
10
  const agentId = route.params.id as string
11
11
 
12
- const { fetchApi } = useApi()
12
+ const { fetchApi, apiBase } = useApi()
13
+ const toast = useToast()
13
14
  const { data: agent, status, error, refresh } = fetchApi<any>(`/api/agents/${agentId}`)
14
15
 
15
16
  // Per-department activity (PR69 endpoint) for the stats row.
@@ -64,6 +65,34 @@ function formatRelative(iso: string | null): string {
64
65
  const favs = useFavorites()
65
66
  await favs.load()
66
67
 
68
+ // PR86c v3.17.0 — export to Obsidian.
69
+ const exporting = ref(false)
70
+ async function exportToVault() {
71
+ if (!agent.value) return
72
+ exporting.value = true
73
+ try {
74
+ const res = await $fetch<{ exported?: boolean, path?: string, error?: string }>(
75
+ `${apiBase}/api/agents/${agentId}/export`,
76
+ { method: 'POST' },
77
+ )
78
+ if (res.error) throw new Error(res.error)
79
+ toast.add({
80
+ title: 'Exported to Obsidian',
81
+ description: res.path ? res.path.split('/').slice(-3).join('/') : undefined,
82
+ color: 'success',
83
+ icon: 'i-lucide-file-text',
84
+ })
85
+ } catch (err) {
86
+ toast.add({
87
+ title: 'Export failed',
88
+ description: err instanceof Error ? err.message : 'unknown error',
89
+ color: 'error',
90
+ })
91
+ } finally {
92
+ exporting.value = false
93
+ }
94
+ }
95
+
67
96
  // PR76 — edit drawer state
68
97
  const editOpen = ref(false)
69
98
 
@@ -282,6 +311,14 @@ function formatTokens(n: number): string {
282
311
  :aria-label="favs.isAgentFavorite(agent.id) ? 'Unfavorite' : 'Favorite'"
283
312
  @click="favs.toggle('agents', agent.id)"
284
313
  />
314
+ <UButton
315
+ label="Export"
316
+ icon="i-lucide-file-text"
317
+ variant="soft"
318
+ size="sm"
319
+ :loading="exporting"
320
+ @click="exportToVault"
321
+ />
285
322
  <UButton
286
323
  label="Edit"
287
324
  icon="i-lucide-pencil"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arkaos",
3
- "version": "3.16.0",
3
+ "version": "3.17.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.16.0"
3
+ version = "3.17.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,28 @@ def persona_delete(persona_id: str):
1361
1361
  return {"error": "Persona not found"}
1362
1362
 
1363
1363
 
1364
+ # --- Agent → Obsidian export (PR86c v3.17.0) ---
1365
+
1366
+ @app.post("/api/agents/{agent_id}/export")
1367
+ def agent_export_to_vault(agent_id: str):
1368
+ """Write the agent profile as Markdown under <vault>/Agents/<id>.md."""
1369
+ detail = agent_detail(agent_id)
1370
+ if "error" in detail:
1371
+ return detail
1372
+ try:
1373
+ from core.agents.obsidian_export import (
1374
+ AgentExportError,
1375
+ export_agent_to_vault,
1376
+ )
1377
+ except Exception as exc: # noqa: BLE001
1378
+ return {"error": f"export module unavailable: {exc}"}
1379
+ try:
1380
+ res = export_agent_to_vault(detail)
1381
+ except AgentExportError as exc:
1382
+ return {"error": str(exc)}
1383
+ return {"exported": True, "path": str(res.path), "vault_path": str(res.vault_path)}
1384
+
1385
+
1364
1386
  # --- Favorites (PR86a v3.15.0) ---
1365
1387
 
1366
1388
  @app.get("/api/favorites")