arkaos 2.2.2 → 2.3.1

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.
Files changed (66) hide show
  1. package/VERSION +1 -1
  2. package/arka/skills/conclave/SKILL.md +194 -0
  3. package/arka/skills/human-writing/SKILL.md +143 -0
  4. package/config/agent-memory-template.md +28 -0
  5. package/config/disc-profiles.json +108 -0
  6. package/config/disc-team-validator.sh +94 -0
  7. package/config/gotchas-fixes.json +148 -0
  8. package/config/profile-template.json +12 -0
  9. package/config/providers-registry.json +56 -0
  10. package/config/settings-template.json +42 -0
  11. package/config/standards/communication.md +64 -0
  12. package/config/standards/orchestration.md +91 -0
  13. package/config/statusline-v2.sh +101 -0
  14. package/config/statusline.sh +139 -0
  15. package/config/system-prompt.sh +190 -0
  16. package/dashboard/LICENSE +21 -0
  17. package/dashboard/README.md +64 -0
  18. package/dashboard/app/app.config.ts +8 -0
  19. package/dashboard/app/app.vue +42 -0
  20. package/dashboard/app/assets/css/main.css +18 -0
  21. package/dashboard/app/composables/useApi.ts +8 -0
  22. package/dashboard/app/composables/useDashboard.ts +19 -0
  23. package/dashboard/app/error.vue +24 -0
  24. package/dashboard/app/layouts/default.vue +114 -0
  25. package/dashboard/app/pages/agents/[id].vue +506 -0
  26. package/dashboard/app/pages/agents/index.vue +225 -0
  27. package/dashboard/app/pages/budget.vue +132 -0
  28. package/dashboard/app/pages/commands.vue +180 -0
  29. package/dashboard/app/pages/health.vue +98 -0
  30. package/dashboard/app/pages/index.vue +126 -0
  31. package/dashboard/app/pages/knowledge.vue +729 -0
  32. package/dashboard/app/pages/personas.vue +597 -0
  33. package/dashboard/app/pages/settings.vue +146 -0
  34. package/dashboard/app/pages/tasks.vue +203 -0
  35. package/dashboard/app/types/index.d.ts +181 -0
  36. package/dashboard/app/utils/index.ts +7 -0
  37. package/dashboard/nuxt.config.ts +39 -0
  38. package/dashboard/package.json +37 -0
  39. package/dashboard/pnpm-workspace.yaml +7 -0
  40. package/dashboard/tsconfig.json +10 -0
  41. package/installer/cli.js +0 -0
  42. package/installer/index.js +262 -62
  43. package/knowledge/INDEX.md +34 -0
  44. package/knowledge/agents-registry.json +254 -0
  45. package/knowledge/channels-config.json +6 -0
  46. package/knowledge/commands-keywords.json +466 -0
  47. package/knowledge/commands-registry.json +2791 -0
  48. package/knowledge/commands-registry.json.bak +2791 -0
  49. package/knowledge/ecosystems.json +7 -0
  50. package/knowledge/obsidian-config.json +112 -0
  51. package/package.json +10 -6
  52. package/pyproject.toml +1 -1
  53. package/scripts/check-version.js +13 -0
  54. package/scripts/dashboard-api.py +636 -0
  55. package/scripts/knowledge-index.py +113 -0
  56. package/scripts/skill_validator.py +217 -0
  57. package/scripts/start-dashboard.sh +54 -0
  58. package/scripts/synapse-bridge.py +199 -0
  59. package/scripts/tools/brand_voice_analyzer.py +192 -0
  60. package/scripts/tools/dcf_calculator.py +168 -0
  61. package/scripts/tools/headline_scorer.py +215 -0
  62. package/scripts/tools/okr_cascade.py +207 -0
  63. package/scripts/tools/rice_prioritizer.py +230 -0
  64. package/scripts/tools/saas_metrics.py +234 -0
  65. package/scripts/tools/seo_checker.py +197 -0
  66. package/scripts/tools/tech_debt_analyzer.py +206 -0
