@ouraihub/aether-ui 0.0.0-stage → 0.1.1
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/LICENSE +789 -0
- package/fixtures/model-classify.json +42 -0
- package/fixtures/pricing-cost.json +56 -0
- package/fixtures/reasoning-infer.json +44 -0
- package/fixtures/sse/anthropic-messages/chinese.sse +24 -0
- package/fixtures/sse/anthropic-messages/multi-tool.sse +33 -0
- package/fixtures/sse/anthropic-messages/single-tool.sse +39 -0
- package/fixtures/sse/anthropic-messages/text.sse +27 -0
- package/fixtures/sse/anthropic-messages/truncated.sse +21 -0
- package/fixtures/sse/anthropic-messages/upstream-error.sse +12 -0
- package/fixtures/sse/expect.json +20 -0
- package/fixtures/sse/openai-chat/chinese.sse +14 -0
- package/fixtures/sse/openai-chat/multi-tool.sse +18 -0
- package/fixtures/sse/openai-chat/single-tool.sse +22 -0
- package/fixtures/sse/openai-chat/text.sse +14 -0
- package/fixtures/sse/openai-chat/truncated.sse +12 -0
- package/fixtures/sse/openai-chat/upstream-error.sse +6 -0
- package/fixtures/sse/openai-responses/chinese.sse +33 -0
- package/fixtures/sse/openai-responses/multi-tool.sse +36 -0
- package/fixtures/sse/openai-responses/single-tool.sse +27 -0
- package/fixtures/sse/openai-responses/text.sse +33 -0
- package/fixtures/sse/openai-responses/truncated.sse +30 -0
- package/fixtures/sse/openai-responses/upstream-error.sse +9 -0
- package/package.json +71 -3
- package/src/CopyInput.svelte +33 -0
- package/src/KitNotifier.svelte +33 -0
- package/src/chat/ApprovalCard.svelte +29 -0
- package/src/chat/ChatComposer.svelte +277 -0
- package/src/chat/ChatMessage.svelte +262 -0
- package/src/chat/ChatPanel.css +386 -0
- package/src/chat/ChatPanel.svelte +496 -0
- package/src/chat/ChatPanelApprovals.svelte +28 -0
- package/src/chat/ChatPanelComposer.svelte +317 -0
- package/src/chat/ChatPanelDialogs.svelte +242 -0
- package/src/chat/ChatPanelHeader.svelte +132 -0
- package/src/chat/ChatPanelMessages.svelte +166 -0
- package/src/chat/ChatPanelMode.svelte +190 -0
- package/src/chat/ChatPanelState.svelte.ts +1 -0
- package/src/chat/ChatPanelToolbar.svelte +425 -0
- package/src/chat/ChatSurface.svelte +168 -0
- package/src/chat/MessageItem.svelte +480 -0
- package/src/chat/ReasoningControl.svelte +286 -0
- package/src/chat/SkillPicker.svelte +133 -0
- package/src/chat/ToolCallItem.svelte +23 -0
- package/src/chat/UsageControl.svelte +216 -0
- package/src/chat/UsageDetails.svelte +76 -0
- package/src/chat/chat-attachments.svelte.ts +74 -0
- package/src/chat/chat-panel-history.svelte.ts +430 -0
- package/src/chat/chat-panel-props.ts +32 -0
- package/src/chat/index.ts +6 -0
- package/src/chat/run-session-history.ts +56 -0
- package/src/chat/run-session-types.ts +69 -0
- package/src/chat/run-session.svelte.ts +494 -0
- package/src/chat/test-support/chat-fixture.ts +178 -0
- package/src/chat/test-support/conversation-fixtures.ts +32 -0
- package/src/chat/test-support/run-session-fixture.ts +112 -0
- package/src/components/ui/alert/alert-action.svelte +20 -0
- package/src/components/ui/alert/alert-description.svelte +23 -0
- package/src/components/ui/alert/alert-title.svelte +23 -0
- package/src/components/ui/alert/alert.svelte +43 -0
- package/src/components/ui/alert/index.ts +17 -0
- package/src/components/ui/alert-dialog/alert-dialog-action.svelte +27 -0
- package/src/components/ui/alert-dialog/alert-dialog-cancel.svelte +27 -0
- package/src/components/ui/alert-dialog/alert-dialog-content.svelte +32 -0
- package/src/components/ui/alert-dialog/alert-dialog-description.svelte +17 -0
- package/src/components/ui/alert-dialog/alert-dialog-footer.svelte +23 -0
- package/src/components/ui/alert-dialog/alert-dialog-header.svelte +20 -0
- package/src/components/ui/alert-dialog/alert-dialog-media.svelte +20 -0
- package/src/components/ui/alert-dialog/alert-dialog-overlay.svelte +17 -0
- package/src/components/ui/alert-dialog/alert-dialog-portal.svelte +7 -0
- package/src/components/ui/alert-dialog/alert-dialog-title.svelte +17 -0
- package/src/components/ui/alert-dialog/alert-dialog-trigger.svelte +7 -0
- package/src/components/ui/alert-dialog/alert-dialog.svelte +7 -0
- package/src/components/ui/alert-dialog/index.ts +40 -0
- package/src/components/ui/button/button.svelte +81 -0
- package/src/components/ui/button/index.ts +17 -0
- package/src/components/ui/dialog/dialog-close.svelte +11 -0
- package/src/components/ui/dialog/dialog-content.svelte +48 -0
- package/src/components/ui/dialog/dialog-description.svelte +17 -0
- package/src/components/ui/dialog/dialog-footer.svelte +32 -0
- package/src/components/ui/dialog/dialog-header.svelte +20 -0
- package/src/components/ui/dialog/dialog-overlay.svelte +17 -0
- package/src/components/ui/dialog/dialog-portal.svelte +7 -0
- package/src/components/ui/dialog/dialog-title.svelte +17 -0
- package/src/components/ui/dialog/dialog-trigger.svelte +11 -0
- package/src/components/ui/dialog/dialog.svelte +7 -0
- package/src/components/ui/dialog/index.ts +34 -0
- package/src/components/ui/dropdown-menu/dropdown-menu-checkbox-group.svelte +16 -0
- package/src/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte +46 -0
- package/src/components/ui/dropdown-menu/dropdown-menu-content.svelte +31 -0
- package/src/components/ui/dropdown-menu/dropdown-menu-group-heading.svelte +22 -0
- package/src/components/ui/dropdown-menu/dropdown-menu-group.svelte +7 -0
- package/src/components/ui/dropdown-menu/dropdown-menu-item.svelte +27 -0
- package/src/components/ui/dropdown-menu/dropdown-menu-label.svelte +24 -0
- package/src/components/ui/dropdown-menu/dropdown-menu-portal.svelte +7 -0
- package/src/components/ui/dropdown-menu/dropdown-menu-radio-group.svelte +17 -0
- package/src/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte +36 -0
- package/src/components/ui/dropdown-menu/dropdown-menu-separator.svelte +17 -0
- package/src/components/ui/dropdown-menu/dropdown-menu-shortcut.svelte +20 -0
- package/src/components/ui/dropdown-menu/dropdown-menu-sub-content.svelte +31 -0
- package/src/components/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte +29 -0
- package/src/components/ui/dropdown-menu/dropdown-menu-sub.svelte +7 -0
- package/src/components/ui/dropdown-menu/dropdown-menu-trigger.svelte +7 -0
- package/src/components/ui/dropdown-menu/dropdown-menu.svelte +7 -0
- package/src/components/ui/dropdown-menu/index.ts +54 -0
- package/src/components/ui/empty/empty-content.svelte +23 -0
- package/src/components/ui/empty/empty-description.svelte +23 -0
- package/src/components/ui/empty/empty-header.svelte +20 -0
- package/src/components/ui/empty/empty-media.svelte +41 -0
- package/src/components/ui/empty/empty-title.svelte +20 -0
- package/src/components/ui/empty/empty.svelte +23 -0
- package/src/components/ui/empty/index.ts +22 -0
- package/src/components/ui/field/field-content.svelte +20 -0
- package/src/components/ui/field/field-description.svelte +25 -0
- package/src/components/ui/field/field-error.svelte +58 -0
- package/src/components/ui/field/field-group.svelte +23 -0
- package/src/components/ui/field/field-label.svelte +25 -0
- package/src/components/ui/field/field-legend.svelte +24 -0
- package/src/components/ui/field/field-separator.svelte +35 -0
- package/src/components/ui/field/field-set.svelte +20 -0
- package/src/components/ui/field/field-title.svelte +20 -0
- package/src/components/ui/field/field.svelte +47 -0
- package/src/components/ui/field/index.ts +33 -0
- package/src/components/ui/input/index.ts +7 -0
- package/src/components/ui/input/input.svelte +48 -0
- package/src/components/ui/label/index.ts +7 -0
- package/src/components/ui/label/label.svelte +20 -0
- package/src/components/ui/select/index.ts +40 -0
- package/src/components/ui/select/select-content.svelte +45 -0
- package/src/components/ui/select/select-group-heading.svelte +21 -0
- package/src/components/ui/select/select-group.svelte +17 -0
- package/src/components/ui/select/select-item.svelte +43 -0
- package/src/components/ui/select/select-label.svelte +20 -0
- package/src/components/ui/select/select-portal.svelte +7 -0
- package/src/components/ui/select/select-scroll-down-button.svelte +20 -0
- package/src/components/ui/select/select-scroll-up-button.svelte +20 -0
- package/src/components/ui/select/select-separator.svelte +18 -0
- package/src/components/ui/select/select-trigger.svelte +29 -0
- package/src/components/ui/select/select-value.svelte +7 -0
- package/src/components/ui/select/select.svelte +11 -0
- package/src/components/ui/separator/index.ts +7 -0
- package/src/components/ui/separator/separator.svelte +21 -0
- package/src/components/ui/sonner/index.ts +1 -0
- package/src/components/ui/sonner/sonner.svelte +40 -0
- package/src/components/ui/switch/index.ts +7 -0
- package/src/components/ui/switch/switch.svelte +34 -0
- package/src/components/ui/textarea/index.ts +7 -0
- package/src/components/ui/textarea/textarea.svelte +22 -0
- package/src/context.ts +34 -0
- package/src/dev/Demo.svelte +47 -0
- package/src/dev/main.ts +7 -0
- package/src/i18n/en.ts +645 -0
- package/src/i18n/index.ts +28 -0
- package/src/i18n/zh-CN.ts +610 -0
- package/src/index.ts +10 -0
- package/src/logs/LogPanel.svelte +103 -0
- package/src/logs/LogSettings.svelte +58 -0
- package/src/logs/index.ts +5 -0
- package/src/logs/logger.ts +96 -0
- package/src/markdown.ts +8 -0
- package/src/mcp/McpClientEditor.svelte +204 -0
- package/src/mcp/McpClientSettings.svelte +418 -0
- package/src/mcp/McpServerSettings.svelte +363 -0
- package/src/mcp/config.ts +26 -0
- package/src/mcp/index.ts +3 -0
- package/src/mcp/input.ts +74 -0
- package/src/notify.svelte.ts +43 -0
- package/src/ports/contract.ts +174 -0
- package/src/ports/fake-optional.ts +81 -0
- package/src/ports/fake.ts +488 -0
- package/src/ports/index.ts +7 -0
- package/src/ports/search.ts +56 -0
- package/src/ports/skill-sources.ts +45 -0
- package/src/ports/test-support/fake-fixture.ts +63 -0
- package/src/providers/EndpointKeysDialog.svelte +357 -0
- package/src/providers/EndpointModels.svelte +311 -0
- package/src/providers/EndpointPanel.svelte +266 -0
- package/src/providers/ModelPickerDialog.svelte +230 -0
- package/src/providers/ModelPricing.svelte +125 -0
- package/src/providers/ModelSelect.svelte +448 -0
- package/src/providers/ModelSettingsDialog.svelte +388 -0
- package/src/providers/ProviderDialogs.svelte +120 -0
- package/src/providers/ProviderList.svelte +159 -0
- package/src/providers/ProvidersSettings.svelte +491 -0
- package/src/providers/RequestConfigDialog.svelte +278 -0
- package/src/providers/SecretInput.svelte +284 -0
- package/src/providers/draft.ts +355 -0
- package/src/providers/edit-session.ts +170 -0
- package/src/providers/index.ts +5 -0
- package/src/providers/reasoning.ts +55 -0
- package/src/providers/revision.svelte.ts +6 -0
- package/src/providers/test-support/providers-fixture.ts +120 -0
- package/src/search/SearchAdvancedTools.svelte +182 -0
- package/src/search/SearchProviderCard.svelte +195 -0
- package/src/search/SearchSettings.svelte +500 -0
- package/src/search/index.ts +13 -0
- package/src/search/settings-flow.ts +67 -0
- package/src/session.ts +20 -0
- package/src/skills/SkillAdd.svelte +380 -0
- package/src/skills/SkillAuthoring.svelte +430 -0
- package/src/skills/SkillEditor.svelte +104 -0
- package/src/skills/SkillPreviewDialog.svelte +136 -0
- package/src/skills/SkillsSettings.svelte +147 -0
- package/src/skills/error.ts +32 -0
- package/src/skills/index.ts +4 -0
- package/src/skills/test-support/skills-fixture.ts +139 -0
- package/src/styles/kit.css +137 -0
- package/src/styles/tokens.css +22 -0
- package/src/tauri/index.ts +298 -0
- package/src/utils.ts +22 -0
- package/src/web/index.ts +441 -0
- package/README.md +0 -3
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
// External MCP server settings (docs/16 §4.4): status, start/stop, port, scope switches, tokens.
|
|
3
|
+
import { onDestroy } from 'svelte'
|
|
4
|
+
import { Alert, AlertDescription, AlertTitle } from '#lib/components/ui/alert/index.js'
|
|
5
|
+
import { Button } from '#lib/components/ui/button/index.js'
|
|
6
|
+
import * as Field from '#lib/components/ui/field/index.js'
|
|
7
|
+
import { Input } from '#lib/components/ui/input/index.js'
|
|
8
|
+
import { Switch } from '#lib/components/ui/switch/index.js'
|
|
9
|
+
import { getKitContext } from '../context'
|
|
10
|
+
import { isKitClientError } from '../ports'
|
|
11
|
+
import type { IssuedToken, ScopeState, Status, TokenRecord } from '../ports'
|
|
12
|
+
import { createClientSession } from '../session'
|
|
13
|
+
import { buildClaudePosixCommand, buildMcpJsonConfig } from './config'
|
|
14
|
+
|
|
15
|
+
/** Server key used in the copied client configs. */
|
|
16
|
+
let { serverName = 'aether' }: { serverName?: string } = $props()
|
|
17
|
+
|
|
18
|
+
const { ports, i18n, notify, logger } = getKitContext()
|
|
19
|
+
const server = ports.mcpServer
|
|
20
|
+
const t = i18n.t
|
|
21
|
+
|
|
22
|
+
let status = $state<Status | null>(null)
|
|
23
|
+
let scopes = $state<ScopeState[]>([])
|
|
24
|
+
let tokens = $state<TokenRecord[]>([])
|
|
25
|
+
let port = $state('')
|
|
26
|
+
let tokenName = $state('')
|
|
27
|
+
let tokenScopes = $state<string[]>([])
|
|
28
|
+
let issued = $state<IssuedToken | null>(null)
|
|
29
|
+
let busy = $state(false)
|
|
30
|
+
let issuing = $state(false)
|
|
31
|
+
const tokenSession = createClientSession()
|
|
32
|
+
let tokenRevision = 0
|
|
33
|
+
|
|
34
|
+
function clearIssued() {
|
|
35
|
+
tokenSession.invalidate()
|
|
36
|
+
issued = null
|
|
37
|
+
issuing = false
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
$effect(() => {
|
|
41
|
+
void serverName
|
|
42
|
+
clearIssued()
|
|
43
|
+
tokenName = ''
|
|
44
|
+
tokenScopes = []
|
|
45
|
+
tokenRevision++
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
onDestroy(() => {
|
|
49
|
+
clearIssued()
|
|
50
|
+
tokenName = ''
|
|
51
|
+
tokenScopes = []
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
function hasHttpUrl(value: string | undefined): value is string {
|
|
55
|
+
if (!value?.trim()) return false
|
|
56
|
+
try {
|
|
57
|
+
const url = new URL(value)
|
|
58
|
+
return url.protocol === 'http:' || url.protocol === 'https:'
|
|
59
|
+
} catch {
|
|
60
|
+
return false
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// JSON accepts dynamic keys; Claude CLI names must also satisfy its naming rules.
|
|
65
|
+
const configInput = $derived.by(() => {
|
|
66
|
+
const url = status?.url
|
|
67
|
+
const token = issued?.token
|
|
68
|
+
if (!hasHttpUrl(url) || !serverName.trim() || !token?.trim()) return null
|
|
69
|
+
return { serverName, url, token }
|
|
70
|
+
})
|
|
71
|
+
const claudeNameValid = $derived(/^[A-Za-z0-9_-]+$/.test(serverName))
|
|
72
|
+
|
|
73
|
+
// Actions only render when the port exists; the throw marks a host wiring bug.
|
|
74
|
+
function need() {
|
|
75
|
+
if (!server) throw new Error('mcpServer port missing')
|
|
76
|
+
return server
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function fail(error: unknown) {
|
|
80
|
+
const code = isKitClientError(error) ? error.code : 'internal'
|
|
81
|
+
logger.warn('mcp.server.ui_failed', { code })
|
|
82
|
+
notify.toast('error', t(`errors.${code}`))
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function act(run: () => Promise<void>) {
|
|
86
|
+
busy = true
|
|
87
|
+
try {
|
|
88
|
+
await run()
|
|
89
|
+
} catch (error) {
|
|
90
|
+
fail(error)
|
|
91
|
+
} finally {
|
|
92
|
+
busy = false
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function setStatus(next: Status) {
|
|
97
|
+
if (status && status.url !== next.url) clearIssued()
|
|
98
|
+
status = next
|
|
99
|
+
port = String(next.port)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function load() {
|
|
103
|
+
if (!server) return
|
|
104
|
+
const [nextStatus, nextScopes, nextTokens] = await Promise.all([
|
|
105
|
+
server.status(),
|
|
106
|
+
server.scopes(),
|
|
107
|
+
server.tokens(),
|
|
108
|
+
])
|
|
109
|
+
setStatus(nextStatus)
|
|
110
|
+
scopes = nextScopes
|
|
111
|
+
tokens = nextTokens
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
$effect(() => {
|
|
115
|
+
void act(load)
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
const toggleRunning = () =>
|
|
119
|
+
act(async () => setStatus(status?.running ? await need().stop() : await need().start()))
|
|
120
|
+
|
|
121
|
+
const savePort = () => act(async () => setStatus(await need().setPort(Number(port))))
|
|
122
|
+
|
|
123
|
+
async function toggleScope(scope: ScopeState, on: boolean) {
|
|
124
|
+
if (on && scope.destructive) {
|
|
125
|
+
const ok = await notify.confirm({
|
|
126
|
+
title: t('mcp.server.destructiveTitle'),
|
|
127
|
+
body: t('mcp.server.destructiveBody', { scope: t(scope.labelKey) }),
|
|
128
|
+
danger: true,
|
|
129
|
+
})
|
|
130
|
+
if (!ok) return
|
|
131
|
+
}
|
|
132
|
+
const ids = scopes.filter((s) => (s.id === scope.id ? on : s.enabled)).map((s) => s.id)
|
|
133
|
+
await act(async () => {
|
|
134
|
+
scopes = await need().setScopes(ids)
|
|
135
|
+
})
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function toggleTokenScope(id: string, on: boolean) {
|
|
139
|
+
tokenRevision++
|
|
140
|
+
tokenScopes = on ? [...tokenScopes, id] : tokenScopes.filter((s) => s !== id)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async function issue() {
|
|
144
|
+
clearIssued()
|
|
145
|
+
const token = tokenSession.capture()
|
|
146
|
+
const targetName = serverName
|
|
147
|
+
const targetUrl = status?.url
|
|
148
|
+
const revision = tokenRevision
|
|
149
|
+
const name = tokenName
|
|
150
|
+
const grants = [...tokenScopes]
|
|
151
|
+
const current = () =>
|
|
152
|
+
tokenSession.isCurrent(token) && targetName === serverName && targetUrl === status?.url
|
|
153
|
+
issuing = true
|
|
154
|
+
try {
|
|
155
|
+
const next = await need().issueToken(name, grants)
|
|
156
|
+
if (!current()) return
|
|
157
|
+
issued = next
|
|
158
|
+
if (revision === tokenRevision) {
|
|
159
|
+
tokenName = ''
|
|
160
|
+
tokenScopes = []
|
|
161
|
+
}
|
|
162
|
+
const nextTokens = await need().tokens()
|
|
163
|
+
if (current()) tokens = nextTokens
|
|
164
|
+
} catch (error) {
|
|
165
|
+
if (current()) fail(error)
|
|
166
|
+
} finally {
|
|
167
|
+
if (current()) issuing = false
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const revoke = (token: TokenRecord) =>
|
|
172
|
+
act(async () => {
|
|
173
|
+
const ok = await notify.confirm({
|
|
174
|
+
title: t('mcp.server.revokeTitle'),
|
|
175
|
+
body: t('mcp.server.revokeBody', { name: token.name }),
|
|
176
|
+
danger: true,
|
|
177
|
+
})
|
|
178
|
+
if (!ok) return
|
|
179
|
+
if (issued?.id === token.id) clearIssued()
|
|
180
|
+
await need().revokeToken(token.id)
|
|
181
|
+
tokens = await need().tokens()
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
async function copy(format: 'json' | 'posix') {
|
|
185
|
+
const input = configInput
|
|
186
|
+
if (!input || (format === 'posix' && !claudeNameValid)) return
|
|
187
|
+
const token = tokenSession.capture()
|
|
188
|
+
const current = () =>
|
|
189
|
+
tokenSession.isCurrent(token) &&
|
|
190
|
+
serverName === input.serverName &&
|
|
191
|
+
status?.url === input.url &&
|
|
192
|
+
issued?.token === input.token
|
|
193
|
+
const text = format === 'json' ? buildMcpJsonConfig(input) : buildClaudePosixCommand(input)
|
|
194
|
+
if (!current()) return
|
|
195
|
+
try {
|
|
196
|
+
await navigator.clipboard.writeText(text)
|
|
197
|
+
if (current()) notify.toast('success', t('mcp.server.copied'))
|
|
198
|
+
} catch (error) {
|
|
199
|
+
if (current()) fail(error)
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const date = (ms: number | null) => (ms === null ? '—' : new Date(ms).toLocaleString(i18n.locale))
|
|
204
|
+
</script>
|
|
205
|
+
|
|
206
|
+
{#if server && status}
|
|
207
|
+
<section class="flex flex-col gap-6" aria-label={t('mcp.server.title')}>
|
|
208
|
+
<Field.Group>
|
|
209
|
+
<Field.Field orientation="horizontal">
|
|
210
|
+
<Field.Content>
|
|
211
|
+
<Field.Label>{t('mcp.server.title')}</Field.Label>
|
|
212
|
+
<Field.Description>
|
|
213
|
+
{status.running
|
|
214
|
+
? t('mcp.server.running', { url: status.url })
|
|
215
|
+
: t('mcp.server.stopped')}
|
|
216
|
+
· {t('mcp.server.toolCount', { count: status.toolCount })}
|
|
217
|
+
</Field.Description>
|
|
218
|
+
</Field.Content>
|
|
219
|
+
<Button
|
|
220
|
+
disabled={busy || issuing}
|
|
221
|
+
variant={status.running ? 'outline' : 'default'}
|
|
222
|
+
onclick={() => void toggleRunning()}
|
|
223
|
+
>
|
|
224
|
+
{status.running ? t('mcp.server.stop') : t('mcp.server.start')}
|
|
225
|
+
</Button>
|
|
226
|
+
</Field.Field>
|
|
227
|
+
<Field.Field orientation="horizontal">
|
|
228
|
+
<Field.Label for="kit-mcp-port">{t('mcp.server.port')}</Field.Label>
|
|
229
|
+
<Input
|
|
230
|
+
id="kit-mcp-port"
|
|
231
|
+
class="w-32"
|
|
232
|
+
type="number"
|
|
233
|
+
min="1024"
|
|
234
|
+
max="65535"
|
|
235
|
+
bind:value={port}
|
|
236
|
+
/>
|
|
237
|
+
<Button
|
|
238
|
+
disabled={busy || issuing || port === String(status.port)}
|
|
239
|
+
variant="outline"
|
|
240
|
+
onclick={() => void savePort()}
|
|
241
|
+
>
|
|
242
|
+
{t('mcp.server.savePort')}
|
|
243
|
+
</Button>
|
|
244
|
+
</Field.Field>
|
|
245
|
+
</Field.Group>
|
|
246
|
+
|
|
247
|
+
<Field.Set>
|
|
248
|
+
<Field.Legend>{t('mcp.server.scopes')}</Field.Legend>
|
|
249
|
+
<Field.Description>{t('mcp.server.scopesHint')}</Field.Description>
|
|
250
|
+
<Field.Group>
|
|
251
|
+
{#each scopes as scope (scope.id)}
|
|
252
|
+
<Field.Field orientation="horizontal">
|
|
253
|
+
<Switch
|
|
254
|
+
id={`kit-mcp-scope-${scope.id}`}
|
|
255
|
+
bind:checked={() => scope.enabled, (on) => void toggleScope(scope, on)}
|
|
256
|
+
disabled={busy || issuing}
|
|
257
|
+
/>
|
|
258
|
+
<Field.Label for={`kit-mcp-scope-${scope.id}`}
|
|
259
|
+
>{t(scope.labelKey)} ({scope.id})</Field.Label
|
|
260
|
+
>
|
|
261
|
+
</Field.Field>
|
|
262
|
+
{/each}
|
|
263
|
+
</Field.Group>
|
|
264
|
+
</Field.Set>
|
|
265
|
+
|
|
266
|
+
<Field.Set>
|
|
267
|
+
<Field.Legend>{t('mcp.server.newToken')}</Field.Legend>
|
|
268
|
+
<Field.Group>
|
|
269
|
+
<Field.Field>
|
|
270
|
+
<Field.Label for="kit-mcp-token-name">{t('mcp.server.tokenName')}</Field.Label>
|
|
271
|
+
<Input
|
|
272
|
+
id="kit-mcp-token-name"
|
|
273
|
+
bind:value={tokenName}
|
|
274
|
+
maxlength={200}
|
|
275
|
+
oninput={() => tokenRevision++}
|
|
276
|
+
/>
|
|
277
|
+
</Field.Field>
|
|
278
|
+
{#each scopes as scope (scope.id)}
|
|
279
|
+
<Field.Field orientation="horizontal">
|
|
280
|
+
<Switch
|
|
281
|
+
id={`kit-mcp-grant-${scope.id}`}
|
|
282
|
+
checked={tokenScopes.includes(scope.id)}
|
|
283
|
+
onCheckedChange={(on) => toggleTokenScope(scope.id, on)}
|
|
284
|
+
/>
|
|
285
|
+
<Field.Label for={`kit-mcp-grant-${scope.id}`}
|
|
286
|
+
>{t(scope.labelKey)} ({scope.id})</Field.Label
|
|
287
|
+
>
|
|
288
|
+
</Field.Field>
|
|
289
|
+
{/each}
|
|
290
|
+
<Field.Field orientation="horizontal">
|
|
291
|
+
<Button
|
|
292
|
+
disabled={busy || issuing || tokenName.trim() === '' || tokenScopes.length === 0}
|
|
293
|
+
onclick={() => void issue()}
|
|
294
|
+
>
|
|
295
|
+
{t('mcp.server.issue')}
|
|
296
|
+
</Button>
|
|
297
|
+
</Field.Field>
|
|
298
|
+
</Field.Group>
|
|
299
|
+
</Field.Set>
|
|
300
|
+
|
|
301
|
+
{#if issued}
|
|
302
|
+
<Alert>
|
|
303
|
+
<AlertTitle>{t('mcp.server.issuedTitle', { name: issued.name })}</AlertTitle>
|
|
304
|
+
<AlertDescription class="flex flex-col gap-2">
|
|
305
|
+
<p>{t('mcp.server.issuedOnce')}</p>
|
|
306
|
+
<code class="break-all">{issued.token}</code>
|
|
307
|
+
<div class="flex flex-wrap gap-2">
|
|
308
|
+
<Button
|
|
309
|
+
size="sm"
|
|
310
|
+
variant="outline"
|
|
311
|
+
disabled={!configInput || !claudeNameValid}
|
|
312
|
+
onclick={() => void copy('posix')}
|
|
313
|
+
>
|
|
314
|
+
{t('mcp.server.copyClaude')}
|
|
315
|
+
</Button>
|
|
316
|
+
<Button
|
|
317
|
+
size="sm"
|
|
318
|
+
variant="outline"
|
|
319
|
+
disabled={!configInput}
|
|
320
|
+
onclick={() => void copy('json')}
|
|
321
|
+
>
|
|
322
|
+
{t('mcp.server.copyJson')}
|
|
323
|
+
</Button>
|
|
324
|
+
<Button size="sm" variant="ghost" onclick={clearIssued}
|
|
325
|
+
>{t('mcp.server.dismiss')}</Button
|
|
326
|
+
>
|
|
327
|
+
</div>
|
|
328
|
+
</AlertDescription>
|
|
329
|
+
</Alert>
|
|
330
|
+
{/if}
|
|
331
|
+
|
|
332
|
+
<Field.Set>
|
|
333
|
+
<Field.Legend>{t('mcp.server.tokens')}</Field.Legend>
|
|
334
|
+
{#if tokens.length === 0}
|
|
335
|
+
<Field.Description>{t('mcp.server.noTokens')}</Field.Description>
|
|
336
|
+
{/if}
|
|
337
|
+
<ul class="flex flex-col gap-2">
|
|
338
|
+
{#each tokens as token (token.id)}
|
|
339
|
+
<li class="flex items-center justify-between gap-4 rounded-md border p-3">
|
|
340
|
+
<div class="flex min-w-0 flex-col">
|
|
341
|
+
<span class="truncate font-medium">{token.name}</span>
|
|
342
|
+
<span class="text-muted-foreground text-sm">
|
|
343
|
+
{token.scopes.join(', ')} · {t('mcp.server.lastUsed')}: {date(token.lastUsedAt)}
|
|
344
|
+
</span>
|
|
345
|
+
</div>
|
|
346
|
+
{#if token.revokedAt === null}
|
|
347
|
+
<Button
|
|
348
|
+
size="sm"
|
|
349
|
+
variant="destructive"
|
|
350
|
+
disabled={busy || issuing}
|
|
351
|
+
onclick={() => void revoke(token)}
|
|
352
|
+
>
|
|
353
|
+
{t('mcp.server.revoke')}
|
|
354
|
+
</Button>
|
|
355
|
+
{:else}
|
|
356
|
+
<span class="text-muted-foreground text-sm">{t('mcp.server.revoked')}</span>
|
|
357
|
+
{/if}
|
|
358
|
+
</li>
|
|
359
|
+
{/each}
|
|
360
|
+
</ul>
|
|
361
|
+
</Field.Set>
|
|
362
|
+
</section>
|
|
363
|
+
{/if}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
interface McpConfigInput {
|
|
2
|
+
serverName: string
|
|
3
|
+
url: string
|
|
4
|
+
token: string
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function buildMcpJsonConfig({ serverName, url, token }: McpConfigInput): string {
|
|
8
|
+
return JSON.stringify(
|
|
9
|
+
{
|
|
10
|
+
mcpServers: {
|
|
11
|
+
[serverName]: { type: 'http', url, headers: { Authorization: `Bearer ${token}` } },
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
null,
|
|
15
|
+
2,
|
|
16
|
+
)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function posixQuote(value: string): string {
|
|
20
|
+
return `'${value.replaceAll("'", "'\"'\"'")}'`
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** POSIX shell only: every dynamic value is exactly one quoted argument. */
|
|
24
|
+
export function buildClaudePosixCommand({ serverName, url, token }: McpConfigInput): string {
|
|
25
|
+
return `claude mcp add --transport http ${posixQuote(serverName)} ${posixQuote(url)} --header ${posixQuote(`Authorization: Bearer ${token}`)}`
|
|
26
|
+
}
|
package/src/mcp/index.ts
ADDED
package/src/mcp/input.ts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { McpTransport, ServerDto, ServerSave, ServerSecret } from '../ports'
|
|
2
|
+
|
|
3
|
+
/** Write-only editor input; credentials never come from a ServerDto. */
|
|
4
|
+
export interface McpInput {
|
|
5
|
+
name: string
|
|
6
|
+
slug: string
|
|
7
|
+
kind: McpTransport['kind']
|
|
8
|
+
url: string
|
|
9
|
+
command: string
|
|
10
|
+
args: string
|
|
11
|
+
cwd: string
|
|
12
|
+
replaceSecret: boolean
|
|
13
|
+
headers: string
|
|
14
|
+
query: string
|
|
15
|
+
env: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export type McpInputResult =
|
|
19
|
+
{ ok: true; value: ServerSave } | { ok: false; error: 'secretLine' | 'transportChanged' }
|
|
20
|
+
|
|
21
|
+
/** Split only at the first separator; values may themselves contain separators. */
|
|
22
|
+
function pairs(text: string, separator: string): Record<string, string> | null {
|
|
23
|
+
const entries: [string, string][] = []
|
|
24
|
+
for (const raw of text.split('\n')) {
|
|
25
|
+
const line = raw.trim()
|
|
26
|
+
if (!line) continue
|
|
27
|
+
const at = line.indexOf(separator)
|
|
28
|
+
const name = line.slice(0, at).trim()
|
|
29
|
+
if (at <= 0 || !name) return null
|
|
30
|
+
entries.push([name, line.slice(at + 1).trim()])
|
|
31
|
+
}
|
|
32
|
+
return Object.fromEntries(entries)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function mapMcpInput(current: ServerDto | null, input: McpInput): McpInputResult {
|
|
36
|
+
if (current && current.transport.kind !== input.kind && !input.replaceSecret) {
|
|
37
|
+
return { ok: false, error: 'transportChanged' }
|
|
38
|
+
}
|
|
39
|
+
const transport: McpTransport =
|
|
40
|
+
input.kind === 'http'
|
|
41
|
+
? { kind: 'http', url: input.url.trim() }
|
|
42
|
+
: {
|
|
43
|
+
kind: 'stdio',
|
|
44
|
+
command: input.command.trim(),
|
|
45
|
+
args: input.args.split('\n').filter((arg) => arg.trim() !== ''),
|
|
46
|
+
...(input.cwd.trim() ? { cwd: input.cwd.trim() } : {}),
|
|
47
|
+
}
|
|
48
|
+
const value: ServerSave = {
|
|
49
|
+
...(current ? { id: current.id } : {}),
|
|
50
|
+
name: input.name.trim(),
|
|
51
|
+
slug: input.slug.trim(),
|
|
52
|
+
transport,
|
|
53
|
+
enabled: current?.enabled ?? false,
|
|
54
|
+
trusted: current?.trusted ?? false,
|
|
55
|
+
allowPrivate: transport.kind === 'http' ? (current?.allowPrivate ?? false) : false,
|
|
56
|
+
}
|
|
57
|
+
if (input.replaceSecret) {
|
|
58
|
+
const secret: ServerSecret = {}
|
|
59
|
+
if (input.kind === 'http') {
|
|
60
|
+
const headers = pairs(input.headers, ':')
|
|
61
|
+
const query = pairs(input.query, '=')
|
|
62
|
+
if (!headers || !query) return { ok: false, error: 'secretLine' }
|
|
63
|
+
if (Object.keys(headers).length) secret.headers = headers
|
|
64
|
+
if (Object.keys(query).length) secret.query = query
|
|
65
|
+
} else {
|
|
66
|
+
const env = pairs(input.env, '=')
|
|
67
|
+
if (!env) return { ok: false, error: 'secretLine' }
|
|
68
|
+
if (Object.keys(env).length) secret.env = env
|
|
69
|
+
}
|
|
70
|
+
// Omitted preserves; explicit {} clears. New servers need no empty secret.
|
|
71
|
+
if (current || Object.keys(secret).length) value.secret = secret
|
|
72
|
+
}
|
|
73
|
+
return { ok: true, value }
|
|
74
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// Default KitNotify for hosts without their own: svelte-sonner toasts + one shadcn AlertDialog (docs/16 §3).
|
|
2
|
+
// Render <KitNotifier notify={...} /> once next to the host root so the toaster and dialog exist.
|
|
3
|
+
import { toast } from 'svelte-sonner'
|
|
4
|
+
import type { ConfirmOptions, KitNotify } from './context'
|
|
5
|
+
|
|
6
|
+
export class DefaultNotify implements KitNotify {
|
|
7
|
+
#request = $state<ConfirmOptions | null>(null)
|
|
8
|
+
#open = $state(false)
|
|
9
|
+
#resolve: ((answer: boolean) => void) | null = null
|
|
10
|
+
|
|
11
|
+
/** Last request; kept after closing so the dialog keeps its text while it animates out. */
|
|
12
|
+
get request(): ConfirmOptions | null {
|
|
13
|
+
return this.#request
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
get open(): boolean {
|
|
17
|
+
return this.#open
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
toast(kind: 'info' | 'success' | 'error', text: string): void {
|
|
21
|
+
toast[kind](text)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** A newer confirm answers the pending one with `false`. */
|
|
25
|
+
confirm(options: ConfirmOptions): Promise<boolean> {
|
|
26
|
+
this.answer(false)
|
|
27
|
+
this.#request = options
|
|
28
|
+
this.#open = true
|
|
29
|
+
const { promise, resolve } = Promise.withResolvers<boolean>()
|
|
30
|
+
this.#resolve = resolve
|
|
31
|
+
return promise
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
answer(isConfirmed: boolean): void {
|
|
35
|
+
this.#open = false
|
|
36
|
+
this.#resolve?.(isConfirmed)
|
|
37
|
+
this.#resolve = null
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function createDefaultNotify(): DefaultNotify {
|
|
42
|
+
return new DefaultNotify()
|
|
43
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
// Port interfaces and KitClientError (docs/16 §2, docs/04 §3.2). Pure TS: no Tauri imports.
|
|
2
|
+
import type {
|
|
3
|
+
Conversation,
|
|
4
|
+
ConversationSummary,
|
|
5
|
+
IngestBatch,
|
|
6
|
+
IngestReceipt,
|
|
7
|
+
IssuedToken,
|
|
8
|
+
KitError,
|
|
9
|
+
KitErrorCode,
|
|
10
|
+
Details,
|
|
11
|
+
EndpointKeySave,
|
|
12
|
+
LogEntry,
|
|
13
|
+
LogLevel,
|
|
14
|
+
LogStatus,
|
|
15
|
+
ManualSave,
|
|
16
|
+
Approval,
|
|
17
|
+
ModelFilter,
|
|
18
|
+
ModelOption,
|
|
19
|
+
ProbeResult,
|
|
20
|
+
Preview,
|
|
21
|
+
ProbeTarget,
|
|
22
|
+
Provider,
|
|
23
|
+
ProviderSave,
|
|
24
|
+
Protocol,
|
|
25
|
+
RunAccepted,
|
|
26
|
+
RunEvent,
|
|
27
|
+
RunRequest,
|
|
28
|
+
RunSnapshot,
|
|
29
|
+
ServerDto,
|
|
30
|
+
ServerSave,
|
|
31
|
+
SkillDto,
|
|
32
|
+
SkillMeta,
|
|
33
|
+
Source,
|
|
34
|
+
ScopeState,
|
|
35
|
+
Status,
|
|
36
|
+
TokenRecord,
|
|
37
|
+
ToolDto,
|
|
38
|
+
} from '@ouraihub/aether-contracts'
|
|
39
|
+
import type { SearchPort } from './search'
|
|
40
|
+
import type { SkillSourcesPort } from './skill-sources'
|
|
41
|
+
|
|
42
|
+
export interface CallOptions {
|
|
43
|
+
signal?: AbortSignal
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface LogsPort {
|
|
47
|
+
tail(limit: number): Promise<LogEntry[]>
|
|
48
|
+
ingest(batch: IngestBatch): Promise<IngestReceipt>
|
|
49
|
+
getLevel(): Promise<LogStatus>
|
|
50
|
+
/** Takes effect immediately and is persisted to `level.json`. */
|
|
51
|
+
setLevel(level: LogLevel): Promise<LogStatus>
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface DesktopActions {
|
|
55
|
+
openLogsDirectory(): Promise<void>
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Host-supplied provider template, seeded once into the store disabled and keyless (docs/39). */
|
|
59
|
+
export interface ProviderPreset {
|
|
60
|
+
id: string
|
|
61
|
+
name: string
|
|
62
|
+
baseUrl: string
|
|
63
|
+
protocol: Protocol
|
|
64
|
+
/** "Get a key" link, e.g. an invite link. */
|
|
65
|
+
apiKeyUrl?: string
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface ProvidersPort {
|
|
69
|
+
/** Browser asks for the current password; desktop uses its local credential boundary. */
|
|
70
|
+
readonly revealNeedsPassword: boolean
|
|
71
|
+
/** Host stores `keyStrategy` and key set-aside state (web); `false` = rotation only, fields ignored. */
|
|
72
|
+
readonly keyStrategies: boolean
|
|
73
|
+
list(): Promise<Provider[]>
|
|
74
|
+
setEnabled(id: string, enabled: boolean): Promise<Provider[]>
|
|
75
|
+
save(provider: ProviderSave): Promise<Provider[]>
|
|
76
|
+
remove(id: string): Promise<Provider[]>
|
|
77
|
+
modelOptions(filter?: ModelFilter): Promise<ModelOption[]>
|
|
78
|
+
probe(target: ProbeTarget, options?: CallOptions): Promise<ProbeResult>
|
|
79
|
+
saveKey(input: EndpointKeySave): Promise<Provider[]>
|
|
80
|
+
removeKey(endpointId: string, keyId: string): Promise<Provider[]>
|
|
81
|
+
revealKey(keyId: string, currentPassword?: string | null): Promise<string>
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface ConversationClient {
|
|
85
|
+
list(scope?: string): Promise<ConversationSummary[]>
|
|
86
|
+
get(id: string): Promise<Conversation>
|
|
87
|
+
remove(id: string): Promise<void>
|
|
88
|
+
rename(id: string, title: string): Promise<ConversationSummary>
|
|
89
|
+
editTurn(
|
|
90
|
+
conversationId: string,
|
|
91
|
+
turnId: string,
|
|
92
|
+
text: string,
|
|
93
|
+
expectedText: string,
|
|
94
|
+
): Promise<Conversation>
|
|
95
|
+
removeTurn(conversationId: string, turnId: string): Promise<Conversation>
|
|
96
|
+
fork(conversationId: string, turnId: string): Promise<Conversation>
|
|
97
|
+
selectTurn(conversationId: string, turnId: string): Promise<Conversation>
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export interface RunClient {
|
|
101
|
+
/** Resolves once accepted; events may arrive before acceptance. */
|
|
102
|
+
start(
|
|
103
|
+
request: RunRequest,
|
|
104
|
+
onEvent: (event: RunEvent) => void,
|
|
105
|
+
options?: CallOptions,
|
|
106
|
+
): Promise<RunAccepted>
|
|
107
|
+
cancel(runId: string): Promise<void>
|
|
108
|
+
approve(runId: string, callId: string, allow: boolean): Promise<void>
|
|
109
|
+
getSnapshot(runId: string): Promise<RunSnapshot>
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export interface SkillsPort {
|
|
113
|
+
list(): Promise<SkillMeta[]>
|
|
114
|
+
get(id: string): Promise<SkillDto>
|
|
115
|
+
/** Reads and validates a package; nothing is stored. */
|
|
116
|
+
preview(source: Source): Promise<Preview>
|
|
117
|
+
/** `confirmedDigest` is the previewed digest; a changed package rejects with `conflict{content_changed}`. */
|
|
118
|
+
install(source: Source, confirmedDigest: string, replace: boolean): Promise<SkillMeta[]>
|
|
119
|
+
save(input: ManualSave): Promise<SkillMeta[]>
|
|
120
|
+
duplicate(id: string, newName: string): Promise<SkillMeta[]>
|
|
121
|
+
reorder(ids: readonly string[]): Promise<SkillMeta[]>
|
|
122
|
+
remove(id: string): Promise<SkillMeta[]>
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export interface McpClientPort {
|
|
126
|
+
list(): Promise<ServerDto[]>
|
|
127
|
+
save(server: ServerSave): Promise<ServerDto[]>
|
|
128
|
+
remove(id: string): Promise<ServerDto[]>
|
|
129
|
+
refresh(serverId: string): Promise<ToolDto[]>
|
|
130
|
+
setToolApproval(serverId: string, tool: string, approval: Approval): Promise<void>
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export interface McpServerPort {
|
|
134
|
+
status(): Promise<Status>
|
|
135
|
+
tokens(): Promise<TokenRecord[]>
|
|
136
|
+
start(): Promise<Status>
|
|
137
|
+
stop(): Promise<Status>
|
|
138
|
+
setPort(port: number): Promise<Status>
|
|
139
|
+
issueToken(name: string, scopes: readonly string[]): Promise<IssuedToken>
|
|
140
|
+
revokeToken(id: string): Promise<void>
|
|
141
|
+
scopes(): Promise<ScopeState[]>
|
|
142
|
+
setScopes(ids: readonly string[]): Promise<ScopeState[]>
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Ports the host declared; components skip the area of a missing port (docs/16 §2). */
|
|
146
|
+
export interface KitPorts {
|
|
147
|
+
logs?: LogsPort
|
|
148
|
+
desktop?: DesktopActions
|
|
149
|
+
providers?: ProvidersPort
|
|
150
|
+
chat?: RunClient
|
|
151
|
+
conversations?: ConversationClient
|
|
152
|
+
search?: SearchPort
|
|
153
|
+
skills?: SkillsPort
|
|
154
|
+
skillSources?: SkillSourcesPort
|
|
155
|
+
mcpClient?: McpClientPort
|
|
156
|
+
mcpServer?: McpServerPort
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** The only error type thrown across a port (docs/04 §3.2). */
|
|
160
|
+
export class KitClientError extends Error {
|
|
161
|
+
readonly code: KitErrorCode
|
|
162
|
+
readonly details: Details
|
|
163
|
+
|
|
164
|
+
constructor(error: KitError) {
|
|
165
|
+
super(error.message)
|
|
166
|
+
this.name = 'KitClientError'
|
|
167
|
+
this.code = error.code
|
|
168
|
+
this.details = error.details ?? {}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export function isKitClientError(error: unknown): error is KitClientError {
|
|
173
|
+
return error instanceof KitClientError
|
|
174
|
+
}
|