arkaos 3.54.0 → 3.56.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.54.0
1
+ 3.56.0
@@ -137,6 +137,42 @@ function goToPersona(id: string) {
137
137
  navigateTo(`/personas/${id}`)
138
138
  }
139
139
 
140
+ // PR96a v3.55.0 — keyboard nav, mirror of PR95d on /agents.
141
+ const cursorIndex = ref(-1)
142
+
143
+ function cursorDown() {
144
+ const total = paginatedPersonas.value.length
145
+ if (total === 0) return
146
+ cursorIndex.value = Math.min(total - 1, Math.max(0, cursorIndex.value + 1))
147
+ scrollCursorIntoView()
148
+ }
149
+ function cursorUp() {
150
+ const total = paginatedPersonas.value.length
151
+ if (total === 0) return
152
+ cursorIndex.value = Math.max(0, cursorIndex.value === -1 ? 0 : cursorIndex.value - 1)
153
+ scrollCursorIntoView()
154
+ }
155
+ function cursorOpen() {
156
+ if (cursorIndex.value < 0) return
157
+ const row = paginatedPersonas.value[cursorIndex.value]
158
+ if (row?.id) goToPersona(row.id)
159
+ }
160
+ function scrollCursorIntoView() {
161
+ if (typeof document === 'undefined') return
162
+ setTimeout(() => {
163
+ const el = document.querySelector('[data-cursor="true"]')
164
+ el?.scrollIntoView({ block: 'nearest', behavior: 'smooth' })
165
+ }, 0)
166
+ }
167
+
168
+ defineShortcuts({
169
+ j: () => cursorDown(),
170
+ k: () => cursorUp(),
171
+ arrowdown: () => cursorDown(),
172
+ arrowup: () => cursorUp(),
173
+ enter: () => cursorOpen(),
174
+ })
175
+
140
176
  // PR86a v3.15.0 — favorites.
141
177
  const favs = useFavorites()
142
178
  await favs.load()
