arkaos 3.35.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.35.0
1
+ 3.36.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
- <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.35.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.35.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"}
@@ -2004,16 +2004,24 @@ def global_search(q: str = "", limit: int = 20):
2004
2004
  def personas_import(body: dict):
2005
2005
  """Import persona Markdown files (frontmatter + body) into the store.
2006
2006
 
2007
- Body: {"files": [{"name": "Alex.md", "content": "<full file body>"}]}
2008
- 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.
2009
2014
 
2010
- Each file MUST have YAML frontmatter with ``type: persona``. Files
2011
- lacking the frontmatter are flagged as failed without partial
2012
- side-effects.
2015
+ Returns: {imported, failed, results: [{filename, status, id?, error?}]}
2013
2016
  """
2014
- files = body.get("files") or []
2015
- 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):
2016
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 [])
2017
2025
  mgr = _get_persona_manager()
2018
2026
  if not mgr:
2019
2027
  return {"error": "Persona manager unavailable"}
@@ -2022,6 +2030,10 @@ def personas_import(body: dict):
2022
2030
 
2023
2031
  from core.personas.obsidian_store import ObsidianPersonaStore
2024
2032
 
2033
+ # Resolve URLs into {name, content} entries.
2034
+ if urls:
2035
+ files.extend(_fetch_url_entries(urls))
2036
+
2025
2037
  imported = 0
2026
2038
  failed = 0
2027
2039
  results: list[dict] = []
@@ -2032,6 +2044,14 @@ def personas_import(body: dict):
2032
2044
  continue
2033
2045
  filename = str(entry.get("name") or "")
2034
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
2035
2055
  if not content.strip():
2036
2056
  failed += 1
2037
2057
  results.append({"filename": filename, "status": "failed", "error": "empty content"})
@@ -2058,6 +2078,38 @@ def personas_import(body: dict):
2058
2078
  return {"imported": imported, "failed": failed, "results": results}
2059
2079
 
2060
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
+
2061
2113
  # --- Trash / Undo (PR85b v3.12.0) ---
2062
2114
 
2063
2115
  @app.get("/api/trash")