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.
|
|
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
|
-
<
|
|
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
|
-
{
|
|
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-
|
|
143
|
-
<
|
|
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
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
class="
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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
package/pyproject.toml
CHANGED
|
Binary file
|
package/scripts/dashboard-api.py
CHANGED
|
@@ -2024,9 +2024,17 @@ TERMINAL_ALLOWLIST: list[dict] = [
|
|
|
2024
2024
|
},
|
|
2025
2025
|
{
|
|
2026
2026
|
"id": "git-log",
|
|
2027
|
-
"label": "git log
|
|
2028
|
-
"
|
|
2029
|
-
"
|
|
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
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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.
|