@@ -593,7 +629,14 @@ async function undoTrashIds(ids: string[]) {
593
629
  />
594
630
  </template>
595
631
  <template #name-cell="{ row }">
596
- <span class="font-medium">{{ row.original.name }}</span>
632
+ <div :data-cursor="row.index === cursorIndex ? 'true' : undefined" class="flex items-center gap-1.5">
633
+ <UIcon
634
+ v-if="row.index === cursorIndex"
635
+ name="i-lucide-chevron-right"
636
+ class="size-3.5 text-primary shrink-0"
637
+ />
638
+ <span class="font-medium">{{ row.original.name }}</span>
639
+ </div>
597
640
  </template>
598
641
  <template #title-cell="{ row }">
599
642
  <span class="text-sm text-muted truncate">{{ row.original.title || '—' }}</span>
@@ -9,10 +9,18 @@
9
9
  // instead ships a controlled command runner with allowlist + capped
10
10
  // output. xterm.js-style PTY can be a later upgrade if needed.
11
11
 
12
+ interface CommandArg {
13
+ name: string
14
+ label: string
15
+ choices: string[]
16
+ default: string
17
+ }
18
+
12
19
  interface CommandEntry {
13
20
  id: string
14
21
  label: string
15
22
  description: string
23
+ args?: CommandArg[]
16
24
  }
17
25
 
18
26
  interface ExecResult {
@@ -41,12 +49,33 @@ const commands = computed<CommandEntry[]>(() => cmdData.value?.commands ?? [])
41
49
  const running = ref<string | null>(null)
42
50
  const history = ref<HistoryEntry[]>([])
43
51
 
52
+ // PR96b v3.56.0 — per-command arg state. Keyed by command id, holds
53
+ // the operator's current selection for each arg.
54
+ const argState = reactive<Record<string, Record<string, string>>>({})
55
+
56
+ function ensureArgState(cmd: CommandEntry) {
57
+ if (!cmd.args || cmd.args.length === 0) return
58
+ if (!argState[cmd.id]) argState[cmd.id] = {}
59
+ for (const arg of cmd.args) {
60
+ if (argState[cmd.id][arg.name] === undefined) {
61
+ argState[cmd.id][arg.name] = arg.default
62
+ }
63
+ }
64
+ }
65
+
44
66
  async function run(cmd: CommandEntry) {
45
67
  running.value = cmd.id
68
+ ensureArgState(cmd)
46
69
  try {
47
70
  const res = await $fetch<ExecResult & { error?: string }>(
48
71
  `${apiBase}/api/terminal/exec`,
49
- { method: 'POST', body: { command_id: cmd.id } },
72
+ {
73
+ method: 'POST',
74
+ body: {
75
+ command_id: cmd.id,
76
+ args: cmd.args ? argState[cmd.id] : undefined,
77
+ },
78
+ },
50
79
  )
51
80
  if (res.error) throw new Error(res.error)
52
81
  history.value = [
@@ -139,22 +168,47 @@ function relative(iso: string): string {
139
168
  </p>
140
169
  </div>
141
170
  </template>
142
- <div class="grid grid-cols-1 md:grid-cols-2 gap-2">
143
- <UButton
171
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-3">
172
+ <div
144
173
  v-for="cmd in commands"
145
174
  :key="cmd.id"
146
- :label="cmd.label"
147
- :description="cmd.description"
148
- icon="i-lucide-terminal"
149
- variant="soft"
150
- color="primary"
151
- size="sm"
152
- block
153
- class="justify-start"
154
- :loading="running === cmd.id"
155
- :disabled="running !== null && running !== cmd.id"
156
- @click="run(cmd)"
157
- />
175
+ class="rounded-lg border border-default p-3 space-y-2"
176
+ >
177
+ <div>
178
+ <p class="text-sm font-semibold">{{ cmd.label }}</p>
179
+ <p class="text-xs text-muted">{{ cmd.description }}</p>
180
+ </div>
181
+ <!-- PR96b v3.56.0 — arg pickers for parameterised commands -->
182
+ <div v-if="cmd.args && cmd.args.length > 0" class="grid grid-cols-2 gap-2">
183
+ <UFormField
184
+ v-for="arg in cmd.args"
185
+ :key="arg.name"
186
+ :label="arg.label"
187
+ size="xs"
188
+ >
189
+ <USelect
190
+ :model-value="(argState[cmd.id] || {})[arg.name] || arg.default"
191
+ :items="arg.choices.map((c) => ({ label: c, value: c }))"
192
+ size="xs"
193
+ class="w-full"
194
+ @update:model-value="(v) => {
195
+ ensureArgState(cmd)
196
+ argState[cmd.id][arg.name] = String(v)
197
+ }"
198
+ />
199
+ </UFormField>
200
+ </div>
201
+ <UButton
202
+ label="Run"
203
+ icon="i-lucide-play"
204
+ color="primary"
205
+ size="sm"
206
+ block
207
+ :loading="running === cmd.id"
208
+ :disabled="running !== null && running !== cmd.id"
209
+ @click="run(cmd)"
210
+ />
211
+ </div>
158
212
  </div>
159
213
  </UCard>
160
214
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arkaos",
3
- "version": "3.54.0",
3
+ "version": "3.56.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.54.0"
3
+ version = "3.56.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"}
@@ -2024,9 +2024,17 @@ TERMINAL_ALLOWLIST: list[dict] = [
2024
2024
  },
2025
2025
  {
2026
2026
  "id": "git-log",
2027
- "label": "git log (10)",
2028
- "cmd": ["git", "log", "-10", "--oneline"],
2029
- "description": "Most recent 10 commits.",
2027
+ "label": "git log",
2028
+ "cmd_template": ["git", "log", "-{count}", "--oneline"],
2029
+ "args": [
2030
+ {
2031
+ "name": "count",
2032
+ "label": "Commits to show",
2033
+ "choices": ["5", "10", "20", "50"],
2034
+ "default": "10",
2035
+ },
2036
+ ],
2037
+ "description": "Most recent N commits (operator picks N).",
2030
2038
  },
2031
2039
  {
2032
2040
  "id": "npm-version",
@@ -2046,6 +2054,20 @@ TERMINAL_ALLOWLIST: list[dict] = [
2046
2054
  "cmd": ["ls", "-la"],
2047
2055
  "description": "List files in the project root.",
2048
2056
  },
2057
+ {
2058
+ "id": "arka-costs",
2059
+ "label": "ArkaOS costs",
2060
+ "cmd_template": ["python3", "-m", "core.runtime.llm_cost_telemetry_cli", "{period}"],
2061
+ "args": [
2062
+ {
2063
+ "name": "period",
2064
+ "label": "Period",
2065
+ "choices": ["today", "week", "month", "all"],
2066
+ "default": "today",
2067
+ },
2068
+ ],
2069
+ "description": "LLM cost summary for the selected period.",
2070
+ },
2049
2071
  ]
2050
2072
 
2051
2073
  _TERMINAL_TIMEOUT_S = 15
@@ -2054,14 +2076,41 @@ _TERMINAL_MAX_OUTPUT = 20_000 # chars — both stdout and stderr capped
2054
2076
 
2055
2077
  @app.get("/api/terminal/commands")
2056
2078
  def terminal_commands():
2057
- """List allowlisted commands the dashboard terminal may run."""
2058
- return {
2059
- "commands": [
2060
- {"id": c["id"], "label": c["label"], "description": c["description"]}
2061
- for c in TERMINAL_ALLOWLIST
2062
- ],
2063
- "total": len(TERMINAL_ALLOWLIST),
2064
- }
2079
+ """List allowlisted commands the dashboard terminal may run.
2080
+
2081
+ PR96b v3.56.0 — includes the `args` schema (label / choices /
2082
+ default) when an entry has parameters. The raw cmd / cmd_template
2083
+ is NEVER returned — defence in depth.
2084
+ """
2085
+ out: list[dict] = []
2086
+ for c in TERMINAL_ALLOWLIST:
2087
+ out.append({
2088
+ "id": c["id"],
2089
+ "label": c["label"],
2090
+ "description": c["description"],
2091
+ "args": _safe_args_schema(c.get("args")),
2092
+ })
2093
+ return {"commands": out, "total": len(out)}
2094
+
2095
+
2096
+ def _safe_args_schema(args) -> list[dict]:
2097
+ """Sanitised arg schema for the public API — never leaks cmd/template."""
2098
+ if not isinstance(args, list):
2099
+ return []
2100
+ out: list[dict] = []
2101
+ for a in args:
2102
+ if not isinstance(a, dict):
2103
+ continue
2104
+ choices = a.get("choices")
2105
+ if not isinstance(choices, list) or not choices:
2106
+ continue
2107
+ out.append({
2108
+ "name": str(a.get("name") or ""),
2109
+ "label": str(a.get("label") or a.get("name") or ""),
2110
+ "choices": [str(x) for x in choices],
2111
+ "default": str(a.get("default") or choices[0]),
2112
+ })
2113
+ return out
2065
2114
 
2066
2115
 
2067
2116
  @app.post("/api/terminal/exec")
@@ -2080,11 +2129,20 @@ def terminal_exec(body: dict):
2080
2129
  entry = next((c for c in TERMINAL_ALLOWLIST if c["id"] == cid), None)
2081
2130
  if entry is None:
2082
2131
  return {"error": f"command '{cid}' is not on the allowlist"}
2132
+
2133
+ # PR96b v3.56.0 — resolve template + args, or fall back to fixed cmd.
2134
+ if "cmd_template" in entry:
2135
+ argv, err = _resolve_cmd_template(entry, body.get("args") or {})
2136
+ if err:
2137
+ return {"error": err}
2138
+ else:
2139
+ argv = entry["cmd"]
2140
+
2083
2141
  import time
2084
2142
  started = time.monotonic()
2085
2143
  try:
2086
2144
  result = subprocess.run(
2087
- entry["cmd"],
2145
+ argv,
2088
2146
  cwd=str(ARKAOS_ROOT),
2089
2147
  capture_output=True,
2090
2148
  text=True,
@@ -2097,7 +2155,7 @@ def terminal_exec(body: dict):
2097
2155
  "stderr": f"command timed out after {_TERMINAL_TIMEOUT_S}s",
2098
2156
  "exit_code": -1,
2099
2157
  "duration_ms": int(_TERMINAL_TIMEOUT_S * 1000),
2100
- "command": " ".join(entry["cmd"]),
2158
+ "command": " ".join(argv),
2101
2159
  }
2102
2160
  except OSError as exc:
2103
2161
  return {
@@ -2105,7 +2163,7 @@ def terminal_exec(body: dict):
2105
2163
  "stderr": f"command failed to launch: {exc}",
2106
2164
  "exit_code": -1,
2107
2165
  "duration_ms": int((time.monotonic() - started) * 1000),
2108
- "command": " ".join(entry["cmd"]),
2166
+ "command": " ".join(argv),
2109
2167
  }
2110
2168
  duration_ms = int((time.monotonic() - started) * 1000)
2111
2169
  return {
@@ -2113,10 +2171,49 @@ def terminal_exec(body: dict):
2113
2171
  "stderr": (result.stderr or "")[:_TERMINAL_MAX_OUTPUT],
2114
2172
  "exit_code": result.returncode,
2115
2173
  "duration_ms": duration_ms,
2116
- "command": " ".join(entry["cmd"]),
2174
+ "command": " ".join(argv),
2117
2175
  }
2118
2176
 
2119
2177
 
2178
+ def _resolve_cmd_template(entry: dict, supplied: dict) -> tuple[list[str], "str | None"]:
2179
+ """Substitute `{name}` placeholders in cmd_template with validated args.
2180
+
2181
+ Returns (argv, None) on success, ([], error_msg) on validation failure.
2182
+ Anything supplied that isn't in the schema's choices is rejected.
2183
+ Unknown arg names are also rejected — no silent passthrough.
2184
+ """
2185
+ schema = entry.get("args") or []
2186
+ if not isinstance(schema, list):
2187
+ return [], "command has invalid args schema"
2188
+ by_name: dict[str, dict] = {a["name"]: a for a in schema if isinstance(a, dict)}
2189
+ chosen: dict[str, str] = {}
2190
+ for arg_def in schema:
2191
+ name = arg_def["name"]
2192
+ s = supplied.get(name)
2193
+ if s is None:
2194
+ chosen[name] = str(arg_def.get("default") or arg_def["choices"][0])
2195
+ continue
2196
+ s_str = str(s)
2197
+ if s_str not in arg_def["choices"]:
2198
+ return [], (
2199
+ f"arg '{name}'='{s_str}' is not in the allowed choices "
2200
+ f"({', '.join(arg_def['choices'])})"
2201
+ )
2202
+ chosen[name] = s_str
2203
+ for k in supplied:
2204
+ if k not in by_name:
2205
+ return [], f"unknown arg '{k}'"
2206
+ argv: list[str] = []
2207
+ for tok in entry["cmd_template"]:
2208
+ if not isinstance(tok, str):
2209
+ continue
2210
+ out_tok = tok
2211
+ for name, val in chosen.items():
2212
+ out_tok = out_tok.replace(f"{{{name}}}", val)
2213
+ argv.append(out_tok)
2214
+ return argv, None
2215
+
2216
+
2120
2217
  @app.put("/api/workflows/{workflow_id}/yaml")
2121
2218
  def workflow_update_yaml(workflow_id: str, body: dict):
2122
2219
  """PR94d v3.50.0 — overwrite a workflow's YAML file in place.