@@ -0,0 +1,636 @@
1
+ #!/usr/bin/env python3
2
+ """ArkaOS Dashboard API — FastAPI backend for the monitoring dashboard.
3
+
4
+ Exposes all ArkaOS data as REST endpoints consumed by the Nuxt 3 frontend.
5
+
6
+ Usage:
7
+ python scripts/dashboard-api.py # Start on :3334
8
+ python scripts/dashboard-api.py --port 8000 # Custom port
9
+ python scripts/dashboard-api.py --host 0.0.0.0 # Allow external access
10
+ """
11
+
12
+ import json
13
+ import os
14
+ import subprocess
15
+ import sys
16
+ from pathlib import Path
17
+ from typing import Optional
18
+
19
+ # Resolve ArkaOS root
20
+ ARKAOS_ROOT = Path(os.environ.get("ARKAOS_ROOT", Path(__file__).resolve().parent.parent))
21
+ sys.path.insert(0, str(ARKAOS_ROOT))
22
+
23
+ from fastapi import FastAPI, Query, WebSocket, WebSocketDisconnect
24
+ from fastapi.middleware.cors import CORSMiddleware
25
+
26
+ app = FastAPI(title="ArkaOS Dashboard API", version="2.2.0")
27
+
28
+ # --- WebSocket — thread-safe message queue ---
29
+ import asyncio
30
+ import queue as _queue
31
+
32
+ _ws_clients: list[WebSocket] = []
33
+ _ws_message_queue: _queue.Queue = _queue.Queue()
34
+
35
+
36
+ def broadcast_from_thread(data: dict):
37
+ """Thread-safe: put message in queue, WebSocket loop picks it up."""
38
+ _ws_message_queue.put(data)
39
+
40
+
41
+ @app.websocket("/ws/tasks")
42
+ async def ws_tasks(websocket: WebSocket):
43
+ await websocket.accept()
44
+ _ws_clients.append(websocket)
45
+ try:
46
+ while True:
47
+ # Check message queue every 100ms
48
+ try:
49
+ while not _ws_message_queue.empty():
50
+ msg = _ws_message_queue.get_nowait()
51
+ dead = []
52
+ for client in _ws_clients:
53
+ try:
54
+ await client.send_json(msg)
55
+ except Exception:
56
+ dead.append(client)
57
+ for d in dead:
58
+ if d in _ws_clients:
59
+ _ws_clients.remove(d)
60
+ except _queue.Empty:
61
+ pass
62
+ await asyncio.sleep(0.1)
63
+ except WebSocketDisconnect:
64
+ if websocket in _ws_clients:
65
+ _ws_clients.remove(websocket)
66
+
67
+ app.add_middleware(
68
+ CORSMiddleware,
69
+ allow_origins=["http://localhost:3333", "http://localhost:3000"],
70
+ allow_methods=["GET", "POST", "PUT", "DELETE"],
71
+ allow_headers=["*"],
72
+ )
73
+
74
+ # --- Data loaders (lazy, cached) ---
75
+
76
+ _agents_cache = None
77
+ _commands_cache = None
78
+
79
+
80
+ def _load_agents() -> list[dict]:
81
+ global _agents_cache
82
+ if _agents_cache is None:
83
+ path = ARKAOS_ROOT / "knowledge" / "agents-registry-v2.json"
84
+ if path.exists():
85
+ data = json.loads(path.read_text())
86
+ _agents_cache = data.get("agents", [])
87
+ else:
88
+ _agents_cache = []
89
+ return _agents_cache
90
+
91
+
92
+ def _load_commands() -> list[dict]:
93
+ global _commands_cache
94
+ if _commands_cache is None:
95
+ path = ARKAOS_ROOT / "knowledge" / "commands-registry-v2.json"
96
+ if path.exists():
97
+ data = json.loads(path.read_text())
98
+ _commands_cache = data.get("commands", [])
99
+ else:
100
+ _commands_cache = []
101
+ return _commands_cache
102
+
103
+
104
+ def _get_budget_manager():
105
+ try:
106
+ from core.budget.manager import BudgetManager
107
+ db_path = Path.home() / ".arkaos" / "budget-usage.json"
108
+ return BudgetManager(storage_path=db_path)
109
+ except Exception:
110
+ return None
111
+
112
+
113
+ def _get_task_manager():
114
+ try:
115
+ from core.tasks.manager import TaskManager
116
+ db_path = Path.home() / ".arkaos" / "tasks.json"
117
+ return TaskManager(storage_path=db_path)
118
+ except Exception:
119
+ return None
120
+
121
+
122
+ def _get_vector_store():
123
+ try:
124
+ from core.knowledge.vector_store import VectorStore
125
+ db_path = Path.home() / ".arkaos" / "knowledge.db"
126
+ if db_path.exists():
127
+ return VectorStore(db_path)
128
+ except Exception:
129
+ pass
130
+ return None
131
+
132
+
133
+ # --- Endpoints ---
134
+
135
+ @app.get("/api/overview")
136
+ def overview():
137
+ agents = _load_agents()
138
+ commands = _load_commands()
139
+ departments = set(a.get("department", "") for a in agents)
140
+
141
+ skills_count = 0
142
+ try:
143
+ skills_count = int(subprocess.run(
144
+ ["find", str(ARKAOS_ROOT / "departments"), "-name", "SKILL.md", "-path", "*/skills/*/SKILL.md"],
145
+ capture_output=True, text=True, timeout=5,
146
+ ).stdout.strip().count("\n")) + 1
147
+ except Exception:
148
+ skills_count = 250
149
+
150
+ budget_mgr = _get_budget_manager()
151
+ task_mgr = _get_task_manager()
152
+ store = _get_vector_store()
153
+
154
+ return {
155
+ "agents": len(agents),
156
+ "commands": len(commands),
157
+ "departments": len(departments),
158
+ "skills": skills_count,
159
+ "workflows": 24,
160
+ "tests": 1809,
161
+ "version": "2.0.3",
162
+ "budget": budget_mgr.get_summary(tier=2).model_dump() if budget_mgr else None,
163
+ "tasks": task_mgr.summary() if task_mgr else {"total": 0, "active": 0, "queued": 0},
164
+ "knowledge": store.get_stats() if store else {"total_chunks": 0, "total_files": 0},
165
+ }
166
+
167
+
168
+ @app.get("/api/agents")
169
+ def agents(dept: Optional[str] = Query(None)):
170
+ data = _load_agents()
171
+ if dept:
172
+ data = [a for a in data if a.get("department") == dept]
173
+ return {"agents": data, "total": len(data)}
174
+
175
+
176
+ @app.get("/api/agents/{agent_id}")
177
+ def agent_detail(agent_id: str):
178
+ """Get full agent detail including YAML data."""
179
+ # First get registry data
180
+ agents = _load_agents()
181
+ base = None
182
+ for a in agents:
183
+ if a.get("id") == agent_id:
184
+ base = dict(a)
185
+ break
186
+ if not base:
187
+ return {"error": "Agent not found"}
188
+
189
+ # Enrich with full YAML data
190
+ yaml_file = ARKAOS_ROOT / base.get("file", "")
191
+ if yaml_file.exists():
192
+ try:
193
+ import yaml
194
+ raw = yaml.safe_load(yaml_file.read_text())
195
+ dna = raw.get("behavioral_dna", {})
196
+ disc = dna.get("disc", {})
197
+ ennea = dna.get("enneagram", {})
198
+
199
+ base["disc"]["communication_style"] = disc.get("communication_style", "")
200
+ base["disc"]["under_pressure"] = disc.get("under_pressure", "")
201
+ base["disc"]["motivator"] = disc.get("motivator", "")
202
+ base["enneagram"]["core_motivation"] = ennea.get("core_motivation", "")
203
+ base["enneagram"]["core_fear"] = ennea.get("core_fear", "")
204
+ base["enneagram"]["subtype"] = ennea.get("subtype", "")
205
+
206
+ base["mental_models"] = raw.get("mental_models", {})
207
+ base["communication"] = raw.get("communication", {})
208
+
209
+ auth = raw.get("authority", {})
210
+ base["authority"]["delegates_to"] = auth.get("delegates_to", [])
211
+ base["authority"]["escalates_to"] = auth.get("escalates_to", "")
212
+
213
+ expertise = raw.get("expertise", {})
214
+ base["expertise_depth"] = expertise.get("depth", "")
215
+ base["expertise_years"] = expertise.get("years_equivalent", 0)
216
+ except Exception:
217
+ pass
218
+
219
+ return base
220
+
221
+
222
+ @app.get("/api/commands")
223
+ def commands(dept: Optional[str] = Query(None), q: Optional[str] = Query(None)):
224
+ data = _load_commands()
225
+ if dept:
226
+ data = [c for c in data if c.get("department") == dept]
227
+ if q:
228
+ q_lower = q.lower()
229
+ data = [c for c in data if q_lower in c.get("command", "").lower() or q_lower in c.get("description", "").lower()]
230
+ return {"commands": data, "total": len(data)}
231
+
232
+
233
+ @app.get("/api/budget")
234
+ def budget_all():
235
+ mgr = _get_budget_manager()
236
+ if not mgr:
237
+ return {"tiers": [], "departments": [], "summary": {"total_tokens": 0, "total_ops": 0, "active_departments": 0}}
238
+
239
+ # Department breakdown from raw usages
240
+ dept_data: dict[str, dict] = {}
241
+ for u in mgr._usages:
242
+ dept = u.department or "system"
243
+ if dept not in dept_data:
244
+ dept_data[dept] = {"department": dept, "tokens": 0, "operations": 0}
245
+ dept_data[dept]["tokens"] += u.tokens
246
+ dept_data[dept]["operations"] += 1
247
+
248
+ departments = sorted(dept_data.values(), key=lambda d: d["tokens"], reverse=True)
249
+ max_tokens = departments[0]["tokens"] if departments else 1
250
+
251
+ # Add relative percentage for bar width
252
+ for d in departments:
253
+ d["percent"] = round((d["tokens"] / max_tokens) * 100) if max_tokens > 0 else 0
254
+
255
+ total_tokens = sum(d["tokens"] for d in departments)
256
+ total_ops = sum(d["operations"] for d in departments)
257
+
258
+ return {
259
+ "summary": {
260
+ "total_tokens": total_tokens,
261
+ "total_ops": total_ops,
262
+ "active_departments": len(departments),
263
+ "estimated_cost_usd": round(total_tokens * 0.000003, 4), # ~$3 per 1M input tokens
264
+ },
265
+ "departments": departments,
266
+ "tiers": [mgr.get_summary(tier=t).model_dump() for t in range(4)],
267
+ }
268
+
269
+
270
+ @app.get("/api/budget/{tier}")
271
+ def budget_tier(tier: int):
272
+ mgr = _get_budget_manager()
273
+ if not mgr:
274
+ return {"error": "Budget manager unavailable"}
275
+ return mgr.get_summary(tier=tier).model_dump()
276
+
277
+
278
+ @app.get("/api/tasks")
279
+ def tasks(status: Optional[str] = Query(None)):
280
+ mgr = _get_task_manager()
281
+ if not mgr:
282
+ return {"tasks": [], "summary": {"total": 0}}
283
+ from core.tasks.schema import TaskStatus
284
+ task_list = mgr.list_all(TaskStatus(status) if status else None)
285
+ return {
286
+ "tasks": [t.model_dump() for t in task_list],
287
+ "summary": mgr.summary(),
288
+ }
289
+
290
+
291
+ @app.get("/api/tasks/active")
292
+ def tasks_active():
293
+ mgr = _get_task_manager()
294
+ if not mgr:
295
+ return {"tasks": []}
296
+ return {"tasks": [t.model_dump() for t in mgr.list_active()]}
297
+
298
+
299
+ # --- Job Queue (SQLite) ---
300
+
301
+ _job_manager = None
302
+
303
+ def _get_job_manager():
304
+ global _job_manager
305
+ if _job_manager is None:
306
+ try:
307
+ from core.jobs.manager import JobManager
308
+ _job_manager = JobManager()
309
+ except Exception:
310
+ return None
311
+ return _job_manager
312
+
313
+
314
+ @app.get("/api/jobs")
315
+ def jobs_list(status: Optional[str] = Query(None), limit: int = Query(50)):
316
+ mgr = _get_job_manager()
317
+ if not mgr:
318
+ return {"jobs": [], "summary": {}}
319
+ if status:
320
+ jobs = mgr.list_by_status(status, limit)
321
+ else:
322
+ jobs = mgr.list_all(limit)
323
+ return {"jobs": [j.to_dict() for j in jobs], "summary": mgr.summary()}
324
+
325
+
326
+ @app.get("/api/jobs/{job_id}")
327
+ def job_detail(job_id: str):
328
+ mgr = _get_job_manager()
329
+ if not mgr:
330
+ return {"error": "Job manager unavailable"}
331
+ job = mgr.get(job_id)
332
+ if not job:
333
+ return {"error": "Job not found"}
334
+ return job.to_dict()
335
+
336
+
337
+ @app.delete("/api/jobs/{job_id}")
338
+ def job_cancel(job_id: str):
339
+ mgr = _get_job_manager()
340
+ if not mgr:
341
+ return {"error": "Job manager unavailable"}
342
+ if mgr.cancel(job_id):
343
+ broadcast_from_thread({"type": "job_cancelled", "job_id": job_id})
344
+ return {"cancelled": True}
345
+ return {"error": "Can only cancel queued jobs"}
346
+
347
+
348
+ @app.post("/api/knowledge/ingest")
349
+ def knowledge_ingest(body: dict):
350
+ """Ingest content into the knowledge base. Runs in background with SQLite job tracking."""
351
+ import threading
352
+
353
+ source = body.get("source", "")
354
+ source_type = body.get("type", "")
355
+ if not source:
356
+ return {"error": "source is required"}
357
+
358
+ store = _get_vector_store()
359
+ if not store:
360
+ from core.knowledge.vector_store import VectorStore
361
+ kb_db = Path.home() / ".arkaos" / "knowledge.db"
362
+ kb_db.parent.mkdir(parents=True, exist_ok=True)
363
+ store = VectorStore(kb_db)
364
+
365
+ from core.knowledge.ingest import IngestEngine, detect_source_type
366
+ if not source_type:
367
+ source_type = detect_source_type(source)
368
+
369
+ # Create job in SQLite
370
+ job_mgr = _get_job_manager()
371
+ job = job_mgr.create(source=source, source_type=source_type)
372
+
373
+ job_id = job.id # Capture ID for thread
374
+
375
+ def run_ingest():
376
+ # Create thread-local JobManager (SQLite objects can't cross threads)
377
+ from core.jobs.manager import JobManager as _JM
378
+ local_mgr = _JM()
379
+
380
+ engine = IngestEngine(store)
381
+ def on_progress(pct, msg):
382
+ status = "processing"
383
+ if "phase 2" in msg.lower() or "download" in msg.lower():
384
+ status = "downloading"
385
+ elif "phase 3" in msg.lower() or "extract" in msg.lower():
386
+ status = "processing"
387
+ elif "phase 4" in msg.lower() or "transcrib" in msg.lower():
388
+ status = "transcribing"
389
+ elif "embed" in msg.lower() or "index" in msg.lower():
390
+ status = "embedding"
391
+ local_mgr.update_progress(job_id, pct, msg, status)
392
+ broadcast_from_thread({
393
+ "type": "job_progress",
394
+ "job_id": job_id,
395
+ "progress": pct,
396
+ "message": msg,
397
+ "status": status,
398
+ })
399
+ try:
400
+ local_mgr.start(job_id)
401
+ broadcast_from_thread({"type": "job_progress", "job_id": job_id, "progress": 0, "message": "Starting...", "status": "processing"})
402
+ result = engine.ingest(source, source_type, on_progress=on_progress)
403
+ if result.success:
404
+ local_mgr.complete(job_id, chunks_created=result.chunks_created)
405
+ broadcast_from_thread({"type": "job_complete", "job_id": job_id, "chunks_created": result.chunks_created})
406
+ else:
407
+ local_mgr.fail(job_id, result.error)
408
+ broadcast_from_thread({"type": "job_failed", "job_id": job_id, "error": result.error})
409
+ except Exception as e:
410
+ local_mgr.fail(job_id, str(e))
411
+ broadcast_from_thread({"type": "job_failed", "job_id": job_id, "error": str(e)})
412
+
413
+ thread = threading.Thread(target=run_ingest, daemon=True)
414
+ thread.start()
415
+
416
+ return {"job_id": job.id, "source_type": source_type, "status": "queued"}
417
+
418
+
419
+ @app.get("/api/tasks/{task_id}")
420
+ def task_detail(task_id: str):
421
+ """Get a single task by ID. Also checks jobs."""
422
+ # Check jobs first (new system)
423
+ job_mgr = _get_job_manager()
424
+ if job_mgr:
425
+ job = job_mgr.get(task_id)
426
+ if job:
427
+ return job.to_dict()
428
+ # Fallback to old task manager
429
+ mgr = _get_task_manager()
430
+ if not mgr:
431
+ return {"error": "Task manager unavailable"}
432
+ task = mgr.get(task_id)
433
+ if not task:
434
+ return {"error": "Task not found"}
435
+ return task.model_dump()
436
+
437
+
438
+ @app.get("/api/knowledge/stats")
439
+ def knowledge_stats():
440
+ store = _get_vector_store()
441
+ if not store:
442
+ return {"total_chunks": 0, "total_files": 0, "vss_available": False, "indexed": False}
443
+ stats = store.get_stats()
444
+ stats["indexed"] = stats["total_chunks"] > 0
445
+ return stats
446
+
447
+
448
+ @app.get("/api/knowledge/search")
449
+ def knowledge_search(q: str = Query(...), top_k: int = Query(5)):
450
+ store = _get_vector_store()
451
+ if not store:
452
+ return {"results": [], "query": q}
453
+ results = store.search(q, top_k=min(top_k, 20))
454
+ return {"results": results, "query": q, "total": len(results)}
455
+
456
+
457
+ @app.get("/api/health")
458
+ def health():
459
+ checks = []
460
+ arkaos_home = Path.home() / ".arkaos"
461
+
462
+ def check(name, condition, fix=""):
463
+ checks.append({"name": name, "passed": condition, "fix": fix})
464
+
465
+ check("install_dir", arkaos_home.exists(), "Run: npx arkaos install")
466
+ check("manifest", (arkaos_home / "install-manifest.json").exists(), "Run: npx arkaos install")
467
+ check("constitution", (ARKAOS_ROOT / "config" / "constitution.yaml").exists())
468
+ check("agents_registry", (ARKAOS_ROOT / "knowledge" / "agents-registry-v2.json").exists())
469
+ check("commands_registry", (ARKAOS_ROOT / "knowledge" / "commands-registry-v2.json").exists())
470
+ check("hooks_dir", (arkaos_home / "config" / "hooks").exists(), "Run: npx arkaos install")
471
+
472
+ try:
473
+ subprocess.run(["python3", "--version"], capture_output=True, timeout=2)
474
+ check("python", True)
475
+ except Exception:
476
+ check("python", False, "Install Python 3.11+")
477
+
478
+ check("knowledge_db", (arkaos_home / "knowledge.db").exists(), "Run: npx arkaos index")
479
+
480
+ passed = sum(1 for c in checks if c["passed"])
481
+ return {"checks": checks, "passed": passed, "total": len(checks), "healthy": passed == len(checks)}
482
+
483
+
484
+ # --- Personas ---
485
+
486
+ def _get_persona_manager():
487
+ try:
488
+ from core.personas.manager import PersonaManager
489
+ db_path = Path.home() / ".arkaos" / "personas.json"
490
+ return PersonaManager(storage_path=db_path)
491
+ except Exception:
492
+ return None
493
+
494
+
495
+ @app.get("/api/personas")
496
+ def personas_list():
497
+ mgr = _get_persona_manager()
498
+ if not mgr:
499
+ return {"personas": [], "total": 0}
500
+ personas = mgr.list_all()
501
+ return {"personas": [p.model_dump() for p in personas], "total": len(personas)}
502
+
503
+
504
+ @app.get("/api/personas/{persona_id}")
505
+ def persona_detail(persona_id: str):
506
+ mgr = _get_persona_manager()
507
+ if not mgr:
508
+ return {"error": "Persona manager unavailable"}
509
+ p = mgr.get(persona_id)
510
+ if not p:
511
+ return {"error": "Persona not found"}
512
+ return p.model_dump()
513
+
514
+
515
+ @app.post("/api/personas")
516
+ def persona_create(body: dict):
517
+ mgr = _get_persona_manager()
518
+ if not mgr:
519
+ return {"error": "Persona manager unavailable"}
520
+
521
+ from core.personas.schema import (
522
+ Persona, PersonaDISC, PersonaEnneagram, PersonaBigFive, PersonaCommunication,
523
+ )
524
+
525
+ # Generate ID from name
526
+ name = body.get("name", "Unknown")
527
+ persona_id = name.lower().replace(" ", "-").replace(".", "")
528
+
529
+ persona = Persona(
530
+ id=persona_id,
531
+ name=name,
532
+ title=body.get("title", ""),
533
+ tagline=body.get("tagline", ""),
534
+ source=body.get("source", name),
535
+ disc=PersonaDISC(**(body.get("disc", {}))),
536
+ enneagram=PersonaEnneagram(**(body.get("enneagram", {}))),
537
+ big_five=PersonaBigFive(**(body.get("big_five", {}))),
538
+ mbti=body.get("mbti", "INTJ"),
539
+ mental_models=body.get("mental_models", []),
540
+ expertise_domains=body.get("expertise_domains", []),
541
+ frameworks=body.get("frameworks", []),
542
+ key_quotes=body.get("key_quotes", []),
543
+ communication=PersonaCommunication(**(body.get("communication", {}))),
544
+ )
545
+
546
+ mgr.create(persona)
547
+ return {"id": persona.id, "created": True}
548
+
549
+
550
+ @app.post("/api/personas/{persona_id}/clone")
551
+ def persona_clone(persona_id: str, body: dict = {}):
552
+ mgr = _get_persona_manager()
553
+ if not mgr:
554
+ return {"error": "Persona manager unavailable"}
555
+
556
+ department = body.get("department", "strategy")
557
+ tier = body.get("tier", 2)
558
+ agents_dir = ARKAOS_ROOT / "departments" / department / "agents"
559
+
560
+ agent_id = mgr.clone_to_agent(persona_id, department=department, tier=tier, agents_dir=str(agents_dir))
561
+ if not agent_id:
562
+ return {"error": "Persona not found"}
563
+
564
+ return {"agent_id": agent_id, "department": department, "file": f"departments/{department}/agents/{agent_id}.yaml"}
565
+
566
+
567
+ @app.delete("/api/personas/{persona_id}")
568
+ def persona_delete(persona_id: str):
569
+ mgr = _get_persona_manager()
570
+ if not mgr:
571
+ return {"error": "Persona manager unavailable"}
572
+ if mgr.delete(persona_id):
573
+ return {"deleted": True}
574
+ return {"error": "Persona not found"}
575
+
576
+
577
+ # --- API Keys ---
578
+
579
+ @app.get("/api/keys")
580
+ def keys_list():
581
+ try:
582
+ from core.keys import list_keys
583
+ return {"keys": list_keys()}
584
+ except Exception:
585
+ return {"keys": []}
586
+
587
+
588
+ @app.post("/api/keys")
589
+ def keys_set(body: dict):
590
+ try:
591
+ from core.keys import set_key
592
+ name = body.get("key", "")
593
+ value = body.get("value", "")
594
+ if not name or not value:
595
+ return {"error": "key and value required"}
596
+ set_key(name, value)
597
+ return {"set": True, "key": name}
598
+ except Exception as e:
599
+ return {"error": str(e)}
600
+
601
+
602
+ @app.delete("/api/keys/{key_name}")
603
+ def keys_delete(key_name: str):
604
+ try:
605
+ from core.keys import remove_key
606
+ if remove_key(key_name):
607
+ return {"deleted": True}
608
+ return {"error": "Key not found"}
609
+ except Exception as e:
610
+ return {"error": str(e)}
611
+
612
+
613
+ @app.get("/api/metrics")
614
+ def metrics():
615
+ metrics_file = Path("/tmp/arkaos-context-cache/hook-metrics.jsonl")
616
+ if not metrics_file.exists():
617
+ return {"entries": [], "avg_ms": 0}
618
+ entries = []
619
+ for line in metrics_file.read_text().strip().split("\n"):
620
+ try:
621
+ entries.append(json.loads(line))
622
+ except Exception:
623
+ continue
624
+ avg_ms = sum(e.get("ms", 0) for e in entries) / len(entries) if entries else 0
625
+ return {"entries": entries[-50:], "avg_ms": round(avg_ms, 1), "total_calls": len(entries)}
626
+
627
+
628
+ if __name__ == "__main__":
629
+ import argparse
630
+ parser = argparse.ArgumentParser()
631
+ parser.add_argument("--port", type=int, default=3334)
632
+ parser.add_argument("--host", type=str, default="127.0.0.1")
633
+ args = parser.parse_args()
634
+
635
+ import uvicorn
636
+ uvicorn.run(app, host=args.host, port=args.port, log_level="info")