arkaos 3.35.0 → 3.37.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.
|
|
1
|
+
3.37.0
|
|
@@ -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
|
-
<
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
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"
|
|
@@ -7,6 +7,14 @@
|
|
|
7
7
|
|
|
8
8
|
import type { TableColumn } from '@nuxt/ui'
|
|
9
9
|
|
|
10
|
+
interface WorkflowPhase {
|
|
11
|
+
id: string
|
|
12
|
+
name: string
|
|
13
|
+
description: string
|
|
14
|
+
gate_type: string
|
|
15
|
+
agent_count: number
|
|
16
|
+
}
|
|
17
|
+
|
|
10
18
|
interface Workflow {
|
|
11
19
|
id: string
|
|
12
20
|
name: string
|
|
@@ -15,6 +23,7 @@ interface Workflow {
|
|
|
15
23
|
tier: string
|
|
16
24
|
command: string
|
|
17
25
|
phases_count: number
|
|
26
|
+
phases: WorkflowPhase[]
|
|
18
27
|
file: string
|
|
19
28
|
content: string
|
|
20
29
|
}
|
|
@@ -35,7 +44,16 @@ interface WorkflowRun {
|
|
|
35
44
|
}
|
|
36
45
|
const runs = ref<WorkflowRun[]>([])
|
|
37
46
|
const runsLoading = ref(false)
|
|
38
|
-
const sidePanelTab = ref<'yaml' | 'runs'>('
|
|
47
|
+
const sidePanelTab = ref<'flow' | 'yaml' | 'runs'>('flow')
|
|
48
|
+
|
|
49
|
+
function gateColor(gateType: string): 'primary' | 'warning' | 'error' | 'neutral' {
|
|
50
|
+
const m: Record<string, 'primary' | 'warning' | 'error' | 'neutral'> = {
|
|
51
|
+
user_approval: 'warning',
|
|
52
|
+
quality_gate: 'error',
|
|
53
|
+
automatic: 'primary',
|
|
54
|
+
}
|
|
55
|
+
return m[gateType] ?? 'neutral'
|
|
56
|
+
}
|
|
39
57
|
|
|
40
58
|
async function loadRuns(id: string) {
|
|
41
59
|
runsLoading.value = true
|
|
@@ -152,7 +170,7 @@ const columns: TableColumn<Workflow>[] = [
|
|
|
152
170
|
th: 'py-2 first:rounded-l-lg last:rounded-r-lg border-y border-default first:border-l last:border-r',
|
|
153
171
|
td: 'border-b border-default',
|
|
154
172
|
}"
|
|
155
|
-
@select="(row: { original: Workflow }) => { selected = row.original; sidePanelTab = '
|
|
173
|
+
@select="(row: { original: Workflow }) => { selected = row.original; sidePanelTab = 'flow'; runs = []; loadRuns(row.original.id) }"
|
|
156
174
|
>
|
|
157
175
|
<template #name-cell="{ row }">
|
|
158
176
|
<div class="min-w-0">
|
|
@@ -205,6 +223,14 @@ const columns: TableColumn<Workflow>[] = [
|
|
|
205
223
|
{{ selected.description }}
|
|
206
224
|
</p>
|
|
207
225
|
<div class="flex items-center gap-1 mt-3 text-xs">
|
|
226
|
+
<button
|
|
227
|
+
type="button"
|
|
228
|
+
class="px-2 py-1 rounded-md transition-colors"
|
|
229
|
+
:class="sidePanelTab === 'flow' ? 'bg-elevated/60 text-default font-semibold' : 'text-muted hover:text-default'"
|
|
230
|
+
@click="sidePanelTab = 'flow'"
|
|
231
|
+
>
|
|
232
|
+
Flow
|
|
233
|
+
</button>
|
|
208
234
|
<button
|
|
209
235
|
type="button"
|
|
210
236
|
class="px-2 py-1 rounded-md transition-colors"
|
|
@@ -223,7 +249,43 @@ const columns: TableColumn<Workflow>[] = [
|
|
|
223
249
|
</button>
|
|
224
250
|
</div>
|
|
225
251
|
</div>
|
|
226
|
-
<div v-if="sidePanelTab === '
|
|
252
|
+
<div v-if="sidePanelTab === 'flow'" class="p-4">
|
|
253
|
+
<ol v-if="selected.phases.length > 0" class="relative border-l border-default ml-2 space-y-3">
|
|
254
|
+
<li
|
|
255
|
+
v-for="(ph, idx) in selected.phases"
|
|
256
|
+
:key="ph.id || idx"
|
|
257
|
+
class="ml-4"
|
|
258
|
+
>
|
|
259
|
+
<span class="absolute -left-1.5 size-3 rounded-full bg-primary border border-primary/60" />
|
|
260
|
+
<div class="rounded-lg border border-default p-3 bg-elevated/20">
|
|
261
|
+
<div class="flex items-center gap-2 flex-wrap">
|
|
262
|
+
<span class="font-mono text-xs text-muted">{{ idx + 1 }}.</span>
|
|
263
|
+
<span class="font-semibold">{{ ph.name || ph.id }}</span>
|
|
264
|
+
<UBadge
|
|
265
|
+
v-if="ph.gate_type"
|
|
266
|
+
:label="ph.gate_type"
|
|
267
|
+
:color="gateColor(ph.gate_type)"
|
|
268
|
+
variant="subtle"
|
|
269
|
+
size="xs"
|
|
270
|
+
/>
|
|
271
|
+
<UBadge
|
|
272
|
+
v-if="ph.agent_count > 0"
|
|
273
|
+
:label="`${ph.agent_count} agent${ph.agent_count === 1 ? '' : 's'}`"
|
|
274
|
+
variant="outline"
|
|
275
|
+
size="xs"
|
|
276
|
+
/>
|
|
277
|
+
</div>
|
|
278
|
+
<p v-if="ph.description" class="text-xs text-muted mt-1">
|
|
279
|
+
{{ ph.description }}
|
|
280
|
+
</p>
|
|
281
|
+
</div>
|
|
282
|
+
</li>
|
|
283
|
+
</ol>
|
|
284
|
+
<div v-else class="py-6 text-center text-sm text-muted">
|
|
285
|
+
No phases defined.
|
|
286
|
+
</div>
|
|
287
|
+
</div>
|
|
288
|
+
<div v-else-if="sidePanelTab === 'yaml'" class="overflow-x-auto">
|
|
227
289
|
<pre class="p-4 text-xs font-mono whitespace-pre">{{ selected.content }}</pre>
|
|
228
290
|
</div>
|
|
229
291
|
<div v-else class="p-4">
|
package/package.json
CHANGED
package/pyproject.toml
CHANGED
|
Binary file
|
package/scripts/dashboard-api.py
CHANGED
|
@@ -1848,12 +1848,33 @@ def workflows_list():
|
|
|
1848
1848
|
"tier": str(raw.get("tier") or ""),
|
|
1849
1849
|
"command": str(raw.get("command") or ""),
|
|
1850
1850
|
"phases_count": len(phases),
|
|
1851
|
+
"phases": _summarise_phases(phases), # PR91c v3.37.0
|
|
1851
1852
|
"file": rel,
|
|
1852
1853
|
"content": content,
|
|
1853
1854
|
})
|
|
1854
1855
|
return {"workflows": out}
|
|
1855
1856
|
|
|
1856
1857
|
|
|
1858
|
+
def _summarise_phases(phases: list) -> list[dict]:
|
|
1859
|
+
"""PR91c v3.37.0 — distil each phase down to what the flow stepper needs."""
|
|
1860
|
+
out: list[dict] = []
|
|
1861
|
+
for p in phases:
|
|
1862
|
+
if not isinstance(p, dict):
|
|
1863
|
+
continue
|
|
1864
|
+
gate = p.get("gate") if isinstance(p.get("gate"), dict) else {}
|
|
1865
|
+
agents = p.get("agents") if isinstance(p.get("agents"), list) else []
|
|
1866
|
+
out.append({
|
|
1867
|
+
"id": str(p.get("id") or ""),
|
|
1868
|
+
"name": str(p.get("name") or ""),
|
|
1869
|
+
"description": str(p.get("description") or ""),
|
|
1870
|
+
"gate_type": str(gate.get("type") or ""),
|
|
1871
|
+
"agent_count": sum(
|
|
1872
|
+
1 for a in agents if isinstance(a, dict) and a.get("agent_id")
|
|
1873
|
+
),
|
|
1874
|
+
})
|
|
1875
|
+
return out
|
|
1876
|
+
|
|
1877
|
+
|
|
1857
1878
|
# --- Sidebar stats widget (PR87d v3.22.0) ---
|
|
1858
1879
|
|
|
1859
1880
|
@app.get("/api/sidebar-stats")
|
|
@@ -2004,16 +2025,24 @@ def global_search(q: str = "", limit: int = 20):
|
|
|
2004
2025
|
def personas_import(body: dict):
|
|
2005
2026
|
"""Import persona Markdown files (frontmatter + body) into the store.
|
|
2006
2027
|
|
|
2007
|
-
Body:
|
|
2008
|
-
|
|
2028
|
+
Body:
|
|
2029
|
+
{"files": [{"name": "Alex.md", "content": "..."}]} # local picker
|
|
2030
|
+
{"urls": ["https://example.com/raw.md", ...]} # remote import
|
|
2031
|
+
|
|
2032
|
+
Both keys are accepted; URLs are fetched server-side (PR91b
|
|
2033
|
+
v3.36.0) and converted into the same `{name, content}` shape
|
|
2034
|
+
before processing.
|
|
2009
2035
|
|
|
2010
|
-
|
|
2011
|
-
lacking the frontmatter are flagged as failed without partial
|
|
2012
|
-
side-effects.
|
|
2036
|
+
Returns: {imported, failed, results: [{filename, status, id?, error?}]}
|
|
2013
2037
|
"""
|
|
2014
|
-
|
|
2015
|
-
|
|
2038
|
+
raw_files = body.get("files")
|
|
2039
|
+
raw_urls = body.get("urls")
|
|
2040
|
+
if raw_files is not None and not isinstance(raw_files, list):
|
|
2016
2041
|
return {"error": "files must be a list"}
|
|
2042
|
+
if raw_urls is not None and not isinstance(raw_urls, list):
|
|
2043
|
+
return {"error": "urls must be a list"}
|
|
2044
|
+
files = list(raw_files or [])
|
|
2045
|
+
urls = list(raw_urls or [])
|
|
2017
2046
|
mgr = _get_persona_manager()
|
|
2018
2047
|
if not mgr:
|
|
2019
2048
|
return {"error": "Persona manager unavailable"}
|
|
@@ -2022,6 +2051,10 @@ def personas_import(body: dict):
|
|
|
2022
2051
|
|
|
2023
2052
|
from core.personas.obsidian_store import ObsidianPersonaStore
|
|
2024
2053
|
|
|
2054
|
+
# Resolve URLs into {name, content} entries.
|
|
2055
|
+
if urls:
|
|
2056
|
+
files.extend(_fetch_url_entries(urls))
|
|
2057
|
+
|
|
2025
2058
|
imported = 0
|
|
2026
2059
|
failed = 0
|
|
2027
2060
|
results: list[dict] = []
|
|
@@ -2032,6 +2065,14 @@ def personas_import(body: dict):
|
|
|
2032
2065
|
continue
|
|
2033
2066
|
filename = str(entry.get("name") or "")
|
|
2034
2067
|
content = str(entry.get("content") or "")
|
|
2068
|
+
# Carry forward URL-fetch errors so the operator sees them.
|
|
2069
|
+
if entry.get("fetch_error"):
|
|
2070
|
+
failed += 1
|
|
2071
|
+
results.append({
|
|
2072
|
+
"filename": filename, "status": "failed",
|
|
2073
|
+
"error": str(entry["fetch_error"]),
|
|
2074
|
+
})
|
|
2075
|
+
continue
|
|
2035
2076
|
if not content.strip():
|
|
2036
2077
|
failed += 1
|
|
2037
2078
|
results.append({"filename": filename, "status": "failed", "error": "empty content"})
|
|
@@ -2058,6 +2099,38 @@ def personas_import(body: dict):
|
|
|
2058
2099
|
return {"imported": imported, "failed": failed, "results": results}
|
|
2059
2100
|
|
|
2060
2101
|
|
|
2102
|
+
def _fetch_url_entries(urls: list[str]) -> list[dict]:
|
|
2103
|
+
"""Fetch each URL and return ``{name, content}`` entries. PR91b."""
|
|
2104
|
+
import urllib.error
|
|
2105
|
+
import urllib.parse
|
|
2106
|
+
import urllib.request
|
|
2107
|
+
out: list[dict] = []
|
|
2108
|
+
for raw in urls:
|
|
2109
|
+
url = str(raw or "").strip()
|
|
2110
|
+
if not url:
|
|
2111
|
+
continue
|
|
2112
|
+
parsed = urllib.parse.urlparse(url)
|
|
2113
|
+
if parsed.scheme not in ("http", "https"):
|
|
2114
|
+
out.append({"name": url, "fetch_error": "scheme must be http(s)"})
|
|
2115
|
+
continue
|
|
2116
|
+
try:
|
|
2117
|
+
req = urllib.request.Request(url, headers={"User-Agent": "ArkaOS/persona-import"})
|
|
2118
|
+
with urllib.request.urlopen(req, timeout=10) as resp:
|
|
2119
|
+
content = resp.read().decode("utf-8", errors="replace")
|
|
2120
|
+
except urllib.error.URLError as exc:
|
|
2121
|
+
out.append({"name": url, "fetch_error": f"fetch failed: {exc.reason}"})
|
|
2122
|
+
continue
|
|
2123
|
+
except (TimeoutError, OSError) as exc:
|
|
2124
|
+
out.append({"name": url, "fetch_error": f"fetch failed: {exc}"})
|
|
2125
|
+
continue
|
|
2126
|
+
# Derive a filename from the URL path.
|
|
2127
|
+
name = (parsed.path.rsplit("/", 1)[-1] or "imported.md").strip()
|
|
2128
|
+
if not name.endswith(".md"):
|
|
2129
|
+
name = f"{name or 'imported'}.md"
|
|
2130
|
+
out.append({"name": name, "content": content})
|
|
2131
|
+
return out
|
|
2132
|
+
|
|
2133
|
+
|
|
2061
2134
|
# --- Trash / Undo (PR85b v3.12.0) ---
|
|
2062
2135
|
|
|
2063
2136
|
@app.get("/api/trash")
|