arkaos 4.20.0 → 4.22.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.
Files changed (80) hide show
  1. package/README.md +1 -1
  2. package/VERSION +1 -1
  3. package/arka/SKILL.md +2 -1
  4. package/config/claude-agents/marta-cqo.md +9 -0
  5. package/config/standards/{claude-md-overlays → stack-rules}/laravel.md +11 -0
  6. package/config/standards/{claude-md-overlays → stack-rules}/node.md +10 -1
  7. package/config/standards/{claude-md-overlays → stack-rules}/nuxt.md +12 -1
  8. package/config/standards/stack-rules/php.md +13 -0
  9. package/config/standards/{claude-md-overlays → stack-rules}/python.md +5 -0
  10. package/config/standards/stack-rules/react.md +14 -0
  11. package/config/standards/stack-rules/vue.md +13 -0
  12. package/core/cognition/evolve.py +299 -0
  13. package/core/cognition/evolve_cli.py +55 -0
  14. package/core/governance/stop_lint.py +266 -0
  15. package/core/governance/stop_lint_telemetry.py +132 -0
  16. package/core/governance/tool_loop_check.py +111 -0
  17. package/core/hooks/stop.py +73 -22
  18. package/core/runtime/pricing.py +53 -19
  19. package/core/synapse/layers.py +49 -476
  20. package/core/synapse/layers_base.py +99 -0
  21. package/core/synapse/layers_kb.py +395 -0
  22. package/core/sync/content_syncer.py +79 -10
  23. package/dashboard/app/components/AgentDependencyGraph.vue +5 -5
  24. package/dashboard/app/components/AgentEditDrawer.vue +120 -52
  25. package/dashboard/app/components/AgentSuggestionsCard.vue +4 -2
  26. package/dashboard/app/components/ConfirmDialog.vue +2 -2
  27. package/dashboard/app/components/DashboardState.vue +5 -3
  28. package/dashboard/app/components/GlobalSearch.vue +11 -7
  29. package/dashboard/app/components/KeyboardShortcutsHelp.vue +8 -6
  30. package/dashboard/app/components/KnowledgeSourcesList.vue +13 -9
  31. package/dashboard/app/components/MarkdownEditor.vue +1 -1
  32. package/dashboard/app/components/NotificationsBell.vue +8 -4
  33. package/dashboard/app/components/OnboardingTour.vue +24 -11
  34. package/dashboard/app/components/PersonaCloneDialog.vue +15 -8
  35. package/dashboard/app/components/PersonaWizard.vue +105 -72
  36. package/dashboard/app/components/SidebarFavoritesWidget.vue +5 -5
  37. package/dashboard/app/components/SidebarStatsWidget.vue +3 -1
  38. package/dashboard/app/components/Terminal.vue +9 -8
  39. package/dashboard/app/components/TextDiff.vue +27 -15
  40. package/dashboard/app/composables/useActivityFeed.ts +3 -3
  41. package/dashboard/app/composables/useApi.ts +4 -2
  42. package/dashboard/app/composables/useConfirmDialog.ts +1 -1
  43. package/dashboard/app/composables/useFavorites.ts +8 -8
  44. package/dashboard/app/composables/useTerminalThemes.ts +13 -13
  45. package/dashboard/app/composables/useThemeColor.ts +7 -4
  46. package/dashboard/app/pages/agents/[id].vue +319 -159
  47. package/dashboard/app/pages/agents/compare.vue +152 -52
  48. package/dashboard/app/pages/agents/index.vue +68 -55
  49. package/dashboard/app/pages/agents/new.vue +125 -58
  50. package/dashboard/app/pages/audit.vue +13 -6
  51. package/dashboard/app/pages/budget.vue +20 -18
  52. package/dashboard/app/pages/commands.vue +26 -22
  53. package/dashboard/app/pages/departments/[dept].vue +67 -31
  54. package/dashboard/app/pages/departments/compare.vue +85 -27
  55. package/dashboard/app/pages/departments/index.vue +31 -7
  56. package/dashboard/app/pages/health.vue +19 -12
  57. package/dashboard/app/pages/knowledge/index.vue +190 -78
  58. package/dashboard/app/pages/models.vue +74 -28
  59. package/dashboard/app/pages/personas/[id].vue +322 -219
  60. package/dashboard/app/pages/personas/archetypes.vue +20 -6
  61. package/dashboard/app/pages/personas/compare-with-agent.vue +109 -39
  62. package/dashboard/app/pages/personas/compare.vue +196 -124
  63. package/dashboard/app/pages/personas/index.vue +67 -60
  64. package/dashboard/app/pages/personas/new.vue +1 -1
  65. package/dashboard/app/pages/settings.vue +1 -0
  66. package/dashboard/app/pages/tasks.vue +80 -38
  67. package/dashboard/app/pages/trash.vue +11 -9
  68. package/dashboard/app/pages/workflows.vue +29 -19
  69. package/dashboard/nuxt.config.ts +2 -1
  70. package/dashboard/package.json +5 -0
  71. package/departments/kb/skills/doc-extraction/SKILL.md +99 -0
  72. package/departments/kb/skills/doc-redaction/SKILL.md +103 -0
  73. package/departments/kb/skills/knowledge-ops/SKILL.md +88 -0
  74. package/departments/ops/skills/github-ops/SKILL.md +89 -0
  75. package/departments/ops/skills/terminal-ops/SKILL.md +83 -0
  76. package/installer/index.js +1 -1
  77. package/knowledge/commands-registry.json +16 -3
  78. package/knowledge/skills-manifest.json +66 -1
  79. package/package.json +1 -1
  80. package/pyproject.toml +1 -1
