arkaos 3.7.0 → 3.9.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.7.0
1
+ 3.9.0
@@ -158,9 +158,59 @@ function goToAgent(id: string) {
158
158
  }
159
159
 
160
160
  // PR83b v3.4.0 — bulk selection + delete.
161
+ // PR84b v3.8.0 — bulk move department.
161
162
  const confirmDialog = useConfirmDialog()
162
163
  const selected = ref<Set<string>>(new Set())
163
164
  const bulkDeleting = ref(false)
165
+ const bulkMoving = ref(false)
166
+
167
+ const departmentMoveOptions = [
168
+ 'dev', 'marketing', 'brand', 'finance', 'strategy', 'ecom', 'kb', 'ops',
169
+ 'pm', 'saas', 'landing', 'content', 'community', 'sales', 'leadership', 'org',
170
+ ].map((d) => ({
171
+ label: `Move to ${d}`,
172
+ icon: 'i-lucide-arrow-right',
173
+ onSelect: () => bulkMove(d),
174
+ }))
175
+
176
+ async function bulkMove(targetDept: string) {
177
+ if (selected.value.size === 0) return
178
+ const ids = Array.from(selected.value)
179
+ const ok = await confirmDialog({
180
+ title: `Move ${ids.length} agent${ids.length === 1 ? '' : 's'} to ${targetDept}?`,
181
+ description: 'The YAML files will be relocated and their `department:` field updated. Tier 0 agents and unknown departments are skipped.',
182
+ confirmLabel: `Move to ${targetDept}`,
183
+ cancelLabel: 'Cancel',
184
+ })
185
+ if (!ok) return
186
+ bulkMoving.value = true
187
+ const results = await Promise.allSettled(
188
+ ids.map((id) =>
189
+ $fetch<{ moved?: boolean, error?: string }>(`${apiBase}/api/agents/${id}/move`, {
190
+ method: 'POST',
191
+ body: { department: targetDept },
192
+ }),
193
+ ),
194
+ )
195
+ const successes = results.filter(
196
+ (r) => r.status === 'fulfilled' && r.value.moved,
197
+ ).length
198
+ const failures = ids.length - successes
199
+ toast.add({
200
+ title: successes > 0
201
+ ? `Moved ${successes} agent${successes === 1 ? '' : 's'}`
202
+ : 'Nothing moved',
203
+ description: failures > 0
204
+ ? `${failures} skipped (Tier 0, collision, or missing)`
205
+ : undefined,
206
+ color: successes > 0 && failures === 0
207
+ ? 'success'
208
+ : failures > 0 && successes > 0 ? 'warning' : 'error',
209
+ })
210
+ clearSelection()
211
+ bulkMoving.value = false
212
+ await refreshAll()
213
+ }
164
214
 
