@cat-factory/app 0.19.0 → 0.20.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.
|
@@ -15,6 +15,8 @@ const tasks = useTasksStore()
|
|
|
15
15
|
const tracker = useTrackerStore()
|
|
16
16
|
const releaseHealth = useReleaseHealthStore()
|
|
17
17
|
const userSecrets = useUserSecretsStore()
|
|
18
|
+
const apiKeys = useApiKeysStore()
|
|
19
|
+
const workspace = useWorkspaceStore()
|
|
18
20
|
|
|
19
21
|
// The selected filing tracker, as a badge label ("GitHub Issues" / "Jira").
|
|
20
22
|
const trackerLabel = computed(() => {
|
|
@@ -31,6 +33,8 @@ watch(
|
|
|
31
33
|
if (isOpen) {
|
|
32
34
|
void releaseHealth.ensureLoaded().catch(() => {})
|
|
33
35
|
void userSecrets.load().catch(() => {})
|
|
36
|
+
// Drives the OpenRouter row's "Key connected" badge.
|
|
37
|
+
if (workspace.workspaceId) void apiKeys.load(workspace.workspaceId).catch(() => {})
|
|
34
38
|
}
|
|
35
39
|
},
|
|
36
40
|
)
|
|
@@ -66,6 +70,38 @@ function go(fn: () => void) {
|
|
|
66
70
|
const groups = computed<IntegrationGroup[]>(() => {
|
|
67
71
|
const out: IntegrationGroup[] = []
|
|
68
72
|
|
|
73
|
+
// --- Models & providers ----------------------------------------------------
|
|
74
|
+
// Top of the hub: an OpenRouter key is the fastest path to 300+ models, so it leads.
|
|
75
|
+
const openRouterKeyConnected = apiKeys.configuredProviders.has('openrouter')
|
|
76
|
+
out.push({
|
|
77
|
+
title: 'Models & providers',
|
|
78
|
+
items: [
|
|
79
|
+
{
|
|
80
|
+
key: 'openrouter',
|
|
81
|
+
icon: 'i-lucide-waypoints',
|
|
82
|
+
label: 'OpenRouter',
|
|
83
|
+
description: 'One gateway to 300+ models — add your key and enable models in one place.',
|
|
84
|
+
status: openRouterKeyConnected ? 'Key connected' : undefined,
|
|
85
|
+
connected: openRouterKeyConnected,
|
|
86
|
+
onClick: () => go(ui.openOpenRouter),
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
key: 'vendors',
|
|
90
|
+
icon: 'i-lucide-key-round',
|
|
91
|
+
label: 'Vendors & keys',
|
|
92
|
+
description: 'LLM vendor subscriptions and provider API keys.',
|
|
93
|
+
onClick: () => go(ui.openVendorCredentials),
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
key: 'local-runners',
|
|
97
|
+
icon: 'i-lucide-server',
|
|
98
|
+
label: 'My local runners',
|
|
99
|
+
description: 'Your own-machine model runners (Ollama, LM Studio, vLLM…).',
|
|
100
|
+
onClick: () => go(ui.openLocalModels),
|
|
101
|
+
},
|
|
102
|
+
],
|
|
103
|
+
})
|
|
104
|
+
|
|
69
105
|
// --- Source control --------------------------------------------------------
|
|
70
106
|
const code: IntegrationItem[] = []
|
|
71
107
|
if (github.available) {
|
|
@@ -188,34 +224,6 @@ const groups = computed<IntegrationGroup[]>(() => {
|
|
|
188
224
|
})
|
|
189
225
|
}
|
|
190
226
|
|
|
191
|
-
// --- Models & providers ----------------------------------------------------
|
|
192
|
-
out.push({
|
|
193
|
-
title: 'Models & providers',
|
|
194
|
-
items: [
|
|
195
|
-
{
|
|
196
|
-
key: 'vendors',
|
|
197
|
-
icon: 'i-lucide-key-round',
|
|
198
|
-
label: 'Vendors & keys',
|
|
199
|
-
description: 'LLM vendor subscriptions and provider API keys.',
|
|
200
|
-
onClick: () => go(ui.openVendorCredentials),
|
|
201
|
-
},
|
|
202
|
-
{
|
|
203
|
-
key: 'local-runners',
|
|
204
|
-
icon: 'i-lucide-server',
|
|
205
|
-
label: 'My local runners',
|
|
206
|
-
description: 'Your own-machine model runners (Ollama, LM Studio, vLLM…).',
|
|
207
|
-
onClick: () => go(ui.openLocalModels),
|
|
208
|
-
},
|
|
209
|
-
{
|
|
210
|
-
key: 'openrouter',
|
|
211
|
-
icon: 'i-lucide-waypoints',
|
|
212
|
-
label: 'OpenRouter models',
|
|
213
|
-
description: 'Browse and enable models from the OpenRouter gateway.',
|
|
214
|
-
onClick: () => go(ui.openOpenRouter),
|
|
215
|
-
},
|
|
216
|
-
],
|
|
217
|
-
})
|
|
218
|
-
|
|
219
227
|
return out
|
|
220
228
|
})
|
|
221
229
|
</script>
|
|
@@ -1,15 +1,20 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
2
|
+
// "OpenRouter" — the one-stop OpenRouter setup panel. OpenRouter is a single gateway to
|
|
3
|
+
// 300+ models reached via the workspace's API-key pool, so this panel owns the whole flow:
|
|
4
|
+
// 1. Connect an OpenRouter key inline (no need to detour through Vendors & keys).
|
|
5
|
+
// 2. The live catalog auto-refreshes as soon as a key exists.
|
|
6
|
+
// 3. Tick models (or one-click "Enable recommended") and Save.
|
|
7
|
+
// Enabled models — with their context window and price — appear in the model picker and
|
|
8
|
+
// meter against the spend budget. The Vendors & keys → Proxies tab remains a valid second
|
|
9
|
+
// entry point for the key; this panel just makes OpenRouter self-sufficient.
|
|
7
10
|
import { computed, ref, watch } from 'vue'
|
|
8
11
|
import type { OpenRouterModelMeta } from '~/types/openrouter'
|
|
9
12
|
|
|
10
13
|
const ui = useUiStore()
|
|
11
14
|
const workspace = useWorkspaceStore()
|
|
12
15
|
const store = useOpenRouterStore()
|
|
16
|
+
const apiKeys = useApiKeysStore()
|
|
17
|
+
const models = useModelsStore()
|
|
13
18
|
const toast = useToast()
|
|
14
19
|
|
|
15
20
|
const open = computed({
|
|
@@ -17,16 +22,39 @@ const open = computed({
|
|
|
17
22
|
set: (v: boolean) => (v ? ui.openOpenRouter() : ui.closeOpenRouter()),
|
|
18
23
|
})
|
|
19
24
|
|
|
25
|
+
// Popular slugs offered by "Enable recommended" — these mirror the curated `openrouter`
|
|
26
|
+
// refs in the backend MODEL_CATALOG. Only the ones present in the live browse list are
|
|
27
|
+
// ticked, so a recommendation never enables a slug OpenRouter doesn't actually serve.
|
|
28
|
+
const RECOMMENDED_SLUGS = [
|
|
29
|
+
'anthropic/claude-opus-4.8',
|
|
30
|
+
'openai/gpt-5.5',
|
|
31
|
+
'google/gemini-3-pro',
|
|
32
|
+
'deepseek/deepseek-chat',
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
// Whether the workspace/user has an OpenRouter key connected at any reachable scope.
|
|
36
|
+
const keyConnected = computed(() => apiKeys.configuredProviders.has('openrouter'))
|
|
37
|
+
|
|
20
38
|
// The enabled slugs the user has ticked (seeded from the persisted catalog on open).
|
|
21
39
|
const selected = ref<Set<string>>(new Set())
|
|
22
40
|
const filter = ref('')
|
|
23
41
|
const busy = ref(false)
|
|
24
42
|
|
|
25
|
-
//
|
|
43
|
+
// Inline key-entry form state (shown until an OpenRouter key is connected).
|
|
44
|
+
const keyScope = ref<'workspace' | 'user'>('workspace')
|
|
45
|
+
const keyLabel = ref('')
|
|
46
|
+
const keyValue = ref('')
|
|
47
|
+
const connectingKey = ref(false)
|
|
48
|
+
|
|
49
|
+
// Load key state + persisted catalog whenever the panel opens; seed the tick selection,
|
|
50
|
+
// then auto-refresh the live catalog if a key is already connected (no extra click).
|
|
26
51
|
watch(open, (isOpen) => {
|
|
27
52
|
if (!isOpen || !workspace.workspaceId) return
|
|
28
|
-
|
|
53
|
+
const ws = workspace.workspaceId
|
|
54
|
+
void apiKeys.load(ws).catch(() => {})
|
|
55
|
+
void store.load(ws).then(() => {
|
|
29
56
|
selected.value = new Set(store.enabled.map((m) => m.id))
|
|
57
|
+
if (keyConnected.value && store.browse.length === 0) void refresh()
|
|
30
58
|
})
|
|
31
59
|
})
|
|
32
60
|
|
|
@@ -45,6 +73,12 @@ const visible = computed(() => {
|
|
|
45
73
|
|
|
46
74
|
const selectedCount = computed(() => selected.value.size)
|
|
47
75
|
|
|
76
|
+
// Recommended slugs that are actually available in the current browse/enabled list.
|
|
77
|
+
const recommendedAvailable = computed(() => {
|
|
78
|
+
const ids = new Set(source.value.map((m) => m.id))
|
|
79
|
+
return RECOMMENDED_SLUGS.filter((slug) => ids.has(slug))
|
|
80
|
+
})
|
|
81
|
+
|
|
48
82
|
function contextLabel(tokens: number | undefined): string {
|
|
49
83
|
if (!tokens) return ''
|
|
50
84
|
return tokens >= 1000 ? `${Math.round(tokens / 1000)}K ctx` : `${tokens} ctx`
|
|
@@ -61,13 +95,47 @@ function toggle(id: string, on: boolean) {
|
|
|
61
95
|
selected.value = next
|
|
62
96
|
}
|
|
63
97
|
|
|
98
|
+
function enableRecommended() {
|
|
99
|
+
const next = new Set(selected.value)
|
|
100
|
+
for (const slug of recommendedAvailable.value) next.add(slug)
|
|
101
|
+
selected.value = next
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function connectKey() {
|
|
105
|
+
if (!keyValue.value.trim() || !workspace.workspaceId) return
|
|
106
|
+
connectingKey.value = true
|
|
107
|
+
try {
|
|
108
|
+
const input = {
|
|
109
|
+
provider: 'openrouter' as const,
|
|
110
|
+
label: keyLabel.value.trim() || 'openrouter key',
|
|
111
|
+
key: keyValue.value.trim(),
|
|
112
|
+
}
|
|
113
|
+
if (keyScope.value === 'workspace') await apiKeys.addWorkspaceKey(input)
|
|
114
|
+
else await apiKeys.addUserKey(input)
|
|
115
|
+
keyValue.value = ''
|
|
116
|
+
keyLabel.value = ''
|
|
117
|
+
toast.add({ title: 'OpenRouter key connected', icon: 'i-lucide-check', color: 'success' })
|
|
118
|
+
// Now that a key exists, load the live catalog automatically.
|
|
119
|
+
await refresh()
|
|
120
|
+
} catch (e) {
|
|
121
|
+
toast.add({
|
|
122
|
+
title: 'Could not connect key',
|
|
123
|
+
description: e instanceof Error ? e.message : String(e),
|
|
124
|
+
icon: 'i-lucide-triangle-alert',
|
|
125
|
+
color: 'error',
|
|
126
|
+
})
|
|
127
|
+
} finally {
|
|
128
|
+
connectingKey.value = false
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
64
132
|
async function refresh() {
|
|
65
133
|
if (!workspace.workspaceId) return
|
|
66
134
|
const result = await store.refresh(workspace.workspaceId)
|
|
67
135
|
if (!result.reachable) {
|
|
68
136
|
toast.add({
|
|
69
137
|
title: 'Could not reach OpenRouter',
|
|
70
|
-
description: store.refreshError ?? 'Connect an OpenRouter key
|
|
138
|
+
description: store.refreshError ?? 'Connect an OpenRouter key first.',
|
|
71
139
|
icon: 'i-lucide-triangle-alert',
|
|
72
140
|
color: 'error',
|
|
73
141
|
})
|
|
@@ -80,10 +148,12 @@ async function save() {
|
|
|
80
148
|
try {
|
|
81
149
|
// Persist the ticked models, carrying the metadata from whichever list they came from.
|
|
82
150
|
const byId = new Map(source.value.map((m) => [m.id, m]))
|
|
83
|
-
const
|
|
151
|
+
const models2 = [...selected.value]
|
|
84
152
|
.map((id) => byId.get(id))
|
|
85
153
|
.filter((m): m is OpenRouterModelMeta => !!m)
|
|
86
|
-
await store.save(workspace.workspaceId,
|
|
154
|
+
await store.save(workspace.workspaceId, models2)
|
|
155
|
+
// Reflect newly-enabled models in the picker immediately.
|
|
156
|
+
await models.refresh(workspace.workspaceId)
|
|
87
157
|
toast.add({ title: 'OpenRouter catalog saved', icon: 'i-lucide-check', color: 'success' })
|
|
88
158
|
} catch (e) {
|
|
89
159
|
toast.add({
|
|
@@ -96,80 +166,171 @@ async function save() {
|
|
|
96
166
|
busy.value = false
|
|
97
167
|
}
|
|
98
168
|
}
|
|
169
|
+
|
|
170
|
+
function manageKeys() {
|
|
171
|
+
ui.closeOpenRouter()
|
|
172
|
+
ui.openVendorCredentials()
|
|
173
|
+
}
|
|
99
174
|
</script>
|
|
100
175
|
|
|
101
176
|
<template>
|
|
102
|
-
<UModal v-model:open="open" title="OpenRouter
|
|
177
|
+
<UModal v-model:open="open" title="OpenRouter" :ui="{ content: 'max-w-2xl' }">
|
|
103
178
|
<template #body>
|
|
104
179
|
<div class="space-y-4">
|
|
105
180
|
<p class="text-xs text-slate-400">
|
|
106
|
-
Reach <strong>300+ models</strong> through one gateway.
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
models you want. Enabled models appear in the model picker with their context window and
|
|
110
|
-
price, and meter against your spend budget.
|
|
181
|
+
Reach <strong>300+ models</strong> through one gateway. Add your OpenRouter key below,
|
|
182
|
+
then enable the models you want — they appear in the model picker with their context
|
|
183
|
+
window and price, and meter against your spend budget.
|
|
111
184
|
</p>
|
|
112
185
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
>
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
186
|
+
<!-- Step 1: connect a key (inline) — hidden once a key is connected -->
|
|
187
|
+
<div
|
|
188
|
+
v-if="!keyConnected"
|
|
189
|
+
class="space-y-3 rounded-lg border border-slate-700 bg-slate-900/60 p-4"
|
|
190
|
+
>
|
|
191
|
+
<h4 class="text-xs font-semibold uppercase tracking-wide text-slate-500">
|
|
192
|
+
Connect your OpenRouter key
|
|
193
|
+
</h4>
|
|
194
|
+
<ol class="list-decimal space-y-1 pl-5 text-sm text-slate-300">
|
|
195
|
+
<li>
|
|
196
|
+
Open
|
|
197
|
+
<a
|
|
198
|
+
href="https://openrouter.ai/keys"
|
|
199
|
+
target="_blank"
|
|
200
|
+
rel="noopener noreferrer"
|
|
201
|
+
class="text-primary-400 underline"
|
|
202
|
+
>openrouter.ai → Keys ↗</a
|
|
203
|
+
>
|
|
204
|
+
and create an API key.
|
|
205
|
+
</li>
|
|
206
|
+
<li>
|
|
207
|
+
Copy the key (starts with <span class="font-mono">sk-or-…</span>) and paste it below.
|
|
208
|
+
</li>
|
|
209
|
+
</ol>
|
|
210
|
+
<div class="flex flex-wrap items-end gap-3">
|
|
211
|
+
<UFormField label="Scope">
|
|
212
|
+
<USelect
|
|
213
|
+
v-model="keyScope"
|
|
214
|
+
:items="[
|
|
215
|
+
{ label: 'This workspace', value: 'workspace' },
|
|
216
|
+
{ label: 'My keys (only me)', value: 'user' },
|
|
217
|
+
]"
|
|
218
|
+
class="w-48"
|
|
219
|
+
/>
|
|
220
|
+
</UFormField>
|
|
221
|
+
<UFormField label="Label (optional)" class="flex-1">
|
|
222
|
+
<UInput v-model="keyLabel" placeholder="e.g. team key" />
|
|
223
|
+
</UFormField>
|
|
224
|
+
</div>
|
|
225
|
+
<UFormField label="API key">
|
|
226
|
+
<UTextarea
|
|
227
|
+
v-model="keyValue"
|
|
228
|
+
:rows="2"
|
|
229
|
+
placeholder="paste your OpenRouter key (sk-or-…)"
|
|
230
|
+
class="font-mono"
|
|
144
231
|
/>
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
232
|
+
</UFormField>
|
|
233
|
+
<div class="flex justify-end">
|
|
234
|
+
<UButton
|
|
235
|
+
:loading="connectingKey"
|
|
236
|
+
:disabled="!keyValue.trim()"
|
|
237
|
+
icon="i-lucide-plus"
|
|
238
|
+
@click="connectKey()"
|
|
239
|
+
>
|
|
240
|
+
Connect & browse
|
|
241
|
+
</UButton>
|
|
242
|
+
</div>
|
|
154
243
|
</div>
|
|
155
|
-
<p v-else class="text-xs text-slate-500">
|
|
156
|
-
No models yet — hit <span class="text-slate-300">Refresh catalog</span> to load
|
|
157
|
-
OpenRouter's live list.
|
|
158
|
-
</p>
|
|
159
244
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
>
|
|
170
|
-
|
|
245
|
+
<!-- Key connected status -->
|
|
246
|
+
<div
|
|
247
|
+
v-else
|
|
248
|
+
class="flex items-center justify-between rounded-lg border border-slate-700 bg-slate-900/60 px-3 py-2 text-sm"
|
|
249
|
+
>
|
|
250
|
+
<span class="flex items-center gap-2 text-slate-300">
|
|
251
|
+
<UIcon name="i-lucide-check-circle" class="h-4 w-4 text-emerald-400" />
|
|
252
|
+
OpenRouter key connected
|
|
253
|
+
</span>
|
|
254
|
+
<UButton color="neutral" variant="ghost" size="xs" @click="manageKeys()">
|
|
255
|
+
Manage in Vendors & keys
|
|
171
256
|
</UButton>
|
|
172
257
|
</div>
|
|
258
|
+
|
|
259
|
+
<!-- Step 2: browse + enable models (only once a key exists) -->
|
|
260
|
+
<template v-if="keyConnected">
|
|
261
|
+
<div class="flex items-center gap-2">
|
|
262
|
+
<UButton
|
|
263
|
+
color="neutral"
|
|
264
|
+
variant="soft"
|
|
265
|
+
size="sm"
|
|
266
|
+
icon="i-lucide-refresh-cw"
|
|
267
|
+
:loading="store.refreshing"
|
|
268
|
+
@click="refresh()"
|
|
269
|
+
>
|
|
270
|
+
Refresh catalog
|
|
271
|
+
</UButton>
|
|
272
|
+
<UButton
|
|
273
|
+
v-if="recommendedAvailable.length"
|
|
274
|
+
color="primary"
|
|
275
|
+
variant="soft"
|
|
276
|
+
size="sm"
|
|
277
|
+
icon="i-lucide-sparkles"
|
|
278
|
+
@click="enableRecommended()"
|
|
279
|
+
>
|
|
280
|
+
Enable recommended
|
|
281
|
+
</UButton>
|
|
282
|
+
<UInput
|
|
283
|
+
v-model="filter"
|
|
284
|
+
size="sm"
|
|
285
|
+
class="flex-1"
|
|
286
|
+
icon="i-lucide-search"
|
|
287
|
+
placeholder="Filter by name or slug…"
|
|
288
|
+
/>
|
|
289
|
+
</div>
|
|
290
|
+
|
|
291
|
+
<p v-if="store.refreshError" class="text-xs text-rose-400">{{ store.refreshError }}</p>
|
|
292
|
+
|
|
293
|
+
<div v-if="visible.length" class="max-h-96 space-y-1 overflow-y-auto pr-1">
|
|
294
|
+
<label
|
|
295
|
+
v-for="m in visible"
|
|
296
|
+
:key="m.id"
|
|
297
|
+
class="flex items-center gap-2 rounded-md border border-slate-800 bg-slate-900/40 px-2.5 py-1.5 text-sm"
|
|
298
|
+
>
|
|
299
|
+
<UCheckbox
|
|
300
|
+
:model-value="selected.has(m.id)"
|
|
301
|
+
@update:model-value="(v: boolean | 'indeterminate') => toggle(m.id, v === true)"
|
|
302
|
+
/>
|
|
303
|
+
<span class="min-w-0 flex-1">
|
|
304
|
+
<span class="block truncate text-slate-200">{{ m.name }}</span>
|
|
305
|
+
<span class="block truncate font-mono text-[11px] text-slate-500">{{ m.id }}</span>
|
|
306
|
+
</span>
|
|
307
|
+
<span class="shrink-0 text-right text-[11px] text-slate-500">
|
|
308
|
+
<span v-if="m.contextLength" class="block">{{
|
|
309
|
+
contextLabel(m.contextLength)
|
|
310
|
+
}}</span>
|
|
311
|
+
<span class="block">{{ priceLabel(m) }}</span>
|
|
312
|
+
</span>
|
|
313
|
+
</label>
|
|
314
|
+
</div>
|
|
315
|
+
<p v-else class="text-xs text-slate-500">
|
|
316
|
+
No models yet — hit <span class="text-slate-300">Refresh catalog</span> to load
|
|
317
|
+
OpenRouter's live list.
|
|
318
|
+
</p>
|
|
319
|
+
|
|
320
|
+
<div class="flex items-center justify-between">
|
|
321
|
+
<span class="text-xs text-slate-500">{{ selectedCount }} enabled</span>
|
|
322
|
+
<UButton
|
|
323
|
+
color="primary"
|
|
324
|
+
variant="soft"
|
|
325
|
+
size="sm"
|
|
326
|
+
icon="i-lucide-save"
|
|
327
|
+
:loading="busy"
|
|
328
|
+
@click="save()"
|
|
329
|
+
>
|
|
330
|
+
Save
|
|
331
|
+
</UButton>
|
|
332
|
+
</div>
|
|
333
|
+
</template>
|
|
173
334
|
</div>
|
|
174
335
|
</template>
|
|
175
336
|
</UModal>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cat-factory/app",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.20.0",
|
|
4
4
|
"description": "Reusable Nuxt layer for the Agent Architecture Board SPA (components, stores, composables, pages). Consume it from a thin deployment app via `extends: ['@cat-factory/app']` and point it at your backend with NUXT_PUBLIC_API_BASE. See deploy/frontend for an example.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|