@@ -9,10 +9,10 @@ const { fetchApi } = useApi()
9
9
 
10
10
  await favs.load()
11
11
  const { data: agentsData } = fetchApi<{ agents: Array<{ id: string, name: string }> }>(
12
- '/api/agents',
12
+ '/api/agents'
13
13
  )
14
14
  const { data: personasData } = fetchApi<{ personas: Array<{ id: string, name: string }> }>(
15
- '/api/personas',
15
+ '/api/personas'
16
16
  )
17
17
 
18
18
  interface FavRow { id: string, name: string }
@@ -20,7 +20,7 @@ interface FavRow { id: string, name: string }
20
20
  const starredAgents = computed<FavRow[]>(() => {
21
21
  const all = agentsData.value?.agents ?? []
22
22
  return favs.state.value.agents
23
- .map((id) => all.find((a) => a.id === id))
23
+ .map(id => all.find(a => a.id === id))
24
24
  .filter((a): a is FavRow => Boolean(a))
25
25
  .slice(0, 4)
26
26
  })
@@ -28,13 +28,13 @@ const starredAgents = computed<FavRow[]>(() => {
28
28
  const starredPersonas = computed<FavRow[]>(() => {
29
29
  const all = personasData.value?.personas ?? []
30
30
  return favs.state.value.personas
31
- .map((id) => all.find((p) => p.id === id))
31
+ .map(id => all.find(p => p.id === id))
32
32
  .filter((p): p is FavRow => Boolean(p))
33
33
  .slice(0, 4)
34
34
  })
35
35
 
36
36
  const hasAny = computed(
37
- () => starredAgents.value.length > 0 || starredPersonas.value.length > 0,
37
+ () => starredAgents.value.length > 0 || starredPersonas.value.length > 0
38
38
  )
39
39
  </script>
40
40
 
@@ -15,7 +15,9 @@ const { data, refresh } = fetchApi<SidebarStats>('/api/sidebar-stats')
15
15
 
16
16
  let timer: ReturnType<typeof setInterval> | null = null
17
17
  onMounted(() => {
18
- timer = setInterval(() => { refresh() }, 60_000)
18
+ timer = setInterval(() => {
19
+ refresh()
20
+ }, 60_000)
19
21
  })
20
22
  onBeforeUnmount(() => {
21
23
  if (timer) clearInterval(timer)
@@ -1,3 +1,4 @@
1
+ <!-- eslint-disable vue/multi-word-component-names -->
1
2
  <script setup lang="ts">
2
3
  // PR99b v3.68.0 — xterm.js terminal mount.
3
4
  // PR99c v3.69.0 — accepts an external session via prop so the tab
@@ -46,7 +47,7 @@ onMounted(async () => {
46
47
  lineHeight: 1.2,
47
48
  scrollback: 5000,
48
49
  theme: effectiveTheme.value,
49
- allowProposedApi: true,
50
+ allowProposedApi: true
50
51
  })
51
52
 
52
53
  // React to theme switches without remounting.
@@ -69,10 +70,10 @@ onMounted(async () => {
69
70
  // first mount and shells started at the default 80 cols, leaving
70
71
  // empty space on the right of the canvas.
71
72
  await nextTick()
72
- await new Promise<void>((resolve) => requestAnimationFrame(() => resolve()))
73
+ await new Promise<void>(resolve => requestAnimationFrame(() => resolve()))
73
74
  try {
74
75
  fitAddon.fit()
75
- } catch (_e) {
76
+ } catch {
76
77
  // dom layout not ready — ResizeObserver below will recover
77
78
  }
78
79
 
@@ -143,7 +144,7 @@ onMounted(async () => {
143
144
  requestAnimationFrame(() => {
144
145
  try {
145
146
  fitAddon.fit()
146
- } catch (_e) { /* layout not ready */ }
147
+ } catch { /* layout not ready */ }
147
148
  session.sendResize(t.cols, t.rows)
148
149
  })
149
150
  }
@@ -153,7 +154,7 @@ onMounted(async () => {
153
154
  try {
154
155
  fitAddon.fit()
155
156
  session.sendResize(t.cols, t.rows)
156
- } catch (_e) {
157
+ } catch {
157
158
  // dom may have unmounted
158
159
  }
159
160
  })
@@ -169,7 +170,7 @@ onMounted(async () => {
169
170
  try {
170
171
  fit.value?.fit()
171
172
  session.sendResize(term.value!.cols, term.value!.rows)
172
- } catch (_e) { /* layout not ready */ }
173
+ } catch { /* layout not ready */ }
173
174
  })
174
175
  })
175
176
  })
@@ -180,7 +181,7 @@ function refit() {
180
181
  try {
181
182
  fit.value.fit()
182
183
  session.sendResize(term.value.cols, term.value.rows)
183
- } catch (_e) {
184
+ } catch {
184
185
  // layout not ready
185
186
  }
186
187
  }
@@ -200,7 +201,7 @@ defineExpose({
200
201
  status: session.status,
201
202
  error: session.error,
202
203
  meta: session.meta,
203
- refit,
204
+ refit
204
205
  })
205
206
  </script>
206
207
 
@@ -25,10 +25,16 @@ function lineDiff(a: string, b: string): DiffRow[] {
25
25
  const n = right.length
26
26
  // Build LCS table.
27
27
  const dp: number[][] = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0))