165
215
  function toggleSelected(id: string) {
166
216
  if (selected.value.has(id)) selected.value.delete(id)
@@ -387,6 +437,16 @@ async function bulkDelete() {
387
437
  @click="clearSelection"
388
438
  />
389
439
  <div class="h-5 w-px bg-default" />
440
+ <UDropdownMenu :items="departmentMoveOptions">
441
+ <UButton
442
+ label="Move to..."
443
+ icon="i-lucide-folder-tree"
444
+ size="sm"
445
+ variant="soft"
446
+ :loading="bulkMoving"
447
+ trailing-icon="i-lucide-chevron-down"
448
+ />
449
+ </UDropdownMenu>
390
450
  <UButton
391
451
  label="Delete"
392
452
  icon="i-lucide-trash-2"
@@ -226,6 +226,72 @@ function csvToList(value: string): string[] {
226
226
  type SuggestField = 'mental_models' | 'frameworks' | 'expertise_domains' | 'communication_avoid' | 'key_quotes'
227
227
  const suggestingField = ref<SuggestField | null>(null)
228
228
 
229
+ // PR84c v3.9.0 — Auto-fill empty lists in one go.
230
+ const autofilling = ref(false)
231
+
232
+ async function autofillEmpties() {
233
+ if (!draft.value || !detail.value) return
234
+ type ListKey = 'mental_models' | 'expertise_domains' | 'frameworks' | 'key_quotes' | 'communication_avoid'
235
+ const targets: ListKey[] = []
236
+ if ((draft.value.mental_models ?? []).length === 0) targets.push('mental_models')
237
+ if ((draft.value.expertise_domains ?? []).length === 0) targets.push('expertise_domains')
238
+ if ((draft.value.frameworks ?? []).length === 0) targets.push('frameworks')
239
+ if ((draft.value.key_quotes ?? []).length === 0) targets.push('key_quotes')
240
+ if ((draft.value.communication.avoid ?? []).length === 0) targets.push('communication_avoid')
241
+ if (targets.length === 0) {
242
+ toast.add({ title: 'No empty lists', description: 'Every list already has at least one item.', color: 'info' })
243
+ return
244
+ }
245
+ autofilling.value = true
246
+ const results = await Promise.allSettled(
247
+ targets.map((field) =>
248
+ $fetch<{ suggestions: string[], provider_name: string, error?: string }>(
249
+ `${apiBase}/api/personas/suggest`,
250
+ {
251
+ method: 'POST',
252
+ body: {
253
+ field,
254
+ count: 5,
255
+ context: {
256
+ name: detail.value!.name,
257
+ title: detail.value!.title,
258
+ current: [],
259
+ },
260
+ },
261
+ },
262
+ ),
263
+ ),
264
+ )
265
+ let filledCount = 0
266
+ let providerName = ''
267
+ results.forEach((r, idx) => {
268
+ if (r.status !== 'fulfilled' || r.value.error) return
269
+ const items = r.value.suggestions ?? []
270
+ if (items.length === 0) return
271
+ const field = targets[idx]
272
+ if (!draft.value) return
273
+ if (field === 'communication_avoid') {
274
+ draft.value.communication.avoid = items
275
+ } else {
276
+ ;(draft.value as any)[field] = items
277
+ }
278
+ filledCount += 1
279
+ providerName = r.value.provider_name || providerName
280
+ })
281
+ autofilling.value = false
282
+ if (filledCount > 0) {
283
+ markDirty()
284
+ toast.add({
285
+ title: `Filled ${filledCount} list${filledCount === 1 ? '' : 's'}`,
286
+ description: `via ${providerName}`,
287
+ color: 'success',
288
+ icon: 'i-lucide-sparkles',
289
+ })
290
+ } else {
291
+ toast.add({ title: 'Nothing filled', description: 'LLM returned no items.', color: 'error' })
292
+ }
293
+ }
294
+
229
295
  // PR83c v3.5.0 — single-string suggester (tone for personas).
230
296
  const suggestingString = ref<'tone' | null>(null)
231
297
 
@@ -657,15 +723,26 @@ const vocabOptions = [
657
723
  }"
658
724
  >
659
725
  <template #header>
660
- <div class="flex items-center justify-between">
726
+ <div class="flex items-center justify-between gap-3">
661
727
  <h2 class="text-xl font-bold">Edit {{ draft.name || 'persona' }}</h2>
662
- <UButton
663
- icon="i-lucide-x"
664
- variant="ghost"
665
- size="sm"
666
- aria-label="Close"
667
- @click="tryCloseEdit"
668
- />
728
+ <div class="flex items-center gap-2">
729
+ <UButton
730
+ label="Auto-fill empties"
731
+ icon="i-lucide-sparkles"
732
+ color="primary"
733
+ variant="soft"
734
+ size="sm"
735
+ :loading="autofilling"
736
+ @click="autofillEmpties"
737
+ />
738
+ <UButton
739
+ icon="i-lucide-x"
740
+ variant="ghost"
741
+ size="sm"
742
+ aria-label="Close"
743
+ @click="tryCloseEdit"
744
+ />
745
+ </div>
669
746
  </div>
670
747
  </template>
671
748
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arkaos",
3
- "version": "3.7.0",
3
+ "version": "3.9.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.7.0"
3
+ version = "3.9.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"}
@@ -1178,6 +1178,56 @@ def persona_clone(persona_id: str, body: dict = {}):
1178
1178
  return {"agent_id": agent_id, "department": department, "file": f"departments/{department}/agents/{agent_id}.yaml"}
1179
1179
 
1180
1180
 
1181
+ @app.post("/api/agents/{agent_id}/move")
1182
+ def agent_move(agent_id: str, body: dict):
1183
+ """PR84b v3.8.0 — move an agent's YAML to another department.
1184
+
1185
+ Body: {"department": "<new-dept>"}
1186
+ Mutates the YAML's `department:` field AND moves the file across
1187
+ `departments/<src>/agents/` → `departments/<dst>/agents/`.
1188
+
1189
+ Refuses Tier 0 (C-Suite) like the delete endpoint. Refuses unknown
1190
+ target department. Refuses overwriting an existing file at the
1191
+ destination.
1192
+ """
1193
+ if not isinstance(body, dict):
1194
+ return {"error": "body must be an object"}
1195
+ target_dept = (body.get("department") or "").strip().lower()
1196
+ if not target_dept:
1197
+ return {"error": "department is required"}
1198
+ yaml_file = _resolve_agent_yaml(agent_id)
1199
+ if yaml_file is None:
1200
+ return {"error": "Agent not found"}
1201
+ if _agent_tier_from_yaml(yaml_file) == 0:
1202
+ return {"error": "Cannot move Tier 0 (C-Suite) agents from the dashboard"}
1203
+ dest_dir = ARKAOS_ROOT / "departments" / target_dept / "agents"
1204
+ if not dest_dir.exists():
1205
+ return {"error": f"department '{target_dept}' not found"}
1206
+ dest_file = dest_dir / yaml_file.name
1207
+ if dest_file.exists():
1208
+ return {"error": f"target file already exists: {dest_file.name}"}
1209
+ try:
1210
+ if yaml_file.resolve() == dest_file.resolve():
1211
+ return {"moved": False, "id": agent_id, "yaml_path": str(yaml_file)}
1212
+ except FileNotFoundError:
1213
+ pass
1214
+ try:
1215
+ import yaml as _yaml
1216
+ raw = _yaml.safe_load(yaml_file.read_text(encoding="utf-8")) or {}
1217
+ if isinstance(raw, dict):
1218
+ raw["department"] = target_dept
1219
+ tmp = yaml_file.with_suffix(yaml_file.suffix + ".tmp")
1220
+ tmp.write_text(
1221
+ _yaml.safe_dump(raw, sort_keys=False, allow_unicode=True, default_flow_style=False),
1222
+ encoding="utf-8",
1223
+ )
1224
+ tmp.replace(yaml_file)
1225
+ yaml_file.rename(dest_file)
1226
+ except (OSError, ImportError) as exc:
1227
+ return {"error": f"move failed: {exc}"}
1228
+ return {"moved": True, "id": agent_id, "yaml_path": str(dest_file)}
1229
+
1230
+
1181
1231
  @app.delete("/api/agents/{agent_id}")
1182
1232
  def agent_delete(agent_id: str):
1183
1233
  """PR83b v3.4.0 — delete an agent's YAML file.