arkaos 3.71.1 → 3.72.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 +1 -1
- package/core/terminal/__pycache__/__init__.cpython-313.pyc +0 -0
- package/core/terminal/__pycache__/connections.cpython-313.pyc +0 -0
- package/dashboard/app/layouts/default.vue +7 -0
- package/dashboard/app/pages/cognition.vue +311 -0
- package/dashboard/app/pages/settings.vue +215 -51
- package/installer/autostart.js +178 -0
- package/installer/cli.js +7 -0
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/scripts/__pycache__/dashboard-api.cpython-313.pyc +0 -0
- package/scripts/dashboard-api.py +155 -1
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
3.
|
|
1
|
+
3.72.0
|
|
Binary file
|
|
Binary file
|
|
@@ -80,6 +80,13 @@ const links = [[{
|
|
|
80
80
|
onSelect: () => {
|
|
81
81
|
open.value = false
|
|
82
82
|
}
|
|
83
|
+
}, {
|
|
84
|
+
label: 'Dreaming',
|
|
85
|
+
icon: 'i-lucide-sparkles',
|
|
86
|
+
to: '/cognition',
|
|
87
|
+
onSelect: () => {
|
|
88
|
+
open.value = false
|
|
89
|
+
}
|
|
83
90
|
}], [{
|
|
84
91
|
label: 'Health',
|
|
85
92
|
icon: 'i-lucide-heart-pulse',
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// v3.72.0 — Cognition page: monitor what Dreaming has been learning.
|
|
3
|
+
// Read-only view over the insights the Cognitive Layer already writes to
|
|
4
|
+
// <vault>/Projects/ArkaOS/Dreams. Backend: /api/cognition/{insights,status}.
|
|
5
|
+
|
|
6
|
+
import { marked } from 'marked'
|
|
7
|
+
|
|
8
|
+
definePageMeta({ layout: 'default' })
|
|
9
|
+
|
|
10
|
+
interface Insight {
|
|
11
|
+
date: string
|
|
12
|
+
title: string
|
|
13
|
+
confidence: string
|
|
14
|
+
sources: string[]
|
|
15
|
+
tags: string[]
|
|
16
|
+
body: string
|
|
17
|
+
}
|
|
18
|
+
interface Status {
|
|
19
|
+
today: number
|
|
20
|
+
week: number
|
|
21
|
+
total: number
|
|
22
|
+
by_confidence: { high: number, medium: number, low: number }
|
|
23
|
+
vault_configured: boolean
|
|
24
|
+
last_date: string | null
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const { apiBase } = useApi()
|
|
28
|
+
|
|
29
|
+
const days = ref(7)
|
|
30
|
+
const confidenceFilter = ref<'all' | 'high' | 'medium' | 'low'>('all')
|
|
31
|
+
const tagFilter = ref('')
|
|
32
|
+
const insights = ref<Insight[]>([])
|
|
33
|
+
const status = ref<Status | null>(null)
|
|
34
|
+
const available = ref(true)
|
|
35
|
+
const loading = ref(true)
|
|
36
|
+
const expanded = ref<Set<string>>(new Set())
|
|
37
|
+
|
|
38
|
+
const windowOptions = [
|
|
39
|
+
{ label: 'Today', value: 1 },
|
|
40
|
+
{ label: 'Last 7 days', value: 7 },
|
|
41
|
+
{ label: 'Last 30 days', value: 30 }
|
|
42
|
+
]
|
|
43
|
+
const confidenceOptions = [
|
|
44
|
+
{ label: 'All confidence', value: 'all' },
|
|
45
|
+
{ label: 'High', value: 'high' },
|
|
46
|
+
{ label: 'Medium', value: 'medium' },
|
|
47
|
+
{ label: 'Low', value: 'low' }
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
async function refresh() {
|
|
51
|
+
loading.value = true
|
|
52
|
+
try {
|
|
53
|
+
const [s, i] = await Promise.all([
|
|
54
|
+
$fetch<Status>(`${apiBase}/api/cognition/status`),
|
|
55
|
+
$fetch<{ insights: Insight[], available: boolean }>(
|
|
56
|
+
`${apiBase}/api/cognition/insights?days=${days.value}`
|
|
57
|
+
)
|
|
58
|
+
])
|
|
59
|
+
status.value = s
|
|
60
|
+
insights.value = i.insights
|
|
61
|
+
available.value = i.available
|
|
62
|
+
} catch {
|
|
63
|
+
available.value = false
|
|
64
|
+
} finally {
|
|
65
|
+
loading.value = false
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
onMounted(refresh)
|
|
70
|
+
// Reset the tag filter when the window changes — a tag may not exist in the
|
|
71
|
+
// new window, which would otherwise strand the user on an empty result.
|
|
72
|
+
watch(days, () => {
|
|
73
|
+
tagFilter.value = ''
|
|
74
|
+
refresh()
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
const allTags = computed(() => {
|
|
78
|
+
const set = new Set<string>()
|
|
79
|
+
for (const i of insights.value) for (const t of i.tags) set.add(t)
|
|
80
|
+
return [...set].sort()
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
const filtered = computed(() => insights.value.filter((i) => {
|
|
84
|
+
if (confidenceFilter.value !== 'all' && i.confidence !== confidenceFilter.value) return false
|
|
85
|
+
if (tagFilter.value && !i.tags.includes(tagFilter.value)) return false
|
|
86
|
+
return true
|
|
87
|
+
}))
|
|
88
|
+
|
|
89
|
+
function confidenceColor(c: string): 'success' | 'warning' | 'neutral' {
|
|
90
|
+
if (c === 'high') return 'success'
|
|
91
|
+
if (c === 'low') return 'neutral'
|
|
92
|
+
return 'warning'
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function toggle(key: string) {
|
|
96
|
+
const next = new Set(expanded.value)
|
|
97
|
+
if (next.has(key)) next.delete(key)
|
|
98
|
+
else next.add(key)
|
|
99
|
+
expanded.value = next
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function renderBody(body: string): string {
|
|
103
|
+
if (!body) return ''
|
|
104
|
+
try {
|
|
105
|
+
return marked.parse(body, { async: false }) as string
|
|
106
|
+
} catch {
|
|
107
|
+
return body
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const isActive = computed(() => {
|
|
112
|
+
if (!status.value?.last_date) return false
|
|
113
|
+
const last = new Date(status.value.last_date + 'T00:00:00Z').getTime()
|
|
114
|
+
return Date.now() - last < 3 * 86_400_000
|
|
115
|
+
})
|
|
116
|
+
</script>
|
|
117
|
+
|
|
118
|
+
<template>
|
|
119
|
+
<UDashboardPanel id="cognition">
|
|
120
|
+
<template #header>
|
|
121
|
+
<UDashboardNavbar title="Dreaming">
|
|
122
|
+
<template #leading>
|
|
123
|
+
<UDashboardSidebarCollapse />
|
|
124
|
+
</template>
|
|
125
|
+
<template #right>
|
|
126
|
+
<UBadge
|
|
127
|
+
:color="isActive ? 'success' : 'neutral'"
|
|
128
|
+
variant="soft"
|
|
129
|
+
size="sm"
|
|
130
|
+
>
|
|
131
|
+
<UIcon
|
|
132
|
+
:name="isActive ? 'i-lucide-activity' : 'i-lucide-moon'"
|
|
133
|
+
class="size-3 mr-1"
|
|
134
|
+
/>
|
|
135
|
+
{{ isActive ? 'Active' : 'Idle' }}
|
|
136
|
+
<span v-if="status?.last_date" class="ml-1 opacity-70">· {{ status.last_date }}</span>
|
|
137
|
+
</UBadge>
|
|
138
|
+
</template>
|
|
139
|
+
</UDashboardNavbar>
|
|
140
|
+
</template>
|
|
141
|
+
|
|
142
|
+
<template #body>
|
|
143
|
+
<div class="flex flex-col gap-4 p-4">
|
|
144
|
+
<p class="text-sm text-muted -mt-1">
|
|
145
|
+
What the Cognitive Layer has been learning — insights surfaced by
|
|
146
|
+
Dreaming from your vault and sessions.
|
|
147
|
+
</p>
|
|
148
|
+
|
|
149
|
+
<!-- Stats -->
|
|
150
|
+
<div v-if="status?.vault_configured" class="grid grid-cols-2 sm:grid-cols-4 gap-3">
|
|
151
|
+
<div class="rounded-lg border border-default bg-elevated/10 p-3">
|
|
152
|
+
<div class="text-2xl font-semibold tabular-nums">
|
|
153
|
+
{{ status.today }}
|
|
154
|
+
</div>
|
|
155
|
+
<div class="text-xs text-muted">
|
|
156
|
+
today
|
|
157
|
+
</div>
|
|
158
|
+
</div>
|
|
159
|
+
<div class="rounded-lg border border-default bg-elevated/10 p-3">
|
|
160
|
+
<div class="text-2xl font-semibold tabular-nums">
|
|
161
|
+
{{ status.week }}
|
|
162
|
+
</div>
|
|
163
|
+
<div class="text-xs text-muted">
|
|
164
|
+
last 7 days
|
|
165
|
+
</div>
|
|
166
|
+
</div>
|
|
167
|
+
<div class="rounded-lg border border-default bg-elevated/10 p-3">
|
|
168
|
+
<div class="text-2xl font-semibold tabular-nums">
|
|
169
|
+
{{ status.total }}
|
|
170
|
+
</div>
|
|
171
|
+
<div class="text-xs text-muted">
|
|
172
|
+
total insights
|
|
173
|
+
</div>
|
|
174
|
+
</div>
|
|
175
|
+
<div class="rounded-lg border border-default bg-elevated/10 p-3 flex flex-col justify-center gap-1">
|
|
176
|
+
<div class="flex items-center gap-2 text-xs">
|
|
177
|
+
<UBadge color="success" variant="soft" size="xs">
|
|
178
|
+
high {{ status.by_confidence.high }}
|
|
179
|
+
</UBadge>
|
|
180
|
+
<UBadge color="warning" variant="soft" size="xs">
|
|
181
|
+
med {{ status.by_confidence.medium }}
|
|
182
|
+
</UBadge>
|
|
183
|
+
<UBadge color="neutral" variant="soft" size="xs">
|
|
184
|
+
low {{ status.by_confidence.low }}
|
|
185
|
+
</UBadge>
|
|
186
|
+
</div>
|
|
187
|
+
</div>
|
|
188
|
+
</div>
|
|
189
|
+
|
|
190
|
+
<!-- Filters -->
|
|
191
|
+
<div class="flex flex-wrap items-center gap-2">
|
|
192
|
+
<USelect
|
|
193
|
+
v-model="days"
|
|
194
|
+
:items="windowOptions"
|
|
195
|
+
size="sm"
|
|
196
|
+
class="w-40"
|
|
197
|
+
/>
|
|
198
|
+
<USelect
|
|
199
|
+
v-model="confidenceFilter"
|
|
200
|
+
:items="confidenceOptions"
|
|
201
|
+
size="sm"
|
|
202
|
+
class="w-44"
|
|
203
|
+
/>
|
|
204
|
+
<USelect
|
|
205
|
+
v-if="allTags.length"
|
|
206
|
+
v-model="tagFilter"
|
|
207
|
+
:items="[{ label: 'All tags', value: '' }, ...allTags.map(t => ({ label: '#' + t, value: t }))]"
|
|
208
|
+
size="sm"
|
|
209
|
+
class="w-40"
|
|
210
|
+
/>
|
|
211
|
+
<UButton
|
|
212
|
+
size="sm"
|
|
213
|
+
variant="ghost"
|
|
214
|
+
icon="i-lucide-refresh-cw"
|
|
215
|
+
:loading="loading"
|
|
216
|
+
@click="refresh"
|
|
217
|
+
>
|
|
218
|
+
Refresh
|
|
219
|
+
</UButton>
|
|
220
|
+
</div>
|
|
221
|
+
|
|
222
|
+
<!-- Feed -->
|
|
223
|
+
<div v-if="loading" class="text-sm text-muted py-8 text-center">
|
|
224
|
+
<UIcon name="i-lucide-loader" class="animate-spin size-5 mx-auto mb-2" />
|
|
225
|
+
Loading insights…
|
|
226
|
+
</div>
|
|
227
|
+
<div
|
|
228
|
+
v-else-if="!available || !status?.vault_configured"
|
|
229
|
+
class="rounded-lg border border-dashed border-default p-10 text-center text-muted"
|
|
230
|
+
>
|
|
231
|
+
<UIcon name="i-lucide-moon-star" class="size-8 mx-auto mb-3 opacity-40" />
|
|
232
|
+
<p class="text-sm">
|
|
233
|
+
No vault connected, or Dreaming hasn't run yet.
|
|
234
|
+
</p>
|
|
235
|
+
<p class="text-xs mt-1 opacity-70">
|
|
236
|
+
Configure a vault and let the Cognitive Layer dream — insights show up here.
|
|
237
|
+
</p>
|
|
238
|
+
</div>
|
|
239
|
+
<div
|
|
240
|
+
v-else-if="filtered.length === 0"
|
|
241
|
+
class="rounded-lg border border-dashed border-default p-10 text-center text-muted"
|
|
242
|
+
>
|
|
243
|
+
<UIcon name="i-lucide-search-x" class="size-7 mx-auto mb-3 opacity-40" />
|
|
244
|
+
<p class="text-sm">
|
|
245
|
+
No insights match these filters.
|
|
246
|
+
</p>
|
|
247
|
+
</div>
|
|
248
|
+
<ul v-else class="flex flex-col gap-3">
|
|
249
|
+
<li
|
|
250
|
+
v-for="(ins, idx) in filtered"
|
|
251
|
+
:key="`${ins.date}-${idx}`"
|
|
252
|
+
class="rounded-lg border border-default bg-elevated/10 overflow-hidden"
|
|
253
|
+
>
|
|
254
|
+
<button
|
|
255
|
+
class="w-full flex items-start gap-3 p-3 text-left hover:bg-elevated/20 transition-colors"
|
|
256
|
+
@click="toggle(`${ins.date}-${idx}`)"
|
|
257
|
+
>
|
|
258
|
+
<UBadge
|
|
259
|
+
:color="confidenceColor(ins.confidence)"
|
|
260
|
+
variant="soft"
|
|
261
|
+
size="xs"
|
|
262
|
+
class="mt-0.5 shrink-0 uppercase"
|
|
263
|
+
>
|
|
264
|
+
{{ ins.confidence }}
|
|
265
|
+
</UBadge>
|
|
266
|
+
<div class="flex-1 min-w-0">
|
|
267
|
+
<div class="font-medium truncate">
|
|
268
|
+
{{ ins.title }}
|
|
269
|
+
</div>
|
|
270
|
+
<div class="flex items-center gap-2 mt-1 flex-wrap">
|
|
271
|
+
<span class="text-xs text-muted tabular-nums">{{ ins.date }}</span>
|
|
272
|
+
<UBadge
|
|
273
|
+
v-for="t in ins.tags"
|
|
274
|
+
:key="t"
|
|
275
|
+
variant="subtle"
|
|
276
|
+
size="xs"
|
|
277
|
+
>
|
|
278
|
+
#{{ t }}
|
|
279
|
+
</UBadge>
|
|
280
|
+
</div>
|
|
281
|
+
</div>
|
|
282
|
+
<UIcon
|
|
283
|
+
:name="expanded.has(`${ins.date}-${idx}`) ? 'i-lucide-chevron-up' : 'i-lucide-chevron-down'"
|
|
284
|
+
class="size-4 shrink-0 text-muted mt-0.5"
|
|
285
|
+
/>
|
|
286
|
+
</button>
|
|
287
|
+
<div v-if="expanded.has(`${ins.date}-${idx}`)" class="px-3 pb-3 border-t border-default/60 pt-3">
|
|
288
|
+
<!-- Trusted content: the operator's own Dreaming output from
|
|
289
|
+
their local vault, served over the localhost-only API —
|
|
290
|
+
same trust boundary as the persona bio renderer. -->
|
|
291
|
+
<!-- eslint-disable-next-line vue/no-v-html -->
|
|
292
|
+
<div class="prose prose-sm dark:prose-invert max-w-none text-sm" v-html="renderBody(ins.body)" />
|
|
293
|
+
<div v-if="ins.sources.length" class="flex items-center gap-1.5 flex-wrap mt-3 pt-2 border-t border-default/40">
|
|
294
|
+
<span class="text-[11px] text-muted">sources:</span>
|
|
295
|
+
<UBadge
|
|
296
|
+
v-for="s in ins.sources"
|
|
297
|
+
:key="s"
|
|
298
|
+
variant="subtle"
|
|
299
|
+
size="xs"
|
|
300
|
+
color="info"
|
|
301
|
+
>
|
|
302
|
+
{{ s }}
|
|
303
|
+
</UBadge>
|
|
304
|
+
</div>
|
|
305
|
+
</div>
|
|
306
|
+
</li>
|
|
307
|
+
</ul>
|
|
308
|
+
</div>
|
|
309
|
+
</template>
|
|
310
|
+
</UDashboardPanel>
|
|
311
|
+
</template>
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
+
// PR92d v3.42.0 — primary color picker.
|
|
3
|
+
import { THEME_COLOR_OPTIONS, useThemeColor } from '~/composables/useThemeColor'
|
|
4
|
+
|
|
2
5
|
interface ProfileResponse {
|
|
3
6
|
version: string
|
|
4
7
|
name: string
|
|
@@ -22,7 +25,7 @@ const {
|
|
|
22
25
|
data: profile,
|
|
23
26
|
status: profileStatus,
|
|
24
27
|
error: profileError,
|
|
25
|
-
refresh: refreshProfile
|
|
28
|
+
refresh: refreshProfile
|
|
26
29
|
} = await fetchApi<ProfileResponse>('/api/profile')
|
|
27
30
|
|
|
28
31
|
const profileDraft = ref({
|
|
@@ -32,7 +35,7 @@ const profileDraft = ref({
|
|
|
32
35
|
market: profile.value?.market ?? '',
|
|
33
36
|
language: profile.value?.language ?? 'en',
|
|
34
37
|
vaultPath: profile.value?.vaultPath ?? '',
|
|
35
|
-
projectsDir: profile.value?.projectsDir ?? ''
|
|
38
|
+
projectsDir: profile.value?.projectsDir ?? ''
|
|
36
39
|
})
|
|
37
40
|
|
|
38
41
|
watch(profile, (p) => {
|
|
@@ -44,7 +47,7 @@ watch(profile, (p) => {
|
|
|
44
47
|
market: p.market,
|
|
45
48
|
language: p.language,
|
|
46
49
|
vaultPath: p.vaultPath,
|
|
47
|
-
projectsDir: p.projectsDir
|
|
50
|
+
projectsDir: p.projectsDir
|
|
48
51
|
}
|
|
49
52
|
}, { immediate: true })
|
|
50
53
|
|
|
@@ -68,7 +71,7 @@ async function testVault() {
|
|
|
68
71
|
if (profileDraft.value.vaultPath !== profile.value?.vaultPath) {
|
|
69
72
|
await $fetch(`${apiBase}/api/profile`, {
|
|
70
73
|
method: 'POST',
|
|
71
|
-
body: { vaultPath: profileDraft.value.vaultPath }
|
|
74
|
+
body: { vaultPath: profileDraft.value.vaultPath }
|
|
72
75
|
})
|
|
73
76
|
}
|
|
74
77
|
vaultStatus.value = await $fetch<VaultStatus>(`${apiBase}/api/settings/vault`)
|
|
@@ -76,13 +79,13 @@ async function testVault() {
|
|
|
76
79
|
title: vaultStatus.value.exists ? 'Vault reachable' : 'Vault not found',
|
|
77
80
|
description: vaultStatus.value.vault_path || 'Set a path first',
|
|
78
81
|
color: vaultStatus.value.exists ? 'success' : 'warning',
|
|
79
|
-
icon: vaultStatus.value.exists ? 'i-lucide-check-circle' : 'i-lucide-alert-circle'
|
|
82
|
+
icon: vaultStatus.value.exists ? 'i-lucide-check-circle' : 'i-lucide-alert-circle'
|
|
80
83
|
})
|
|
81
84
|
} catch (err) {
|
|
82
85
|
toast.add({
|
|
83
86
|
title: 'Test failed',
|
|
84
87
|
description: err instanceof Error ? err.message : 'unknown error',
|
|
85
|
-
color: 'error'
|
|
88
|
+
color: 'error'
|
|
86
89
|
})
|
|
87
90
|
} finally {
|
|
88
91
|
testingVault.value = false
|
|
@@ -94,19 +97,19 @@ async function saveProfile() {
|
|
|
94
97
|
try {
|
|
95
98
|
await $fetch<ProfileResponse>(`${apiBase}/api/profile`, {
|
|
96
99
|
method: 'POST',
|
|
97
|
-
body: profileDraft.value
|
|
100
|
+
body: profileDraft.value
|
|
98
101
|
})
|
|
99
102
|
await refreshProfile()
|
|
100
103
|
toast.add({
|
|
101
104
|
title: 'Profile saved',
|
|
102
105
|
description: 'Settings written to ~/.arkaos/profile.json',
|
|
103
|
-
color: 'success'
|
|
106
|
+
color: 'success'
|
|
104
107
|
})
|
|
105
108
|
} catch (err) {
|
|
106
109
|
toast.add({
|
|
107
110
|
title: 'Save failed',
|
|
108
111
|
description: err instanceof Error ? err.message : 'unknown error',
|
|
109
|
-
color: 'error'
|
|
112
|
+
color: 'error'
|
|
110
113
|
})
|
|
111
114
|
} finally {
|
|
112
115
|
savingProfile.value = false
|
|
@@ -115,7 +118,7 @@ async function saveProfile() {
|
|
|
115
118
|
|
|
116
119
|
const languageOptions = [
|
|
117
120
|
{ label: 'English', value: 'en' },
|
|
118
|
-
{ label: 'Português', value: 'pt' }
|
|
121
|
+
{ label: 'Português', value: 'pt' }
|
|
119
122
|
]
|
|
120
123
|
|
|
121
124
|
const roleOptions = [
|
|
@@ -125,16 +128,23 @@ const roleOptions = [
|
|
|
125
128
|
{ label: 'Engineer', value: 'engineer' },
|
|
126
129
|
{ label: 'Designer', value: 'designer' },
|
|
127
130
|
{ label: 'Operator', value: 'operator' },
|
|
128
|
-
{ label: 'Consultant', value: 'consultant' }
|
|
131
|
+
{ label: 'Consultant', value: 'consultant' }
|
|
129
132
|
]
|
|
130
133
|
|
|
131
134
|
// ─── API Keys (preserved from earlier) ──────────────────────────────────
|
|
132
135
|
|
|
136
|
+
interface KeyRow {
|
|
137
|
+
key: string
|
|
138
|
+
provider: string
|
|
139
|
+
configured: boolean
|
|
140
|
+
used_for?: string
|
|
141
|
+
}
|
|
142
|
+
|
|
133
143
|
const {
|
|
134
144
|
data: keysData,
|
|
135
145
|
status: keysStatus,
|
|
136
|
-
refresh: refreshKeys
|
|
137
|
-
} = fetchApi<
|
|
146
|
+
refresh: refreshKeys
|
|
147
|
+
} = fetchApi<{ keys: KeyRow[] }>('/api/keys')
|
|
138
148
|
|
|
139
149
|
const keys = computed(() => keysData.value?.keys ?? [])
|
|
140
150
|
|
|
@@ -154,13 +164,13 @@ async function saveKey() {
|
|
|
154
164
|
try {
|
|
155
165
|
await $fetch(`${apiBase}/api/keys`, {
|
|
156
166
|
method: 'POST',
|
|
157
|
-
body: { key: keyName, value: newValue.value }
|
|
167
|
+
body: { key: keyName, value: newValue.value }
|
|
158
168
|
})
|
|
159
169
|
newKey.value = ''
|
|
160
170
|
newValue.value = ''
|
|
161
171
|
customKeyName.value = ''
|
|
162
172
|
await refreshKeys()
|
|
163
|
-
} catch {}
|
|
173
|
+
} catch { /* best-effort; surfaced via list refresh */ }
|
|
164
174
|
saving.value = false
|
|
165
175
|
}
|
|
166
176
|
|
|
@@ -169,7 +179,7 @@ async function deleteKey(keyName: string) {
|
|
|
169
179
|
try {
|
|
170
180
|
await $fetch(`${apiBase}/api/keys/${keyName}`, { method: 'DELETE' })
|
|
171
181
|
await refreshKeys()
|
|
172
|
-
} catch {}
|
|
182
|
+
} catch { /* best-effort; surfaced via list refresh */ }
|
|
173
183
|
deletingKey.value = null
|
|
174
184
|
}
|
|
175
185
|
|
|
@@ -177,7 +187,7 @@ const keyOptions = [
|
|
|
177
187
|
{ label: 'OPENAI_API_KEY', value: 'OPENAI_API_KEY' },
|
|
178
188
|
{ label: 'FAL_API_KEY', value: 'FAL_API_KEY' },
|
|
179
189
|
{ label: 'GOOGLE_API_KEY', value: 'GOOGLE_API_KEY' },
|
|
180
|
-
{ label: 'Custom...', value: 'custom' }
|
|
190
|
+
{ label: 'Custom...', value: 'custom' }
|
|
181
191
|
]
|
|
182
192
|
|
|
183
193
|
// ─── PR63b v2.89.0 — MCPs / Hooks / Plugins / Theme sections ────────────
|
|
@@ -231,11 +241,8 @@ const colorMode = useColorMode()
|
|
|
231
241
|
const themeOptions = [
|
|
232
242
|
{ label: 'System (auto)', value: 'system' },
|
|
233
243
|
{ label: 'Light', value: 'light' },
|
|
234
|
-
{ label: 'Dark', value: 'dark' }
|
|
244
|
+
{ label: 'Dark', value: 'dark' }
|
|
235
245
|
]
|
|
236
|
-
|
|
237
|
-
// PR92d v3.42.0 — primary color picker.
|
|
238
|
-
import { THEME_COLOR_OPTIONS, useThemeColor } from '~/composables/useThemeColor'
|
|
239
246
|
const themeColor = useThemeColor()
|
|
240
247
|
const themeColorOptions = THEME_COLOR_OPTIONS
|
|
241
248
|
|
|
@@ -256,19 +263,52 @@ function formatInstalledAt(iso: string): string {
|
|
|
256
263
|
|
|
257
264
|
// ─── Section nav ────────────────────────────────────────────────────────
|
|
258
265
|
|
|
259
|
-
type SectionId = 'profile' | 'projects' | 'keys' | 'mcps' | 'hooks' | 'plugins' | 'theme'
|
|
260
|
-
|
|
261
|
-
const sections: { id: SectionId
|
|
262
|
-
{ id: 'profile',
|
|
263
|
-
{ id: 'projects', label: 'Projects',
|
|
264
|
-
{ id: 'keys',
|
|
265
|
-
{ id: 'mcps',
|
|
266
|
-
{ id: 'hooks',
|
|
267
|
-
{ id: 'plugins',
|
|
268
|
-
{ id: 'theme',
|
|
266
|
+
type SectionId = 'profile' | 'projects' | 'keys' | 'mcps' | 'hooks' | 'plugins' | 'theme' | 'updates'
|
|
267
|
+
|
|
268
|
+
const sections: { id: SectionId, label: string, icon: string }[] = [
|
|
269
|
+
{ id: 'profile', label: 'Profile', icon: 'i-lucide-user-circle' },
|
|
270
|
+
{ id: 'projects', label: 'Projects', icon: 'i-lucide-folders' },
|
|
271
|
+
{ id: 'keys', label: 'API Keys', icon: 'i-lucide-key' },
|
|
272
|
+
{ id: 'mcps', label: 'MCPs', icon: 'i-lucide-plug-2' },
|
|
273
|
+
{ id: 'hooks', label: 'Hooks', icon: 'i-lucide-webhook' },
|
|
274
|
+
{ id: 'plugins', label: 'Plugins', icon: 'i-lucide-puzzle' },
|
|
275
|
+
{ id: 'theme', label: 'Theme', icon: 'i-lucide-palette' },
|
|
276
|
+
{ id: 'updates', label: 'Updates', icon: 'i-lucide-download' }
|
|
269
277
|
]
|
|
270
278
|
|
|
271
279
|
const activeSection = ref<SectionId>('profile')
|
|
280
|
+
|
|
281
|
+
// ─── Updates (v3.72.0) — version check + one-click core update ──────────
|
|
282
|
+
const ver = ref<{ current: string, latest: string | null, update_available: boolean } | null>(null)
|
|
283
|
+
const checkingVer = ref(false)
|
|
284
|
+
const updating = ref(false)
|
|
285
|
+
const updateResult = ref<{ ok: boolean, output: string } | null>(null)
|
|
286
|
+
|
|
287
|
+
async function checkVersion() {
|
|
288
|
+
checkingVer.value = true
|
|
289
|
+
try {
|
|
290
|
+
ver.value = await $fetch(`${apiBase}/api/system/version`)
|
|
291
|
+
} catch {
|
|
292
|
+
ver.value = null
|
|
293
|
+
} finally {
|
|
294
|
+
checkingVer.value = false
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
async function runUpdate() {
|
|
299
|
+
updating.value = true
|
|
300
|
+
updateResult.value = null
|
|
301
|
+
try {
|
|
302
|
+
updateResult.value = await $fetch(`${apiBase}/api/system/update`, { method: 'POST' })
|
|
303
|
+
} catch (e) {
|
|
304
|
+
updateResult.value = { ok: false, output: e instanceof Error ? e.message : String(e) }
|
|
305
|
+
} finally {
|
|
306
|
+
updating.value = false
|
|
307
|
+
checkVersion()
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
onMounted(checkVersion)
|
|
272
312
|
</script>
|
|
273
313
|
|
|
274
314
|
<template>
|
|
@@ -299,8 +339,8 @@ const activeSection = ref<SectionId>('profile')
|
|
|
299
339
|
<span>{{ s.label }}</span>
|
|
300
340
|
</button>
|
|
301
341
|
<p class="text-xs text-muted px-3 mt-6">
|
|
302
|
-
|
|
303
|
-
read-only diagnostics
|
|
342
|
+
8 sections. Profile + Projects edit data; Updates runs the core
|
|
343
|
+
update; everything else is read-only diagnostics.
|
|
304
344
|
</p>
|
|
305
345
|
</nav>
|
|
306
346
|
|
|
@@ -308,7 +348,9 @@ const activeSection = ref<SectionId>('profile')
|
|
|
308
348
|
<div>
|
|
309
349
|
<!-- Profile -->
|
|
310
350
|
<section v-if="activeSection === 'profile'">
|
|
311
|
-
<h2 class="text-lg font-semibold mb-1">
|
|
351
|
+
<h2 class="text-lg font-semibold mb-1">
|
|
352
|
+
Profile
|
|
353
|
+
</h2>
|
|
312
354
|
<p class="text-sm text-muted mb-6">
|
|
313
355
|
Your identity, role, and language. Stored locally at
|
|
314
356
|
<code class="font-mono text-xs">~/.arkaos/profile.json</code>.
|
|
@@ -448,7 +490,9 @@ const activeSection = ref<SectionId>('profile')
|
|
|
448
490
|
|
|
449
491
|
<!-- Projects -->
|
|
450
492
|
<section v-else-if="activeSection === 'projects'">
|
|
451
|
-
<h2 class="text-lg font-semibold mb-1">
|
|
493
|
+
<h2 class="text-lg font-semibold mb-1">
|
|
494
|
+
Project directories
|
|
495
|
+
</h2>
|
|
452
496
|
<p class="text-sm text-muted mb-6">
|
|
453
497
|
Directories the sync engine scans for projects.
|
|
454
498
|
Comma-separated absolute paths (e.g.
|
|
@@ -500,7 +544,9 @@ const activeSection = ref<SectionId>('profile')
|
|
|
500
544
|
|
|
501
545
|
<!-- API Keys -->
|
|
502
546
|
<section v-else-if="activeSection === 'keys'">
|
|
503
|
-
<h2 class="text-lg font-semibold mb-1">
|
|
547
|
+
<h2 class="text-lg font-semibold mb-1">
|
|
548
|
+
API Keys
|
|
549
|
+
</h2>
|
|
504
550
|
<p class="text-sm text-muted mb-6">
|
|
505
551
|
Configure API keys for external services. Keys are stored
|
|
506
552
|
locally at <code class="font-mono text-xs">~/.arkaos/keys.json</code>.
|
|
@@ -508,11 +554,18 @@ const activeSection = ref<SectionId>('profile')
|
|
|
508
554
|
|
|
509
555
|
<UCard class="mb-6">
|
|
510
556
|
<div class="space-y-4">
|
|
511
|
-
<p class="text-xs font-semibold text-muted uppercase tracking-wider">
|
|
557
|
+
<p class="text-xs font-semibold text-muted uppercase tracking-wider">
|
|
558
|
+
Add API Key
|
|
559
|
+
</p>
|
|
512
560
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-3 items-end">
|
|
513
561
|
<div>
|
|
514
562
|
<label class="text-xs text-muted mb-1 block">Provider</label>
|
|
515
|
-
<USelect
|
|
563
|
+
<USelect
|
|
564
|
+
v-model="newKey"
|
|
565
|
+
:items="keyOptions"
|
|
566
|
+
class="w-full"
|
|
567
|
+
placeholder="Select key..."
|
|
568
|
+
/>
|
|
516
569
|
</div>
|
|
517
570
|
<div v-if="isCustom">
|
|
518
571
|
<label class="text-xs text-muted mb-1 block">Key Name</label>
|
|
@@ -520,7 +573,12 @@ const activeSection = ref<SectionId>('profile')
|
|
|
520
573
|
</div>
|
|
521
574
|
<div :class="isCustom ? '' : 'md:col-span-1'">
|
|
522
575
|
<label class="text-xs text-muted mb-1 block">Value</label>
|
|
523
|
-
<UInput
|
|
576
|
+
<UInput
|
|
577
|
+
v-model="newValue"
|
|
578
|
+
type="password"
|
|
579
|
+
class="w-full"
|
|
580
|
+
placeholder="sk-..."
|
|
581
|
+
/>
|
|
524
582
|
</div>
|
|
525
583
|
<div>
|
|
526
584
|
<UButton
|
|
@@ -560,9 +618,17 @@ const activeSection = ref<SectionId>('profile')
|
|
|
560
618
|
variant="subtle"
|
|
561
619
|
size="xs"
|
|
562
620
|
/>
|
|
563
|
-
<UBadge
|
|
621
|
+
<UBadge
|
|
622
|
+
v-else
|
|
623
|
+
label="Not Set"
|
|
624
|
+
color="neutral"
|
|
625
|
+
variant="outline"
|
|
626
|
+
size="xs"
|
|
627
|
+
/>
|
|
564
628
|
</div>
|
|
565
|
-
<p v-if="k.used_for" class="text-xs text-muted mt-0.5">
|
|
629
|
+
<p v-if="k.used_for" class="text-xs text-muted mt-0.5">
|
|
630
|
+
{{ k.used_for }}
|
|
631
|
+
</p>
|
|
566
632
|
<p v-if="k.masked_value && k.configured" class="text-xs font-mono text-muted/60 mt-0.5">
|
|
567
633
|
{{ k.masked_value }}
|
|
568
634
|
</p>
|
|
@@ -585,7 +651,9 @@ const activeSection = ref<SectionId>('profile')
|
|
|
585
651
|
<!-- MCPs -->
|
|
586
652
|
<section v-else-if="activeSection === 'mcps'">
|
|
587
653
|
<div class="flex items-baseline justify-between mb-1">
|
|
588
|
-
<h2 class="text-lg font-semibold">
|
|
654
|
+
<h2 class="text-lg font-semibold">
|
|
655
|
+
MCPs
|
|
656
|
+
</h2>
|
|
589
657
|
<UButton
|
|
590
658
|
label="Refresh"
|
|
591
659
|
variant="ghost"
|
|
@@ -601,7 +669,9 @@ const activeSection = ref<SectionId>('profile')
|
|
|
601
669
|
</p>
|
|
602
670
|
<div v-if="!mcps.length" class="rounded-lg border border-default p-6 text-center">
|
|
603
671
|
<UIcon name="i-lucide-plug-2" class="size-10 text-muted mx-auto mb-2" />
|
|
604
|
-
<p class="text-sm text-muted">
|
|
672
|
+
<p class="text-sm text-muted">
|
|
673
|
+
No MCP servers configured.
|
|
674
|
+
</p>
|
|
605
675
|
</div>
|
|
606
676
|
<div v-else class="space-y-2">
|
|
607
677
|
<div
|
|
@@ -632,7 +702,9 @@ const activeSection = ref<SectionId>('profile')
|
|
|
632
702
|
<!-- Hooks -->
|
|
633
703
|
<section v-else-if="activeSection === 'hooks'">
|
|
634
704
|
<div class="flex items-baseline justify-between mb-1">
|
|
635
|
-
<h2 class="text-lg font-semibold">
|
|
705
|
+
<h2 class="text-lg font-semibold">
|
|
706
|
+
Hooks
|
|
707
|
+
</h2>
|
|
636
708
|
<UButton
|
|
637
709
|
label="Refresh"
|
|
638
710
|
variant="ghost"
|
|
@@ -657,7 +729,9 @@ const activeSection = ref<SectionId>('profile')
|
|
|
657
729
|
</div>
|
|
658
730
|
<div v-if="!hooks.length" class="rounded-lg border border-default p-6 text-center">
|
|
659
731
|
<UIcon name="i-lucide-webhook" class="size-10 text-muted mx-auto mb-2" />
|
|
660
|
-
<p class="text-sm text-muted">
|
|
732
|
+
<p class="text-sm text-muted">
|
|
733
|
+
No hooks wired in settings.json.
|
|
734
|
+
</p>
|
|
661
735
|
</div>
|
|
662
736
|
<div v-else class="space-y-3">
|
|
663
737
|
<div
|
|
@@ -691,7 +765,9 @@ const activeSection = ref<SectionId>('profile')
|
|
|
691
765
|
<!-- Plugins -->
|
|
692
766
|
<section v-else-if="activeSection === 'plugins'">
|
|
693
767
|
<div class="flex items-baseline justify-between mb-1">
|
|
694
|
-
<h2 class="text-lg font-semibold">
|
|
768
|
+
<h2 class="text-lg font-semibold">
|
|
769
|
+
Plugins
|
|
770
|
+
</h2>
|
|
695
771
|
<UButton
|
|
696
772
|
label="Refresh"
|
|
697
773
|
variant="ghost"
|
|
@@ -707,7 +783,9 @@ const activeSection = ref<SectionId>('profile')
|
|
|
707
783
|
</p>
|
|
708
784
|
<div v-if="!plugins.length" class="rounded-lg border border-default p-6 text-center">
|
|
709
785
|
<UIcon name="i-lucide-puzzle" class="size-10 text-muted mx-auto mb-2" />
|
|
710
|
-
<p class="text-sm text-muted">
|
|
786
|
+
<p class="text-sm text-muted">
|
|
787
|
+
No plugins installed.
|
|
788
|
+
</p>
|
|
711
789
|
<p class="text-xs text-muted mt-2">
|
|
712
790
|
Try <code class="font-mono">/plugin marketplace add andreagroferreira/arka-os</code>
|
|
713
791
|
from Claude Code.
|
|
@@ -724,8 +802,18 @@ const activeSection = ref<SectionId>('profile')
|
|
|
724
802
|
<div class="flex items-center gap-2 mb-0.5">
|
|
725
803
|
<span class="text-sm font-semibold">{{ p.name }}</span>
|
|
726
804
|
<UBadge :label="p.marketplace" variant="outline" size="xs" />
|
|
727
|
-
<UBadge
|
|
728
|
-
|
|
805
|
+
<UBadge
|
|
806
|
+
v-if="p.scope"
|
|
807
|
+
:label="p.scope"
|
|
808
|
+
variant="soft"
|
|
809
|
+
size="xs"
|
|
810
|
+
/>
|
|
811
|
+
<UBadge
|
|
812
|
+
v-if="p.version"
|
|
813
|
+
:label="`v${p.version}`"
|
|
814
|
+
variant="subtle"
|
|
815
|
+
size="xs"
|
|
816
|
+
/>
|
|
729
817
|
</div>
|
|
730
818
|
<p v-if="p.installed_at" class="text-xs text-muted">
|
|
731
819
|
Installed {{ formatInstalledAt(p.installed_at) }}
|
|
@@ -737,7 +825,9 @@ const activeSection = ref<SectionId>('profile')
|
|
|
737
825
|
|
|
738
826
|
<!-- Theme -->
|
|
739
827
|
<section v-else-if="activeSection === 'theme'">
|
|
740
|
-
<h2 class="text-lg font-semibold mb-1">
|
|
828
|
+
<h2 class="text-lg font-semibold mb-1">
|
|
829
|
+
Theme
|
|
830
|
+
</h2>
|
|
741
831
|
<p class="text-sm text-muted mb-6">
|
|
742
832
|
Light / dark / system (follows OS preference).
|
|
743
833
|
Stored locally by your browser.
|
|
@@ -779,7 +869,7 @@ const activeSection = ref<SectionId>('profile')
|
|
|
779
869
|
'bg-rose-500': opt.value === 'rose',
|
|
780
870
|
'bg-amber-500': opt.value === 'amber',
|
|
781
871
|
'bg-teal-500': opt.value === 'teal',
|
|
782
|
-
'bg-cyan-500': opt.value === 'cyan'
|
|
872
|
+
'bg-cyan-500': opt.value === 'cyan'
|
|
783
873
|
}"
|
|
784
874
|
/>
|
|
785
875
|
{{ opt.label }}
|
|
@@ -789,6 +879,80 @@ const activeSection = ref<SectionId>('profile')
|
|
|
789
879
|
</div>
|
|
790
880
|
</UCard>
|
|
791
881
|
</section>
|
|
882
|
+
|
|
883
|
+
<section v-else-if="activeSection === 'updates'">
|
|
884
|
+
<h2 class="text-lg font-semibold mb-1">
|
|
885
|
+
Updates
|
|
886
|
+
</h2>
|
|
887
|
+
<p class="text-sm text-muted mb-6">
|
|
888
|
+
Keep ArkaOS current. The button runs the core update
|
|
889
|
+
(<code class="text-xs">npx arkaos@latest update</code>); finish
|
|
890
|
+
the project sync by running <code class="text-xs">/arka update</code>
|
|
891
|
+
in Claude Code.
|
|
892
|
+
</p>
|
|
893
|
+
<UCard>
|
|
894
|
+
<div class="space-y-4">
|
|
895
|
+
<div class="flex items-center gap-3 flex-wrap">
|
|
896
|
+
<div class="text-sm">
|
|
897
|
+
Installed:
|
|
898
|
+
<UBadge :label="`v${ver?.current ?? '—'}`" variant="subtle" size="sm" />
|
|
899
|
+
</div>
|
|
900
|
+
<div class="text-sm">
|
|
901
|
+
Latest:
|
|
902
|
+
<UBadge
|
|
903
|
+
:label="ver?.latest ? `v${ver.latest}` : '—'"
|
|
904
|
+
:color="ver?.update_available ? 'warning' : 'success'"
|
|
905
|
+
variant="subtle"
|
|
906
|
+
size="sm"
|
|
907
|
+
/>
|
|
908
|
+
</div>
|
|
909
|
+
<UButton
|
|
910
|
+
size="xs"
|
|
911
|
+
variant="ghost"
|
|
912
|
+
icon="i-lucide-refresh-cw"
|
|
913
|
+
:loading="checkingVer"
|
|
914
|
+
@click="checkVersion"
|
|
915
|
+
>
|
|
916
|
+
Check
|
|
917
|
+
</UButton>
|
|
918
|
+
</div>
|
|
919
|
+
|
|
920
|
+
<div v-if="ver?.update_available" class="flex items-center gap-3">
|
|
921
|
+
<UButton
|
|
922
|
+
icon="i-lucide-download"
|
|
923
|
+
color="primary"
|
|
924
|
+
:loading="updating"
|
|
925
|
+
@click="runUpdate"
|
|
926
|
+
>
|
|
927
|
+
{{ updating ? 'Updating…' : `Update to v${ver.latest}` }}
|
|
928
|
+
</UButton>
|
|
929
|
+
<span class="text-xs text-muted">Runs the core update, then asks you to finish in Claude Code.</span>
|
|
930
|
+
</div>
|
|
931
|
+
<p v-else-if="ver && !ver.update_available" class="text-sm text-success flex items-center gap-1.5">
|
|
932
|
+
<UIcon name="i-lucide-check-circle" class="size-4" />
|
|
933
|
+
You're on the latest version.
|
|
934
|
+
</p>
|
|
935
|
+
<p v-else class="text-sm text-muted">
|
|
936
|
+
Couldn't reach the version service.
|
|
937
|
+
</p>
|
|
938
|
+
|
|
939
|
+
<div
|
|
940
|
+
v-if="updateResult"
|
|
941
|
+
class="rounded-lg border p-3 text-xs"
|
|
942
|
+
:class="updateResult.ok ? 'border-success/40 bg-success/5' : 'border-error/40 bg-error/5'"
|
|
943
|
+
>
|
|
944
|
+
<div class="flex items-center gap-1.5 font-medium mb-1">
|
|
945
|
+
<UIcon :name="updateResult.ok ? 'i-lucide-check-circle' : 'i-lucide-alert-triangle'" class="size-4" />
|
|
946
|
+
{{ updateResult.ok ? 'Core updated' : 'Update failed' }}
|
|
947
|
+
</div>
|
|
948
|
+
<p v-if="updateResult.ok" class="text-muted">
|
|
949
|
+
Now run <code>/arka update</code> in Claude Code to sync all projects (step 2).
|
|
950
|
+
</p>
|
|
951
|
+
<pre class="mt-2 whitespace-pre-wrap font-mono text-[11px] text-muted max-h-48 overflow-y-auto">{{ updateResult.output }}</pre>
|
|
952
|
+
</div>
|
|
953
|
+
</div>
|
|
954
|
+
</UCard>
|
|
955
|
+
</section>
|
|
792
956
|
</div>
|
|
793
957
|
</div>
|
|
794
958
|
</template>
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-OS autostart for the ArkaOS dashboard (v3.72.0, feature #2).
|
|
3
|
+
*
|
|
4
|
+
* Opt-in: `npx arkaos autostart enable|disable|status`. Installs a per-OS
|
|
5
|
+
* boot item that runs `scripts/start-dashboard.sh` (macOS/Linux) or
|
|
6
|
+
* `scripts/start-dashboard.ps1` (Windows) — the same launcher the
|
|
7
|
+
* `dashboard` command uses, which already starts the Python API + the
|
|
8
|
+
* dashboard UI (preferring the production build, falling back to dev).
|
|
9
|
+
*
|
|
10
|
+
* ESM, Node + Bun, no interactive prompts. The unit-generation (`unitFor`)
|
|
11
|
+
* is pure so it can be unit-tested without touching the OS.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { execSync } from "node:child_process";
|
|
15
|
+
import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
16
|
+
import { homedir } from "node:os";
|
|
17
|
+
import { dirname, join } from "node:path";
|
|
18
|
+
import { fileURLToPath } from "node:url";
|
|
19
|
+
|
|
20
|
+
export const AUTOSTART_LABEL = "io.wizardingcode.arkaos.dashboard";
|
|
21
|
+
const SERVICE_NAME = "arkaos-dashboard.service";
|
|
22
|
+
|
|
23
|
+
function defaultRepoRoot() {
|
|
24
|
+
return join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Pure: the boot unit for a given platform. Throws on unsupported OS. */
|
|
28
|
+
export function unitFor(os, { repoRoot, home }) {
|
|
29
|
+
if (os === "darwin") {
|
|
30
|
+
const startScript = `${repoRoot}/scripts/start-dashboard.sh`;
|
|
31
|
+
const log = `${home}/.arkaos/logs/dashboard-autostart.log`;
|
|
32
|
+
const content = `<?xml version="1.0" encoding="UTF-8"?>
|
|
33
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
34
|
+
<plist version="1.0">
|
|
35
|
+
<dict>
|
|
36
|
+
<key>Label</key>
|
|
37
|
+
<string>${AUTOSTART_LABEL}</string>
|
|
38
|
+
<key>ProgramArguments</key>
|
|
39
|
+
<array>
|
|
40
|
+
<string>/bin/bash</string>
|
|
41
|
+
<string>${startScript}</string>
|
|
42
|
+
</array>
|
|
43
|
+
<key>EnvironmentVariables</key>
|
|
44
|
+
<dict>
|
|
45
|
+
<key>ARKAOS_ROOT</key>
|
|
46
|
+
<string>${repoRoot}</string>
|
|
47
|
+
</dict>
|
|
48
|
+
<key>RunAtLoad</key>
|
|
49
|
+
<true/>
|
|
50
|
+
<key>StandardOutPath</key>
|
|
51
|
+
<string>${log}</string>
|
|
52
|
+
<key>StandardErrorPath</key>
|
|
53
|
+
<string>${log}</string>
|
|
54
|
+
</dict>
|
|
55
|
+
</plist>
|
|
56
|
+
`;
|
|
57
|
+
return {
|
|
58
|
+
kind: "launchd",
|
|
59
|
+
path: join(home, "Library", "LaunchAgents", `${AUTOSTART_LABEL}.plist`),
|
|
60
|
+
content,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (os === "linux") {
|
|
65
|
+
const startScript = `${repoRoot}/scripts/start-dashboard.sh`;
|
|
66
|
+
const content = `[Unit]
|
|
67
|
+
Description=ArkaOS Dashboard (Python API + UI)
|
|
68
|
+
After=network.target
|
|
69
|
+
|
|
70
|
+
[Service]
|
|
71
|
+
Type=simple
|
|
72
|
+
Environment=ARKAOS_ROOT=${repoRoot}
|
|
73
|
+
ExecStart=/bin/bash ${startScript}
|
|
74
|
+
Restart=on-failure
|
|
75
|
+
|
|
76
|
+
[Install]
|
|
77
|
+
WantedBy=default.target
|
|
78
|
+
`;
|
|
79
|
+
return {
|
|
80
|
+
kind: "systemd",
|
|
81
|
+
path: join(home, ".config", "systemd", "user", SERVICE_NAME),
|
|
82
|
+
content,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (os === "win32") {
|
|
87
|
+
const ps1 = join(repoRoot, "scripts", "start-dashboard.ps1");
|
|
88
|
+
const content = `@echo off
|
|
89
|
+
rem ArkaOS dashboard autostart (login).
|
|
90
|
+
powershell -NoProfile -NonInteractive -ExecutionPolicy Bypass -File "${ps1}"
|
|
91
|
+
`;
|
|
92
|
+
return {
|
|
93
|
+
kind: "startup",
|
|
94
|
+
path: join(
|
|
95
|
+
home,
|
|
96
|
+
"AppData", "Roaming", "Microsoft", "Windows",
|
|
97
|
+
"Start Menu", "Programs", "Startup",
|
|
98
|
+
"arkaos-dashboard.cmd",
|
|
99
|
+
),
|
|
100
|
+
content,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
throw new Error(`unsupported platform for autostart: ${os}`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function _silent(cmd) {
|
|
108
|
+
try {
|
|
109
|
+
execSync(cmd, { stdio: "pipe" });
|
|
110
|
+
return true;
|
|
111
|
+
} catch {
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function enable({ repoRoot = defaultRepoRoot() } = {}) {
|
|
117
|
+
const unit = unitFor(process.platform, { repoRoot, home: homedir() });
|
|
118
|
+
mkdirSync(dirname(unit.path), { recursive: true });
|
|
119
|
+
mkdirSync(join(homedir(), ".arkaos", "logs"), { recursive: true });
|
|
120
|
+
writeFileSync(unit.path, unit.content, "utf8");
|
|
121
|
+
if (unit.kind === "launchd") {
|
|
122
|
+
_silent(`launchctl unload "${unit.path}"`);
|
|
123
|
+
if (!_silent(`launchctl load -w "${unit.path}"`)) {
|
|
124
|
+
return { ok: false, path: unit.path,
|
|
125
|
+
message: `Plist written but launchctl load failed — it will still run at next login.` };
|
|
126
|
+
}
|
|
127
|
+
} else if (unit.kind === "systemd") {
|
|
128
|
+
_silent("systemctl --user daemon-reload");
|
|
129
|
+
if (!_silent(`systemctl --user enable --now ${SERVICE_NAME}`)) {
|
|
130
|
+
return { ok: false, path: unit.path,
|
|
131
|
+
message: `Service written but systemctl enable failed — check 'systemctl --user' availability.` };
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
// startup: writing the .cmd is sufficient; it runs on next login.
|
|
135
|
+
return { ok: true, path: unit.path,
|
|
136
|
+
message: `Autostart enabled (${unit.kind}). The dashboard will start on login.` };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function disable() {
|
|
140
|
+
const unit = unitFor(process.platform, { repoRoot: defaultRepoRoot(), home: homedir() });
|
|
141
|
+
if (unit.kind === "launchd") _silent(`launchctl unload "${unit.path}"`);
|
|
142
|
+
else if (unit.kind === "systemd") _silent(`systemctl --user disable --now ${SERVICE_NAME}`);
|
|
143
|
+
if (existsSync(unit.path)) rmSync(unit.path);
|
|
144
|
+
return { ok: true, path: unit.path, message: "Autostart disabled." };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function status() {
|
|
148
|
+
let unit;
|
|
149
|
+
try {
|
|
150
|
+
unit = unitFor(process.platform, { repoRoot: defaultRepoRoot(), home: homedir() });
|
|
151
|
+
} catch (err) {
|
|
152
|
+
return { installed: false, supported: false, message: err.message };
|
|
153
|
+
}
|
|
154
|
+
return {
|
|
155
|
+
installed: existsSync(unit.path),
|
|
156
|
+
supported: true,
|
|
157
|
+
kind: unit.kind,
|
|
158
|
+
path: unit.path,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export async function autostart(args = []) {
|
|
163
|
+
const action = (args[0] || "status").toLowerCase();
|
|
164
|
+
if (action === "enable") {
|
|
165
|
+
const r = enable();
|
|
166
|
+
console.log(` ${r.ok ? "✓" : "⚠"} ${r.message}\n ${r.path}`);
|
|
167
|
+
} else if (action === "disable") {
|
|
168
|
+
const r = disable();
|
|
169
|
+
console.log(` ✓ ${r.message}`);
|
|
170
|
+
} else if (action === "status") {
|
|
171
|
+
const s = status();
|
|
172
|
+
if (!s.supported) console.log(` Autostart not supported here: ${s.message}`);
|
|
173
|
+
else console.log(` Autostart: ${s.installed ? "ENABLED" : "disabled"} (${s.kind})\n ${s.path}`);
|
|
174
|
+
} else {
|
|
175
|
+
console.error(` Unknown autostart action: ${action}. Use enable | disable | status.`);
|
|
176
|
+
process.exitCode = 1;
|
|
177
|
+
}
|
|
178
|
+
}
|
package/installer/cli.js
CHANGED
|
@@ -45,6 +45,7 @@ Usage:
|
|
|
45
45
|
npx arkaos migrate Migrate from v1 to v2
|
|
46
46
|
npx arkaos migrate-user-data Move user data (~/.claude/skills/arka/ → ~/.arkaos/)
|
|
47
47
|
npx arkaos dashboard Start monitoring dashboard
|
|
48
|
+
npx arkaos autostart <enable|disable|status> Start dashboard on boot
|
|
48
49
|
npx arkaos keys Manage API keys (OpenAI, fal.ai, etc.)
|
|
49
50
|
npx arkaos doctor Run health checks
|
|
50
51
|
npx arkaos uninstall Remove ArkaOS
|
|
@@ -101,6 +102,12 @@ async function main() {
|
|
|
101
102
|
await update();
|
|
102
103
|
break;
|
|
103
104
|
|
|
105
|
+
case "autostart": {
|
|
106
|
+
const { autostart } = await import("./autostart.js");
|
|
107
|
+
await autostart(positionals.slice(1));
|
|
108
|
+
break;
|
|
109
|
+
}
|
|
110
|
+
|
|
104
111
|
case "uninstall":
|
|
105
112
|
const { uninstall } = await import("./uninstall.js");
|
|
106
113
|
await uninstall();
|
package/package.json
CHANGED
package/pyproject.toml
CHANGED
|
Binary file
|
package/scripts/dashboard-api.py
CHANGED
|
@@ -20,7 +20,7 @@ from typing import Optional
|
|
|
20
20
|
ARKAOS_ROOT = Path(os.environ.get("ARKAOS_ROOT", Path(__file__).resolve().parent.parent))
|
|
21
21
|
sys.path.insert(0, str(ARKAOS_ROOT))
|
|
22
22
|
|
|
23
|
-
from fastapi import FastAPI, Query, WebSocket, WebSocketDisconnect
|
|
23
|
+
from fastapi import FastAPI, Query, Request, WebSocket, WebSocketDisconnect
|
|
24
24
|
from fastapi.middleware.cors import CORSMiddleware
|
|
25
25
|
|
|
26
26
|
app = FastAPI(title="ArkaOS Dashboard API", version="2.2.0")
|
|
@@ -2259,6 +2259,160 @@ def workflow_update_yaml(workflow_id: str, body: dict):
|
|
|
2259
2259
|
return {"updated": True, "id": workflow_id, "file": str(target)}
|
|
2260
2260
|
|
|
2261
2261
|
|
|
2262
|
+
# ============================================================================
|
|
2263
|
+
# v3.72.0 — Cognition: surface Dreaming insights (read-only) to the dashboard.
|
|
2264
|
+
# Reads existing markdown insights from <vault>/Projects/ArkaOS/Dreams via
|
|
2265
|
+
# core.cognition.dreams_reader. No new engine — pure read.
|
|
2266
|
+
# ============================================================================
|
|
2267
|
+
|
|
2268
|
+
|
|
2269
|
+
def _dreams_dir():
|
|
2270
|
+
"""Resolve the Dreams folder from the user's vault, or None."""
|
|
2271
|
+
from core.runtime.path_resolver import load_profile, ProfileMissingError
|
|
2272
|
+
try:
|
|
2273
|
+
profile = load_profile()
|
|
2274
|
+
except ProfileMissingError:
|
|
2275
|
+
return None
|
|
2276
|
+
except Exception: # noqa: BLE001 — never break the endpoint on profile errors
|
|
2277
|
+
return None
|
|
2278
|
+
vault = getattr(profile, "vault_path", None)
|
|
2279
|
+
return (Path(vault) / "Projects" / "ArkaOS" / "Dreams") if vault else None
|
|
2280
|
+
|
|
2281
|
+
|
|
2282
|
+
def _insight_to_dict(ins) -> dict:
|
|
2283
|
+
return {
|
|
2284
|
+
"date": ins.date,
|
|
2285
|
+
"title": ins.title,
|
|
2286
|
+
"confidence": ins.confidence,
|
|
2287
|
+
"sources": list(ins.sources),
|
|
2288
|
+
"tags": list(ins.tags),
|
|
2289
|
+
"body": ins.body,
|
|
2290
|
+
}
|
|
2291
|
+
|
|
2292
|
+
|
|
2293
|
+
@app.get("/api/cognition/insights")
|
|
2294
|
+
def cognition_insights(days: int = 7):
|
|
2295
|
+
"""Recent Dreaming insights within the given window. Never 500s."""
|
|
2296
|
+
dreams = _dreams_dir()
|
|
2297
|
+
if dreams is None:
|
|
2298
|
+
return {"insights": [], "available": False}
|
|
2299
|
+
from core.cognition.dreams_reader import list_insights
|
|
2300
|
+
items = list_insights(dreams, since_days=max(1, int(days)))
|
|
2301
|
+
return {"insights": [_insight_to_dict(i) for i in items], "available": True}
|
|
2302
|
+
|
|
2303
|
+
|
|
2304
|
+
@app.get("/api/cognition/status")
|
|
2305
|
+
def cognition_status():
|
|
2306
|
+
"""Insight counts + confidence breakdown + last activity date."""
|
|
2307
|
+
dreams = _dreams_dir()
|
|
2308
|
+
if dreams is None:
|
|
2309
|
+
return {
|
|
2310
|
+
"today": 0, "week": 0, "total": 0,
|
|
2311
|
+
"by_confidence": {"high": 0, "medium": 0, "low": 0},
|
|
2312
|
+
"vault_configured": False, "last_date": None,
|
|
2313
|
+
}
|
|
2314
|
+
from core.cognition.dreams_reader import list_insights
|
|
2315
|
+
all_items = list_insights(dreams, since_days=36500)
|
|
2316
|
+
by_conf = {"high": 0, "medium": 0, "low": 0}
|
|
2317
|
+
for i in all_items:
|
|
2318
|
+
by_conf[i.confidence if i.confidence in by_conf else "medium"] += 1
|
|
2319
|
+
return {
|
|
2320
|
+
"today": len(list_insights(dreams, since_days=1)),
|
|
2321
|
+
"week": len(list_insights(dreams, since_days=7)),
|
|
2322
|
+
"total": len(all_items),
|
|
2323
|
+
"by_confidence": by_conf,
|
|
2324
|
+
"vault_configured": True,
|
|
2325
|
+
"last_date": all_items[0].date if all_items else None,
|
|
2326
|
+
}
|
|
2327
|
+
|
|
2328
|
+
|
|
2329
|
+
# ============================================================================
|
|
2330
|
+
# v3.72.0 — System: version check + one-click core update (feature #3).
|
|
2331
|
+
# ============================================================================
|
|
2332
|
+
|
|
2333
|
+
_npm_latest_cache = {"version": None, "ts": 0.0}
|
|
2334
|
+
|
|
2335
|
+
|
|
2336
|
+
def _current_version() -> str:
|
|
2337
|
+
root = os.environ.get("ARKAOS_ROOT") or str(Path(__file__).resolve().parent.parent)
|
|
2338
|
+
try:
|
|
2339
|
+
return (Path(root) / "VERSION").read_text(encoding="utf-8").strip()
|
|
2340
|
+
except OSError:
|
|
2341
|
+
return "0.0.0"
|
|
2342
|
+
|
|
2343
|
+
|
|
2344
|
+
def _npm_latest_version():
|
|
2345
|
+
import subprocess
|
|
2346
|
+
import time
|
|
2347
|
+
now = time.time()
|
|
2348
|
+
if _npm_latest_cache["version"] and now - _npm_latest_cache["ts"] < 600:
|
|
2349
|
+
return _npm_latest_cache["version"]
|
|
2350
|
+
try:
|
|
2351
|
+
out = subprocess.run(
|
|
2352
|
+
["npm", "view", "arkaos", "version"],
|
|
2353
|
+
capture_output=True, text=True, timeout=20,
|
|
2354
|
+
)
|
|
2355
|
+
latest = (out.stdout or "").strip() or None
|
|
2356
|
+
except Exception: # noqa: BLE001
|
|
2357
|
+
latest = None
|
|
2358
|
+
if latest:
|
|
2359
|
+
_npm_latest_cache["version"] = latest
|
|
2360
|
+
_npm_latest_cache["ts"] = now
|
|
2361
|
+
return latest
|
|
2362
|
+
|
|
2363
|
+
|
|
2364
|
+
def _is_newer(latest: str, current: str) -> bool:
|
|
2365
|
+
import re
|
|
2366
|
+
def _parts(v):
|
|
2367
|
+
nums = [int(n) for n in re.findall(r"\d+", v)[:3]]
|
|
2368
|
+
return nums or [0]
|
|
2369
|
+
try:
|
|
2370
|
+
return _parts(latest) > _parts(current)
|
|
2371
|
+
except Exception: # noqa: BLE001
|
|
2372
|
+
return False
|
|
2373
|
+
|
|
2374
|
+
|
|
2375
|
+
def _run_core_update() -> dict:
|
|
2376
|
+
import subprocess
|
|
2377
|
+
try:
|
|
2378
|
+
out = subprocess.run(
|
|
2379
|
+
["npx", "arkaos@latest", "update"],
|
|
2380
|
+
capture_output=True, text=True, timeout=600,
|
|
2381
|
+
)
|
|
2382
|
+
tail = ((out.stdout or "") + (out.stderr or ""))[-2000:]
|
|
2383
|
+
return {"ok": out.returncode == 0, "output": tail}
|
|
2384
|
+
except Exception as exc: # noqa: BLE001
|
|
2385
|
+
return {"ok": False, "output": f"update failed: {exc}"}
|
|
2386
|
+
|
|
2387
|
+
|
|
2388
|
+
@app.get("/api/system/version")
|
|
2389
|
+
def system_version():
|
|
2390
|
+
"""Current installed version vs the latest on npm."""
|
|
2391
|
+
current = _current_version()
|
|
2392
|
+
latest = _npm_latest_version()
|
|
2393
|
+
return {
|
|
2394
|
+
"current": current,
|
|
2395
|
+
"latest": latest,
|
|
2396
|
+
"update_available": bool(latest) and _is_newer(latest, current),
|
|
2397
|
+
}
|
|
2398
|
+
|
|
2399
|
+
|
|
2400
|
+
@app.post("/api/system/update")
|
|
2401
|
+
def system_update(request: Request):
|
|
2402
|
+
"""Run `npx arkaos@latest update` (update step 1). Step 2 (project
|
|
2403
|
+
sync via /arka update) is a Claude Code action the UI prompts for.
|
|
2404
|
+
|
|
2405
|
+
A regular POST is CORS-protected; as defense-in-depth for an endpoint
|
|
2406
|
+
that runs an update, reject any explicitly non-localhost origin (an
|
|
2407
|
+
empty origin — local CLI / same-origin — is allowed).
|
|
2408
|
+
"""
|
|
2409
|
+
origin = request.headers.get("origin", "")
|
|
2410
|
+
if origin and not _terminal_origin_ok(origin):
|
|
2411
|
+
from fastapi import HTTPException
|
|
2412
|
+
raise HTTPException(status_code=403, detail="origin not allowed")
|
|
2413
|
+
return _run_core_update()
|
|
2414
|
+
|
|
2415
|
+
|
|
2262
2416
|
# ============================================================================
|
|
2263
2417
|
# PR99a v3.67.0 — Terminal PTY WebSocket + REST.
|
|
2264
2418
|
#
|