28
+ // Indices below are always in-bounds (loops bounded by lengths; dp has
29
+ // m+1 rows / n+1 cols), so the fallbacks never fire — they only satisfy
30
+ // noUncheckedIndexedAccess. 0 matches the table fill value.
31
+ const cell = (i: number, j: number): number => dp[i]?.[j] ?? 0
28
32
  for (let i = m - 1; i >= 0; i -= 1) {
33
+ const row = dp[i]
34
+ if (!row) continue
29
35
  for (let j = n - 1; j >= 0; j -= 1) {
30
- if (left[i] === right[j]) dp[i][j] = dp[i + 1][j + 1] + 1
31
- else dp[i][j] = Math.max(dp[i + 1][j], dp[i][j + 1])
36
+ if (left[i] === right[j]) row[j] = cell(i + 1, j + 1) + 1
37
+ else row[j] = Math.max(cell(i + 1, j), cell(i, j + 1))
32
38
  }
33
39
  }
34
40
  // Walk back, emitting rows.
@@ -36,24 +42,26 @@ function lineDiff(a: string, b: string): DiffRow[] {
36
42
  let i = 0
37
43
  let j = 0
38
44
  while (i < m && j < n) {
39
- if (left[i] === right[j]) {
40
- rows.push({ kind: 'eq', left: left[i], right: right[j] })
45
+ const l = left[i] ?? ''
46
+ const r = right[j] ?? ''
47
+ if (l === r) {
48
+ rows.push({ kind: 'eq', left: l, right: r })
41
49
  i += 1
42
50
  j += 1
43
- } else if (dp[i + 1][j] >= dp[i][j + 1]) {
44
- rows.push({ kind: 'del', left: left[i], right: '' })
51
+ } else if (cell(i + 1, j) >= cell(i, j + 1)) {
52
+ rows.push({ kind: 'del', left: l, right: '' })
45
53
  i += 1
46
54
  } else {
47
- rows.push({ kind: 'add', left: '', right: right[j] })
55
+ rows.push({ kind: 'add', left: '', right: r })
48
56
  j += 1
49
57
  }
50
58
  }
51
59
  while (i < m) {
52
- rows.push({ kind: 'del', left: left[i], right: '' })
60
+ rows.push({ kind: 'del', left: left[i] ?? '', right: '' })
53
61
  i += 1
54
62
  }
55
63
  while (j < n) {
56
- rows.push({ kind: 'add', left: '', right: right[j] })
64
+ rows.push({ kind: 'add', left: '', right: right[j] ?? '' })
57
65
  j += 1
58
66
  }
59
67
  return rows
@@ -62,8 +70,8 @@ function lineDiff(a: string, b: string): DiffRow[] {
62
70
  const rows = computed(() => lineDiff(props.left || '', props.right || ''))
63
71
 
64
72
  const summary = computed(() => {
65
- const adds = rows.value.filter((r) => r.kind === 'add').length
66
- const dels = rows.value.filter((r) => r.kind === 'del').length
73
+ const adds = rows.value.filter(r => r.kind === 'add').length
74
+ const dels = rows.value.filter(r => r.kind === 'del').length
67
75
  return { adds, dels }
68
76
  })
69
77
  </script>
@@ -90,9 +98,11 @@ const summary = computed(() => {
90
98
  :class="[
91
99
  'px-3 py-1 whitespace-pre-wrap break-words border-b border-default/50 min-h-[1.5rem]',
92
100
  row.kind === 'del' ? 'bg-rose-500/10 text-rose-600 dark:text-rose-400' : '',
93
- row.kind === 'add' ? 'bg-muted/10' : '',
101
+ row.kind === 'add' ? 'bg-muted/10' : ''
94
102
  ]"
95
- >{{ row.left || '·' }}</div>
103
+ >
104
+ {{ row.left || '·' }}
105
+ </div>
96
106
  </div>
97
107
  <div class="border-l border-default">
98
108
  <div
@@ -101,9 +111,11 @@ const summary = computed(() => {
101
111
  :class="[
102
112
  'px-3 py-1 whitespace-pre-wrap break-words border-b border-default/50 min-h-[1.5rem]',
103
113
  row.kind === 'add' ? 'bg-emerald-500/10 text-emerald-600 dark:text-emerald-400' : '',
104
- row.kind === 'del' ? 'bg-muted/10' : '',
114
+ row.kind === 'del' ? 'bg-muted/10' : ''
105
115
  ]"
106
- >{{ row.right || '·' }}</div>
116
+ >
117
+ {{ row.right || '·' }}
118
+ </div>
107
119
  </div>
108
120
  </div>
109
121
  </div>
@@ -8,7 +8,7 @@ import { createSharedComposable } from '@vueuse/core'
8
8
 
9
9
  export interface ActivityEvent {
10
10
  id: string
11
- ts: string // ISO
11
+ ts: string // ISO
12
12
  kind: 'success' | 'warning' | 'error' | 'info'
13
13
  title: string
14
14
  description?: string
@@ -24,7 +24,7 @@ const _useActivityFeed = () => {
24
24
  const loaded = useState<boolean>('activityFeedLoaded', () => false)
25
25
  // PR94a v3.47.0 — unread count is now derived from `read: false`.
26
26
  const unreadCount = computed(
27
- () => events.value.filter((e) => !e.read).length,
27
+ () => events.value.filter(e => !e.read).length
28
28
  )
29
29
 
30
30
  function _persist() {
@@ -91,7 +91,7 @@ const _useActivityFeed = () => {
91
91
  }
92
92
 
93
93
  function remove(id: string) {
94
- events.value = events.value.filter((e) => e.id !== id)
94
+ events.value = events.value.filter(e => e.id !== id)
95
95
  _persist()
96
96
  }
97
97
 
@@ -1,8 +1,10 @@
1
1
  export const useApi = () => {
2
2
  const apiBase = useRuntimeConfig().public.apiBase || 'http://localhost:3334'
3
3
 
4
- const fetchApi = <T>(path: string, opts?: Record<string, any>) =>
5
- useFetch<T>(`${apiBase}${path}`, { ...opts })
4
+ // path may be a plain string or a getter — useFetch accepts a getter URL
5
+ // and refetches when its reactive deps change (compare pages rely on it).
6
+ const fetchApi = <T>(path: MaybeRefOrGetter<string>, opts?: Record<string, unknown>) =>
7
+ useFetch<T>(() => `${apiBase}${toValue(path)}`, { ...opts })
6
8
 
7
9
  return { fetchApi, apiBase }
8
10
  }
@@ -29,7 +29,7 @@ export const useConfirmDialog = () => {
29
29
  return async (options: ConfirmDialogOptions): Promise<boolean> => {
30
30
  const modal = overlay.create(ConfirmDialog, {
31
31
  destroyOnClose: true,
32
- props: options,
32
+ props: options
33
33
  })
34
34
 
35
35
  const result = await modal.open()
@@ -16,7 +16,7 @@ const _useFavorites = () => {
16
16
  const toast = useToast()
17
17
  const state = useState<FavoritesPayload>('favorites', () => ({
18
18
  agents: [],
19
- personas: [],
19
+ personas: []
20
20
  }))
21
21
  const loaded = useState<boolean>('favoritesLoaded', () => false)
22
22
 
@@ -26,7 +26,7 @@ const _useFavorites = () => {
26
26
  const res = await $fetch<FavoritesPayload>(`${apiBase}/api/favorites`)
27
27
  state.value = {
28
28
  agents: res.agents ?? [],
29
- personas: res.personas ?? [],
29
+ personas: res.personas ?? []
30
30
  }
31
31
  loaded.value = true
32
32
  } catch {
@@ -47,7 +47,7 @@ const _useFavorites = () => {
47
47
  async function setMany(
48
48
  kind: 'agents' | 'personas',
49
49
  ids: string[],
50
- favorited: boolean,
50
+ favorited: boolean
51
51
  ) {
52
52
  try {
53
53
  const res = await $fetch<{
@@ -56,7 +56,7 @@ const _useFavorites = () => {
56
56
  error?: string
57
57
  }>(`${apiBase}/api/favorites/bulk`, {
58
58
  method: 'POST',
59
- body: { kind, ids, favorited },
59
+ body: { kind, ids, favorited }
60
60
  })
61
61
  if (res.error) throw new Error(res.error)
62
62
  // Sync local state with the new server truth.
@@ -66,7 +66,7 @@ const _useFavorites = () => {
66
66
  toast.add({
67
67
  title: 'Favorites bulk update failed',
68
68
  description: err instanceof Error ? err.message : 'unknown error',
69
- color: 'error',
69
+ color: 'error'
70
70
  })
71
71
  return null
72
72
  }
@@ -76,21 +76,21 @@ const _useFavorites = () => {
76
76
  try {
77
77
  const res = await $fetch<{ favorited?: boolean, error?: string }>(
78
78
  `${apiBase}/api/favorites/${kind}/${id}`,
79
- { method: 'POST' },
79
+ { method: 'POST' }
80
80
  )
81
81
  if (res.error) throw new Error(res.error)
82
82
  const bucket = state.value[kind]
83
83
  if (res.favorited && !bucket.includes(id)) {
84
84
  state.value = { ...state.value, [kind]: [...bucket, id] }
85
85
  } else if (!res.favorited) {
86
- state.value = { ...state.value, [kind]: bucket.filter((x) => x !== id) }
86
+ state.value = { ...state.value, [kind]: bucket.filter(x => x !== id) }
87
87
  }
88
88
  return res.favorited
89
89
  } catch (err) {
90
90
  toast.add({
91
91
  title: 'Favorite toggle failed',
92
92
  description: err instanceof Error ? err.message : 'unknown error',
93
- color: 'error',
93
+ color: 'error'
94
94
  })
95
95
  return null
96
96
  }
@@ -50,9 +50,9 @@ export const TERMINAL_THEMES: Record<string, XtermTheme> = {
50
50
  brightBlue: '#bae6fd',
51
51
  brightMagenta: '#f5d0fe',
52
52
  brightCyan: '#a5f3fc',
53
- brightWhite: '#fafafa',
53
+ brightWhite: '#fafafa'
54
54
  },
55
- dracula: {
55
+ 'dracula': {
56
56
  background: '#282a36',
57
57
  foreground: '#f8f8f2',
58
58
  cursor: '#f8f8f2',
@@ -73,7 +73,7 @@ export const TERMINAL_THEMES: Record<string, XtermTheme> = {
73
73
  brightBlue: '#d6acff',
74
74
  brightMagenta: '#ff92df',
75
75
  brightCyan: '#a4ffff',
76
- brightWhite: '#ffffff',
76
+ brightWhite: '#ffffff'
77
77
  },
78
78
  'solarized-dark': {
79
79
  background: '#002b36',
@@ -96,7 +96,7 @@ export const TERMINAL_THEMES: Record<string, XtermTheme> = {
96
96
  brightBlue: '#839496',
97
97
  brightMagenta: '#6c71c4',
98
98
  brightCyan: '#93a1a1',
99
- brightWhite: '#fdf6e3',
99
+ brightWhite: '#fdf6e3'
100
100
  },
101
101
  'solarized-light': {
102
102
  background: '#fdf6e3',
@@ -119,9 +119,9 @@ export const TERMINAL_THEMES: Record<string, XtermTheme> = {
119
119
  brightBlue: '#839496',
120
120
  brightMagenta: '#6c71c4',
121
121
  brightCyan: '#93a1a1',
122
- brightWhite: '#fdf6e3',
122
+ brightWhite: '#fdf6e3'
123
123
  },
124
- nord: {
124
+ 'nord': {
125
125
  background: '#2e3440',
126
126
  foreground: '#d8dee9',
127
127
  cursor: '#d8dee9',
@@ -142,16 +142,16 @@ export const TERMINAL_THEMES: Record<string, XtermTheme> = {
142
142
  brightBlue: '#81a1c1',
143
143
  brightMagenta: '#b48ead',
144
144
  brightCyan: '#8fbcbb',
145
- brightWhite: '#eceff4',
146
- },
145
+ brightWhite: '#eceff4'
146
+ }
147
147
  }
148
148
 
149
149
  export const THEME_LABELS: Record<string, string> = {
150
150
  'arkaos-dark': 'ArkaOS Dark',
151
- dracula: 'Dracula',
151
+ 'dracula': 'Dracula',
152
152
  'solarized-dark': 'Solarized Dark',
153
153
  'solarized-light': 'Solarized Light',
154
- nord: 'Nord',
154
+ 'nord': 'Nord'
155
155
  }
156
156
 
157
157
  const STORAGE_KEY = 'arka-terminal-theme'
@@ -174,17 +174,17 @@ export function useTerminalThemes() {
174
174
  }
175
175
 
176
176
  const activeTheme = computed<XtermTheme>(
177
- () => TERMINAL_THEMES[themeName.value] ?? TERMINAL_THEMES[DEFAULT_THEME]!,
177
+ () => TERMINAL_THEMES[themeName.value] ?? TERMINAL_THEMES[DEFAULT_THEME]!
178
178
  )
179
179
 
180
180
  const options = computed(() =>
181
- Object.entries(THEME_LABELS).map(([value, label]) => ({ value, label })),
181
+ Object.entries(THEME_LABELS).map(([value, label]) => ({ value, label }))
182
182
  )
183
183
 
184
184
  return {
185
185
  themeName,
186
186
  activeTheme,
187
187
  setTheme,
188
- options,
188
+ options
189
189
  }
190
190
  }
@@ -16,7 +16,7 @@ export const THEME_COLOR_OPTIONS = [
16
16
  { label: 'Rose', value: 'rose' },
17
17
  { label: 'Amber', value: 'amber' },
18
18
  { label: 'Teal', value: 'teal' },
19
- { label: 'Cyan', value: 'cyan' },
19
+ { label: 'Cyan', value: 'cyan' }
20
20
  ] as const
21
21
 
22
22
  export type ThemeColor = (typeof THEME_COLOR_OPTIONS)[number]['value']
@@ -27,8 +27,11 @@ const _useThemeColor = () => {
27
27
 
28
28
  function apply(color: ThemeColor) {
29
29
  current.value = color
30
- if (appConfig.ui?.colors) {
31
- ;(appConfig.ui.colors as any).primary = color
30
+ // ui.colors comes from app.config.ts and isn't part of Nuxt's typed
31
+ // AppConfig, so narrow it structurally once before writing.
32
+ const ui = appConfig.ui as { colors?: Record<string, string> } | undefined
33
+ if (ui?.colors) {
34
+ ui.colors.primary = color
32
35
  }
33
36
  }
34
37
 
@@ -42,7 +45,7 @@ const _useThemeColor = () => {
42
45
  function loadFromStorage() {
43
46
  if (typeof window === 'undefined') return
44
47
  const saved = window.localStorage.getItem(STORAGE_KEY) as ThemeColor | null
45
- if (saved && THEME_COLOR_OPTIONS.some((o) => o.value === saved)) {
48
+ if (saved && THEME_COLOR_OPTIONS.some(o => o.value === saved)) {
46
49
  apply(saved)
47
50
  }
48
51
  }