@briancray/belte 0.1.0 → 0.2.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/bin/belte.ts +25 -12
- package/package.json +2 -1
- package/src/appEntry.ts +124 -0
- package/src/belteResolverPlugin.ts +236 -202
- package/src/build.ts +6 -67
- package/src/buildCli.ts +36 -63
- package/src/buildDisconnected.ts +127 -0
- package/src/bundleApp.ts +123 -0
- package/src/bundleDisconnectedEntry.ts +17 -0
- package/src/cliEntry.ts +3 -9
- package/src/compile.ts +4 -15
- package/src/controlServerWorker.ts +261 -0
- package/src/dedupeSveltePlugin.ts +66 -0
- package/src/discoveryEntry.ts +12 -11
- package/src/lib/browser/cache.ts +3 -6
- package/src/lib/browser/page.svelte.ts +19 -21
- package/src/lib/browser/socketChannel.ts +11 -1
- package/src/lib/browser/types/Pages.ts +1 -1
- package/src/lib/bundle/BundleMenu.ts +11 -0
- package/src/lib/bundle/BundleMenuItem.ts +24 -0
- package/src/lib/bundle/BundleWindow.ts +20 -0
- package/src/lib/bundle/bindConnectedFlag.ts +29 -0
- package/src/lib/bundle/bindRequestNavigate.ts +31 -0
- package/src/lib/bundle/buildWebviewLib.ts +111 -0
- package/src/lib/bundle/disconnected.css +9 -0
- package/src/lib/bundle/disconnected.svelte +192 -0
- package/src/lib/bundle/ensureWebviewLib.ts +20 -0
- package/src/lib/bundle/exitWithParent.ts +28 -0
- package/src/lib/bundle/findFreePort.ts +14 -0
- package/src/lib/bundle/infoPlist.ts +46 -0
- package/src/lib/bundle/installMacMenu.ts +39 -0
- package/src/lib/bundle/listenLocalControlServer.ts +19 -0
- package/src/lib/bundle/native/belteMenu.mm +298 -0
- package/src/lib/bundle/native/webview.h +4557 -0
- package/src/lib/bundle/onMenu.ts +26 -0
- package/src/lib/bundle/openWebview.ts +81 -0
- package/src/lib/bundle/pngToIcns.ts +47 -0
- package/src/lib/bundle/probeBelteServer.ts +34 -0
- package/src/lib/bundle/resolveServerBinary.ts +12 -0
- package/src/lib/bundle/resolveWebviewLib.ts +51 -0
- package/src/lib/bundle/serverBinaryFilename.ts +8 -0
- package/src/lib/bundle/stableLocalPort.ts +19 -0
- package/src/lib/bundle/waitForServer.ts +23 -0
- package/src/lib/bundle/webviewBuildRevision.ts +9 -0
- package/src/lib/bundle/webviewCachePath.ts +23 -0
- package/src/lib/bundle/webviewLibName.ts +11 -0
- package/src/lib/bundle/webviewVersion.ts +7 -0
- package/src/lib/cli/createClient.ts +34 -36
- package/src/lib/cli/printHelp.ts +45 -2
- package/src/lib/cli/runCli.ts +12 -3
- package/src/lib/mcp/createMcpResourceServer.ts +1 -1
- package/src/lib/mcp/dispatchMcpRequest.ts +53 -73
- package/src/lib/server/AppModule.ts +2 -2
- package/src/lib/server/cli/handleCliDownload.ts +4 -5
- package/src/lib/server/cli/handleCliInstall.ts +17 -0
- package/src/lib/server/error.ts +23 -9
- package/src/lib/server/json.ts +5 -5
- package/src/lib/server/jsonl.ts +10 -5
- package/src/lib/server/prompts/definePrompt.ts +6 -6
- package/src/lib/server/prompts/renderPromptTemplate.ts +16 -0
- package/src/lib/server/prompts/types/Prompt.ts +8 -9
- package/src/lib/server/prompts/types/PromptOptions.ts +7 -12
- package/src/lib/server/prompts/types/PromptRegistryEntry.ts +3 -5
- package/src/lib/server/prompts/types/PromptRoutes.ts +4 -4
- package/src/lib/server/redirect.ts +13 -8
- package/src/lib/server/rpc/defineVerb.ts +4 -3
- package/src/lib/server/rpc/findVerbByCommandName.ts +18 -0
- package/src/lib/server/rpc/types/RemoteFunction.ts +1 -1
- package/src/lib/server/rpc/types/RemoteHandler.ts +4 -0
- package/src/lib/server/runtime/acceptsZstd.ts +8 -0
- package/src/lib/server/runtime/buildOpenApiSpec.ts +2 -0
- package/src/lib/server/runtime/cacheControlForAsset.ts +7 -2
- package/src/lib/server/runtime/createAssetHeaderCache.ts +35 -0
- package/src/lib/server/runtime/createPublicAssetServer.ts +6 -20
- package/src/lib/server/runtime/createServer.ts +50 -58
- package/src/lib/server/runtime/registryManifests.ts +33 -15
- package/src/lib/server/runtime/types/RequestStore.ts +2 -3
- package/src/lib/server/runtime/withResponseDefaults.ts +24 -0
- package/src/lib/server/sockets/createSocketDispatcher.ts +10 -7
- package/src/lib/server/sse.ts +10 -5
- package/src/lib/shared/belteImportName.test.ts +58 -0
- package/src/lib/shared/belteImportName.ts +45 -0
- package/src/lib/shared/beltePackageName.ts +7 -0
- package/src/lib/shared/cacheControlValues.ts +10 -2
- package/src/lib/shared/canonicalJson.ts +1 -5
- package/src/lib/shared/createCacheStore.ts +29 -20
- package/src/lib/shared/exitOnBuildFailure.ts +17 -0
- package/src/lib/shared/fileStem.ts +9 -0
- package/src/lib/shared/jsonSchemaForPromptArguments.ts +29 -0
- package/src/lib/shared/keyForRemoteCall.ts +7 -5
- package/src/lib/shared/parsePromptMarkdown.ts +34 -0
- package/src/lib/shared/prepareRpcModule.ts +14 -4
- package/src/lib/shared/prepareSocketModule.ts +16 -2
- package/src/lib/shared/promptNameForFile.ts +5 -5
- package/src/lib/shared/subscribableFromResponse.ts +104 -215
- package/src/lib/shared/types/PromptArgument.ts +12 -0
- package/src/lib/shared/writeRoutesDts.ts +5 -7
- package/src/serverBuildPlugins.ts +25 -0
- package/src/serverEntry.ts +4 -0
- package/template/package.json +3 -2
- package/src/lib/server/prompt.ts +0 -30
- package/src/lib/server/prompts/types/PromptMessage.ts +0 -10
- package/src/lib/shared/preparePromptModule.ts +0 -36
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { promptRegistry } from '../server/prompts/promptRegistry.ts'
|
|
2
|
-
import
|
|
2
|
+
import { findVerbByCommandName } from '../server/rpc/findVerbByCommandName.ts'
|
|
3
|
+
import type { VerbRegistryEntry } from '../server/rpc/types/VerbRegistryEntry.ts'
|
|
3
4
|
import { verbRegistry } from '../server/rpc/verbRegistry.ts'
|
|
4
5
|
import { ensureRegistriesLoaded } from '../server/runtime/registryManifests.ts'
|
|
5
6
|
import { buildRpcRequest } from '../shared/buildRpcRequest.ts'
|
|
@@ -28,47 +29,57 @@ function jsonRpcOk(id: string | number | null, result: unknown): JsonRpcResponse
|
|
|
28
29
|
return { jsonrpc: '2.0', id, result }
|
|
29
30
|
}
|
|
30
31
|
|
|
32
|
+
type ToolDescriptor = {
|
|
33
|
+
name: string
|
|
34
|
+
description: string
|
|
35
|
+
inputSchema: Record<string, unknown>
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
type PromptDescriptor = {
|
|
39
|
+
name: string
|
|
40
|
+
description?: string
|
|
41
|
+
arguments: Array<{ name: string; description?: string; required: boolean }>
|
|
42
|
+
}
|
|
43
|
+
|
|
31
44
|
/*
|
|
32
45
|
Builds the array of MCP tool descriptors. Every rpc with clients.mcp=true
|
|
33
46
|
becomes one tool named after the export's URL (folder segments joined
|
|
34
47
|
with `-`), regardless of HTTP verb — GET reads and mutating verbs alike.
|
|
35
48
|
Sockets are never exposed to MCP.
|
|
36
49
|
*/
|
|
37
|
-
function buildTools():
|
|
38
|
-
|
|
39
|
-
description: string
|
|
40
|
-
inputSchema: Record<string, unknown>
|
|
41
|
-
}> {
|
|
42
|
-
const tools: Array<{
|
|
43
|
-
name: string
|
|
44
|
-
description: string
|
|
45
|
-
inputSchema: Record<string, unknown>
|
|
46
|
-
}> = []
|
|
50
|
+
function buildTools(): ToolDescriptor[] {
|
|
51
|
+
const tools: ToolDescriptor[] = []
|
|
47
52
|
for (const entry of verbRegistry.values()) {
|
|
48
53
|
if (!entry.clients.mcp) {
|
|
49
54
|
continue
|
|
50
55
|
}
|
|
56
|
+
/*
|
|
57
|
+
Tool description favours the schema's top-level description (the
|
|
58
|
+
vendor's JSON Schema conversion carries `.describe(...)` through),
|
|
59
|
+
falling back to `method url` so the tool is still labelled when
|
|
60
|
+
the schema has none.
|
|
61
|
+
*/
|
|
62
|
+
const inputSchema = jsonSchemaForSchema(entry.schema, entry.jsonSchema)
|
|
51
63
|
tools.push({
|
|
52
64
|
name: commandNameForUrl(entry.remote.url),
|
|
53
|
-
description:
|
|
54
|
-
|
|
65
|
+
description:
|
|
66
|
+
(inputSchema.description as string | undefined) ??
|
|
67
|
+
`${entry.remote.method} ${entry.remote.url}`,
|
|
68
|
+
inputSchema,
|
|
55
69
|
})
|
|
56
70
|
}
|
|
57
71
|
return tools
|
|
58
72
|
}
|
|
59
73
|
|
|
60
74
|
/*
|
|
61
|
-
MCP prompts derived from src/mcp/prompts. Arguments come from the
|
|
62
|
-
|
|
63
|
-
|
|
75
|
+
MCP prompts derived from src/mcp/prompts. Arguments come from the JSON
|
|
76
|
+
Schema the resolver built from each prompt's frontmatter `arguments` list
|
|
77
|
+
(top-level properties + required flags); the model fills them in and the
|
|
78
|
+
framework interpolates them into the body on prompts/get.
|
|
64
79
|
*/
|
|
65
|
-
function buildPrompts():
|
|
66
|
-
name: string
|
|
67
|
-
description?: string
|
|
68
|
-
arguments: Array<{ name: string; description?: string; required: boolean }>
|
|
69
|
-
}> {
|
|
80
|
+
function buildPrompts(): PromptDescriptor[] {
|
|
70
81
|
return Array.from(promptRegistry.values()).map((entry) => {
|
|
71
|
-
const jsonSchema =
|
|
82
|
+
const jsonSchema = entry.jsonSchema ?? {}
|
|
72
83
|
const properties = (jsonSchema.properties ?? {}) as Record<string, { description?: string }>
|
|
73
84
|
const required = new Set((jsonSchema.required as string[] | undefined) ?? [])
|
|
74
85
|
return {
|
|
@@ -94,20 +105,11 @@ async function callTool(
|
|
|
94
105
|
args: Record<string, unknown> | undefined,
|
|
95
106
|
inbound: Request,
|
|
96
107
|
): Promise<Record<string, unknown>> {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
if (!entry.clients.mcp) {
|
|
100
|
-
continue
|
|
101
|
-
}
|
|
102
|
-
if (commandNameForUrl(entry.remote.url) === toolName) {
|
|
103
|
-
found = entry
|
|
104
|
-
break
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
if (!found) {
|
|
108
|
+
const entry = findVerbByCommandName(toolName)
|
|
109
|
+
if (!entry?.clients.mcp) {
|
|
108
110
|
throw new Error(`unknown tool: ${toolName}`)
|
|
109
111
|
}
|
|
110
|
-
const response = await
|
|
112
|
+
const response = await dispatchVerb(entry, args, inbound)
|
|
111
113
|
if (!response.ok) {
|
|
112
114
|
return {
|
|
113
115
|
content: [
|
|
@@ -119,7 +121,7 @@ async function callTool(
|
|
|
119
121
|
isError: true,
|
|
120
122
|
}
|
|
121
123
|
}
|
|
122
|
-
const body = await decodeResponse(response
|
|
124
|
+
const body = await decodeResponse(response)
|
|
123
125
|
return {
|
|
124
126
|
content: [
|
|
125
127
|
{
|
|
@@ -131,66 +133,44 @@ async function callTool(
|
|
|
131
133
|
}
|
|
132
134
|
|
|
133
135
|
/*
|
|
134
|
-
Synthesizes the rpc Request
|
|
135
|
-
the
|
|
136
|
-
|
|
136
|
+
Synthesizes the rpc Request from a resolved registry entry and dispatches
|
|
137
|
+
through verb.fetch — the same code path the HTTP router uses — forwarding the
|
|
138
|
+
inbound MCP request's auth headers so session/bearer middleware keeps working.
|
|
137
139
|
*/
|
|
138
|
-
function
|
|
139
|
-
|
|
140
|
-
url: string,
|
|
140
|
+
function dispatchVerb(
|
|
141
|
+
entry: VerbRegistryEntry,
|
|
141
142
|
args: Record<string, unknown> | undefined,
|
|
142
143
|
inbound: Request,
|
|
143
144
|
): Promise<Response> {
|
|
144
145
|
const inboundUrl = new URL(inbound.url)
|
|
145
146
|
const baseUrl = `${inboundUrl.protocol}//${inboundUrl.host}/`
|
|
146
147
|
const request = buildRpcRequest({
|
|
147
|
-
method,
|
|
148
|
-
url,
|
|
148
|
+
method: entry.remote.method,
|
|
149
|
+
url: entry.remote.url,
|
|
149
150
|
args,
|
|
150
151
|
baseUrl,
|
|
151
152
|
headers: forwardHeaders(inbound.headers),
|
|
152
153
|
})
|
|
153
|
-
|
|
154
|
-
if (entry && entry.remote.method === method) {
|
|
155
|
-
return entry.remote.fetch(request)
|
|
156
|
-
}
|
|
157
|
-
throw new Error(`unknown rpc: ${method} ${url}`)
|
|
154
|
+
return entry.remote.fetch(request)
|
|
158
155
|
}
|
|
159
156
|
|
|
160
157
|
/*
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
158
|
+
Interpolates the caller's arguments into the prompt body and wraps the
|
|
159
|
+
result in the MCP prompts/get wire shape — a markdown prompt is a single
|
|
160
|
+
user message whose text is the rendered template.
|
|
164
161
|
*/
|
|
165
|
-
|
|
162
|
+
function getPrompt(
|
|
166
163
|
name: string,
|
|
167
164
|
args: Record<string, unknown> | undefined,
|
|
168
|
-
):
|
|
165
|
+
): Record<string, unknown> {
|
|
169
166
|
const entry = promptRegistry.get(name)
|
|
170
167
|
if (!entry) {
|
|
171
168
|
throw new Error(`unknown prompt: ${name}`)
|
|
172
169
|
}
|
|
173
|
-
|
|
174
|
-
if (entry.schema) {
|
|
175
|
-
const result = await entry.schema['~standard'].validate(value)
|
|
176
|
-
if (result.issues) {
|
|
177
|
-
throw new Error(
|
|
178
|
-
`prompt "${name}" arguments failed validation: ${JSON.stringify(result.issues)}`,
|
|
179
|
-
)
|
|
180
|
-
}
|
|
181
|
-
value = result.value
|
|
182
|
-
}
|
|
183
|
-
const rendered = await entry.prompt.render(value as Record<string, string>)
|
|
184
|
-
const messages =
|
|
185
|
-
typeof rendered === 'string'
|
|
186
|
-
? [{ role: 'user', content: { type: 'text', text: rendered } }]
|
|
187
|
-
: rendered.map((message) => ({
|
|
188
|
-
role: message.role,
|
|
189
|
-
content: { type: 'text', text: message.text },
|
|
190
|
-
}))
|
|
170
|
+
const rendered = entry.prompt.render((args ?? {}) as Record<string, string>)
|
|
191
171
|
return {
|
|
192
172
|
...(entry.prompt.description ? { description: entry.prompt.description } : {}),
|
|
193
|
-
messages,
|
|
173
|
+
messages: [{ role: 'user', content: { type: 'text', text: rendered } }],
|
|
194
174
|
}
|
|
195
175
|
}
|
|
196
176
|
|
|
@@ -277,7 +257,7 @@ export async function dispatchMcpRequest(
|
|
|
277
257
|
if (!params?.name) {
|
|
278
258
|
return jsonRpcError(id, -32602, 'Missing prompt name')
|
|
279
259
|
}
|
|
280
|
-
return jsonRpcOk(id,
|
|
260
|
+
return jsonRpcOk(id, getPrompt(params.name, params.arguments))
|
|
281
261
|
}
|
|
282
262
|
default:
|
|
283
263
|
return jsonRpcError(id, -32601, `Method not found: ${envelope.method}`)
|
|
@@ -7,10 +7,10 @@ function that runs on SIGINT/SIGTERM. handle is single-middleware with
|
|
|
7
7
|
next so user code can mutate the response or branch on the URL.
|
|
8
8
|
|
|
9
9
|
WebSockets are not exposed here — belte's only native WebSocket
|
|
10
|
-
surface is the sockets hub (see `belte/
|
|
10
|
+
surface is the sockets hub (see `belte/server/socket`), multiplexed onto a
|
|
11
11
|
single framework-owned connection per client at `/__belte/sockets`.
|
|
12
12
|
Inside request scopes, the live Bun.Server is reachable via the
|
|
13
|
-
exported `server`
|
|
13
|
+
exported `server()` function from `belte/server`; `init` receives it
|
|
14
14
|
explicitly because it runs outside a request.
|
|
15
15
|
*/
|
|
16
16
|
export type AppModule = {
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { existsSync, statSync } from 'node:fs'
|
|
2
1
|
import { NO_STORE } from '../../shared/cacheControlValues.ts'
|
|
3
2
|
import { log } from '../../shared/log.ts'
|
|
4
3
|
import { normalizeTarget } from '../../shared/normalizeTarget.ts'
|
|
@@ -55,8 +54,9 @@ async function computeBinary(
|
|
|
55
54
|
didn't run `belte cli` again. Other source paths (project lib,
|
|
56
55
|
transitive imports) fall back to manual rebuild.
|
|
57
56
|
*/
|
|
58
|
-
|
|
59
|
-
|
|
57
|
+
const binaryFile = Bun.file(binaryPath)
|
|
58
|
+
if (await binaryFile.exists()) {
|
|
59
|
+
const binaryMtime = (await binaryFile.stat()).mtimeMs
|
|
60
60
|
const sourceMtime = await maxSourceMtime(cwd)
|
|
61
61
|
if (binaryMtime >= sourceMtime) {
|
|
62
62
|
return binaryPath
|
|
@@ -71,9 +71,8 @@ async function computeBinary(
|
|
|
71
71
|
await buildCli({
|
|
72
72
|
cwd,
|
|
73
73
|
platforms: [normalizeTarget(platform)],
|
|
74
|
-
thin: true,
|
|
75
74
|
})
|
|
76
|
-
return
|
|
75
|
+
return (await binaryFile.exists()) ? binaryPath : undefined
|
|
77
76
|
} catch (error) {
|
|
78
77
|
log.error(error)
|
|
79
78
|
return undefined
|
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
import { NO_STORE } from '../../shared/cacheControlValues.ts'
|
|
2
2
|
import { installScript } from './installScript.ts'
|
|
3
3
|
|
|
4
|
+
/*
|
|
5
|
+
The request host is reflected verbatim into a shell script the user pipes
|
|
6
|
+
to `sh`, so it's constrained to the strict authority charset: letters,
|
|
7
|
+
digits, `.`, `-`, `_`, `:` (port + IPv6 separators), and IPv6 `[` `]`
|
|
8
|
+
brackets. That set excludes every character that could break out of the
|
|
9
|
+
interpolated `URL="…"` line in installScript (`"`, `$`, backtick, `\`,
|
|
10
|
+
whitespace), neutralising shell injection via a crafted Host header
|
|
11
|
+
regardless of how lenient the upstream URL parser is.
|
|
12
|
+
*/
|
|
13
|
+
const SAFE_HOST = /^[A-Za-z0-9._:[\]-]+$/
|
|
14
|
+
|
|
4
15
|
/*
|
|
5
16
|
Handles GET /__belte/cli — returns the platform-detecting shell script.
|
|
6
17
|
Authoritative URL for the tarball is derived from the inbound request
|
|
@@ -9,6 +20,12 @@ on). Program name is the bundler-emitted `belte:cli-name` value.
|
|
|
9
20
|
*/
|
|
10
21
|
export function handleCliInstall(request: Request, programName: string): Response {
|
|
11
22
|
const url = new URL(request.url)
|
|
23
|
+
if (!SAFE_HOST.test(url.host)) {
|
|
24
|
+
return new Response('Bad Request', {
|
|
25
|
+
status: 400,
|
|
26
|
+
headers: { 'Cache-Control': NO_STORE },
|
|
27
|
+
})
|
|
28
|
+
}
|
|
12
29
|
const appUrl = `${url.protocol}//${url.host}`
|
|
13
30
|
const script = installScript(appUrl, programName)
|
|
14
31
|
return new Response(script, {
|
package/src/lib/server/error.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { NO_STORE } from '../shared/cacheControlValues.ts'
|
|
2
2
|
import type { TypedResponse } from './rpc/types/TypedResponse.ts'
|
|
3
|
+
import { withResponseDefaults } from './runtime/withResponseDefaults.ts'
|
|
3
4
|
|
|
4
5
|
/*
|
|
5
6
|
Plain-text error Response — clearer than constructing a Response by
|
|
@@ -11,7 +12,9 @@ returns the message, no parsing).
|
|
|
11
12
|
|
|
12
13
|
`message` defaults to the status's standard reason phrase when
|
|
13
14
|
omitted (e.g. `error(404)` body = 'Not Found'). The body is
|
|
14
|
-
text/plain so intermediaries don't try to render or sniff it.
|
|
15
|
+
text/plain so intermediaries don't try to render or sniff it. A final
|
|
16
|
+
`ResponseInit` adds headers (e.g. `Retry-After` on a 429); the positional
|
|
17
|
+
`status` always wins over any `init.status`.
|
|
15
18
|
|
|
16
19
|
To short-circuit a handler instead of returning, `throw new Error(...)`
|
|
17
20
|
or `throw new HttpError(error(...))` — the framework's `app.handleError`
|
|
@@ -19,6 +22,13 @@ hook catches thrown errors. This helper deliberately returns a Response
|
|
|
19
22
|
rather than throwing one so a single `return error(...)` is the
|
|
20
23
|
expected pattern, with the same control flow as `return json(...)`.
|
|
21
24
|
*/
|
|
25
|
+
|
|
26
|
+
/*
|
|
27
|
+
Standard reason phrases for the statuses error() is realistically called
|
|
28
|
+
with. Maintained explicitly because Bun's `Response` does not populate
|
|
29
|
+
`statusText` from the status code, so there's no platform table to read.
|
|
30
|
+
Unlisted codes fall back to `HTTP <status>`.
|
|
31
|
+
*/
|
|
22
32
|
const STATUS_TEXT: Record<number, string> = {
|
|
23
33
|
400: 'Bad Request',
|
|
24
34
|
401: 'Unauthorized',
|
|
@@ -44,13 +54,17 @@ the union of branches in a handler narrow to whatever the success
|
|
|
44
54
|
branch carries (`TypedResponse<{user}> | TypedResponse<never>` → Return
|
|
45
55
|
= {user}).
|
|
46
56
|
*/
|
|
47
|
-
export function error(status: number, message?: string): TypedResponse<never> {
|
|
57
|
+
export function error(status: number, message?: string, init?: ResponseInit): TypedResponse<never> {
|
|
48
58
|
const body = message ?? STATUS_TEXT[status] ?? `HTTP ${status}`
|
|
49
|
-
return new Response(
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
59
|
+
return new Response(
|
|
60
|
+
body,
|
|
61
|
+
withResponseDefaults(
|
|
62
|
+
init,
|
|
63
|
+
{
|
|
64
|
+
'Content-Type': 'text/plain; charset=utf-8',
|
|
65
|
+
'Cache-Control': NO_STORE,
|
|
66
|
+
},
|
|
67
|
+
status,
|
|
68
|
+
),
|
|
69
|
+
) as TypedResponse<never>
|
|
56
70
|
}
|
package/src/lib/server/json.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { NO_STORE } from '../shared/cacheControlValues.ts'
|
|
2
2
|
import type { TypedResponse } from './rpc/types/TypedResponse.ts'
|
|
3
|
+
import { withResponseDefaults } from './runtime/withResponseDefaults.ts'
|
|
3
4
|
|
|
4
5
|
/*
|
|
5
6
|
JSON Response with rpc-friendly defaults — same shape as
|
|
@@ -20,9 +21,8 @@ For non-default cache policy pass `init.headers`; explicit
|
|
|
20
21
|
`cache-control` wins over the default.
|
|
21
22
|
*/
|
|
22
23
|
export function json<T>(data: T, init?: ResponseInit): TypedResponse<T> {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
return Response.json(data, { ...init, headers }) as TypedResponse<T>
|
|
24
|
+
return Response.json(
|
|
25
|
+
data,
|
|
26
|
+
withResponseDefaults(init, { 'Cache-Control': NO_STORE }),
|
|
27
|
+
) as TypedResponse<T>
|
|
28
28
|
}
|
package/src/lib/server/jsonl.ts
CHANGED
|
@@ -24,17 +24,22 @@ message crosses the wire.
|
|
|
24
24
|
import { NO_STORE } from '../shared/cacheControlValues.ts'
|
|
25
25
|
import type { TypedResponse } from './rpc/types/TypedResponse.ts'
|
|
26
26
|
import { streamFromIterator } from './runtime/streamFromIterator.ts'
|
|
27
|
+
import { withResponseDefaults } from './runtime/withResponseDefaults.ts'
|
|
27
28
|
|
|
28
|
-
export function jsonl<Frame>(
|
|
29
|
+
export function jsonl<Frame>(
|
|
30
|
+
iterable: AsyncIterable<Frame>,
|
|
31
|
+
init?: ResponseInit,
|
|
32
|
+
): TypedResponse<Frame> {
|
|
29
33
|
const body = streamFromIterator(iterable, {
|
|
30
34
|
encodeFrame: (value) => `${JSON.stringify(value)}\n`,
|
|
31
35
|
encodeError: (message) => `${JSON.stringify({ $error: message })}\n`,
|
|
32
36
|
})
|
|
33
|
-
return new Response(
|
|
34
|
-
|
|
37
|
+
return new Response(
|
|
38
|
+
body,
|
|
39
|
+
withResponseDefaults(init, {
|
|
35
40
|
'Content-Type': 'application/jsonl; charset=utf-8',
|
|
36
41
|
'Cache-Control': NO_STORE,
|
|
37
42
|
'X-Content-Type-Options': 'nosniff',
|
|
38
|
-
},
|
|
39
|
-
|
|
43
|
+
}),
|
|
44
|
+
) as TypedResponse<Frame>
|
|
40
45
|
}
|
|
@@ -3,11 +3,11 @@ import type { Prompt } from './types/Prompt.ts'
|
|
|
3
3
|
import type { PromptOptions } from './types/PromptOptions.ts'
|
|
4
4
|
|
|
5
5
|
/*
|
|
6
|
-
Builds a Prompt from a name + options. The
|
|
7
|
-
`
|
|
8
|
-
|
|
9
|
-
prompt's identity. Registers itself so the MCP dispatcher
|
|
10
|
-
and render it.
|
|
6
|
+
Builds a Prompt from a name + options. The resolver plugin parses every
|
|
7
|
+
`src/mcp/prompts/<file>.md` and generates a module that calls
|
|
8
|
+
`definePrompt("<name>", { description, jsonSchema, render })`, so the file
|
|
9
|
+
path becomes the prompt's identity. Registers itself so the MCP dispatcher
|
|
10
|
+
can enumerate and render it.
|
|
11
11
|
*/
|
|
12
12
|
export function definePrompt(name: string, opts: PromptOptions): Prompt {
|
|
13
13
|
const self: Prompt = {
|
|
@@ -15,6 +15,6 @@ export function definePrompt(name: string, opts: PromptOptions): Prompt {
|
|
|
15
15
|
description: opts.description,
|
|
16
16
|
render: opts.render,
|
|
17
17
|
}
|
|
18
|
-
registerPrompt({ prompt: self,
|
|
18
|
+
registerPrompt({ prompt: self, jsonSchema: opts.jsonSchema })
|
|
19
19
|
return self
|
|
20
20
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// `{{name}}` placeholder, surrounding whitespace tolerated, names are
|
|
2
|
+
// word chars or hyphens to match valid MCP argument identifiers.
|
|
3
|
+
const PLACEHOLDER = /\{\{\s*([\w-]+)\s*\}\}/g
|
|
4
|
+
|
|
5
|
+
/*
|
|
6
|
+
Renders a markdown prompt body by substituting each `{{name}}` placeholder
|
|
7
|
+
with the matching argument value. Missing arguments collapse to an empty
|
|
8
|
+
string — MCP only enforces `required` at the client, so an optional
|
|
9
|
+
argument the model omits should simply vanish from the text. Called by the
|
|
10
|
+
render closure the resolver plugin generates for every `.md` prompt.
|
|
11
|
+
*/
|
|
12
|
+
export function renderPromptTemplate(template: string, args: Record<string, string>): string {
|
|
13
|
+
return template.replace(PLACEHOLDER, (_match, key: string) =>
|
|
14
|
+
args[key] === undefined ? '' : String(args[key]),
|
|
15
|
+
)
|
|
16
|
+
}
|
|
@@ -1,14 +1,13 @@
|
|
|
1
|
-
import type { PromptMessage } from './PromptMessage.ts'
|
|
2
|
-
|
|
3
1
|
/*
|
|
4
|
-
An MCP prompt declared
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
2
|
+
An MCP prompt declared by a markdown file under `src/mcp/prompts/`. The
|
|
3
|
+
resolver plugin parses the file's frontmatter + body and generates a call
|
|
4
|
+
to definePrompt, stamping in the `name` from the file path; `render(args)`
|
|
5
|
+
interpolates the body's `{{name}}` placeholders into the single user
|
|
6
|
+
message returned by `prompts/get`. Prompts are MCP-only — there is no
|
|
7
|
+
client-side counterpart, so the shape carries no ClientFlags.
|
|
9
8
|
*/
|
|
10
|
-
export type Prompt
|
|
9
|
+
export type Prompt = {
|
|
11
10
|
readonly name: string
|
|
12
11
|
readonly description: string | undefined
|
|
13
|
-
render(args:
|
|
12
|
+
render(args: Record<string, string>): string
|
|
14
13
|
}
|
|
@@ -1,17 +1,12 @@
|
|
|
1
|
-
import type { StandardSchemaV1 } from '../../rpc/types/StandardSchemaV1.ts'
|
|
2
|
-
import type { PromptMessage } from './PromptMessage.ts'
|
|
3
|
-
|
|
4
1
|
/*
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
All of this is server-only — prompts are never imported by client code.
|
|
2
|
+
Options definePrompt receives for one markdown prompt. The resolver plugin
|
|
3
|
+
generates this object from the file: `description` + `jsonSchema` come from
|
|
4
|
+
the frontmatter (the schema built from the `arguments` list), and `render`
|
|
5
|
+
closes over the parsed body. All of this is server-only — prompts are never
|
|
6
|
+
imported by client code.
|
|
11
7
|
*/
|
|
12
|
-
export type PromptOptions
|
|
8
|
+
export type PromptOptions = {
|
|
13
9
|
description?: string
|
|
14
|
-
schema?: StandardSchemaV1
|
|
15
10
|
jsonSchema?: Record<string, unknown>
|
|
16
|
-
render: (args:
|
|
11
|
+
render: (args: Record<string, string>) => string
|
|
17
12
|
}
|
|
@@ -1,15 +1,13 @@
|
|
|
1
|
-
import type { StandardSchemaV1 } from '../../rpc/types/StandardSchemaV1.ts'
|
|
2
1
|
import type { Prompt } from './Prompt.ts'
|
|
3
2
|
|
|
4
3
|
/*
|
|
5
4
|
Per-prompt registry record. The MCP dispatcher enumerates this to build
|
|
6
|
-
`prompts/list` (description + arguments from the
|
|
7
|
-
`prompts/get` (
|
|
8
|
-
jsonSchema
|
|
5
|
+
`prompts/list` (description + arguments from the JSON Schema) and to
|
|
6
|
+
dispatch `prompts/get` (render the body with the caller's arguments).
|
|
7
|
+
jsonSchema stays off the public Prompt shape so the render closure isn't
|
|
9
8
|
burdened with metadata it never reads.
|
|
10
9
|
*/
|
|
11
10
|
export type PromptRegistryEntry = {
|
|
12
11
|
prompt: Prompt
|
|
13
|
-
schema: StandardSchemaV1 | undefined
|
|
14
12
|
jsonSchema: Record<string, unknown> | undefined
|
|
15
13
|
}
|
|
@@ -2,9 +2,9 @@ import type { Prompt } from './Prompt.ts'
|
|
|
2
2
|
|
|
3
3
|
/*
|
|
4
4
|
Manifest of prompt-name → module loader. Produced by the resolver plugin
|
|
5
|
-
from each `.
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
dispatcher can enumerate the full prompt surface.
|
|
5
|
+
from each `.md` under src/mcp/prompts/. Each markdown file is transformed
|
|
6
|
+
into a module that registers one Prompt (its `.name` stamped in by the
|
|
7
|
+
generated definePrompt call) on import. The registry loader imports every
|
|
8
|
+
module once so the MCP dispatcher can enumerate the full prompt surface.
|
|
9
9
|
*/
|
|
10
10
|
export type PromptRoutes = Record<string, () => Promise<Record<string, Prompt>>>
|
|
@@ -14,9 +14,13 @@ Status guidance:
|
|
|
14
14
|
- 303 — "after a POST, GET this" (forces GET on the follow-up)
|
|
15
15
|
- 307 — temporary, preserve method
|
|
16
16
|
- 308 — permanent, preserve method
|
|
17
|
+
|
|
18
|
+
A final `ResponseInit` adds headers (e.g. a `Set-Cookie` on the redirect);
|
|
19
|
+
the positional `status` always wins over any `init.status`.
|
|
17
20
|
*/
|
|
18
21
|
import { NO_STORE } from '../shared/cacheControlValues.ts'
|
|
19
22
|
import type { TypedResponse } from './rpc/types/TypedResponse.ts'
|
|
23
|
+
import { withResponseDefaults } from './runtime/withResponseDefaults.ts'
|
|
20
24
|
|
|
21
25
|
type RedirectStatus = 301 | 302 | 303 | 307 | 308
|
|
22
26
|
|
|
@@ -26,12 +30,13 @@ the wire response is a 3xx with no body the caller resolves to, so it
|
|
|
26
30
|
must not pollute the inferred `Return` of a route that conditionally
|
|
27
31
|
redirects vs returns json.
|
|
28
32
|
*/
|
|
29
|
-
export function redirect(
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
33
|
+
export function redirect(
|
|
34
|
+
url: string,
|
|
35
|
+
status: RedirectStatus = 302,
|
|
36
|
+
init?: ResponseInit,
|
|
37
|
+
): TypedResponse<never> {
|
|
38
|
+
return new Response(
|
|
39
|
+
null,
|
|
40
|
+
withResponseDefaults(init, { Location: url, 'Cache-Control': NO_STORE }, status),
|
|
41
|
+
) as TypedResponse<never>
|
|
37
42
|
}
|
|
@@ -77,9 +77,10 @@ export function defineVerb<Args, Return>(
|
|
|
77
77
|
|
|
78
78
|
/*
|
|
79
79
|
`getRequest` is unused on the server path — handlers receive parsed
|
|
80
|
-
`args` directly
|
|
81
|
-
|
|
82
|
-
to allocate one per
|
|
80
|
+
`args` directly and reach the inbound Request via `request()`.
|
|
81
|
+
createRemoteFunction passes a thunk so the client side can lazily
|
|
82
|
+
synthesize its Request without forcing the server to allocate one per
|
|
83
|
+
SSR call.
|
|
83
84
|
*/
|
|
84
85
|
function invoke(args: Args | undefined): Promise<Response> {
|
|
85
86
|
return schema ? validateThenHandle(args) : runHandler(args)
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { commandNameForUrl } from '../../shared/commandNameForUrl.ts'
|
|
2
|
+
import type { VerbRegistryEntry } from './types/VerbRegistryEntry.ts'
|
|
3
|
+
import { verbRegistry } from './verbRegistry.ts'
|
|
4
|
+
|
|
5
|
+
/*
|
|
6
|
+
Finds the registered verb whose URL maps to a given command name (folder
|
|
7
|
+
segments joined with `-`, per commandNameForUrl). The CLI client proxy and
|
|
8
|
+
the MCP tool dispatcher both key off this name, so the scan lives here once
|
|
9
|
+
rather than being re-implemented — and reused — at each call site.
|
|
10
|
+
*/
|
|
11
|
+
export function findVerbByCommandName(name: string): VerbRegistryEntry | undefined {
|
|
12
|
+
for (const entry of verbRegistry.values()) {
|
|
13
|
+
if (commandNameForUrl(entry.remote.url) === name) {
|
|
14
|
+
return entry
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return undefined
|
|
18
|
+
}
|
|
@@ -19,7 +19,7 @@ stored entry — the decode just happens on the way out for callers of
|
|
|
19
19
|
SSE / JSONL handlers yield each frame; non-streaming handlers yield the
|
|
20
20
|
decoded body once then complete. The result is a Subscribable, so it
|
|
21
21
|
can be passed to subscribe() and shared across reactive consumers.
|
|
22
|
-
For sustained broadcast / pub-sub use the `belte/
|
|
22
|
+
For sustained broadcast / pub-sub use the `belte/server/socket` primitive —
|
|
23
23
|
HTTP rpc isn't the place for long-lived multi-publisher subscriptions.
|
|
24
24
|
`.fetch(req)` is the framework's request-dispatch entry point — used by
|
|
25
25
|
the router to invoke the handler from an incoming HTTP request, not
|
|
@@ -16,6 +16,10 @@ verb helper can infer `Return` automatically — no need to annotate
|
|
|
16
16
|
`GET<Args, Return>` when the handler returns one of the respond helpers.
|
|
17
17
|
A bare `new Response(...)` is still acceptable: the brand is optional, so
|
|
18
18
|
untagged Responses simply fall back to `Return = unknown`.
|
|
19
|
+
|
|
20
|
+
Handlers that need the inbound Request (headers, `request.signal`, …) read
|
|
21
|
+
it via `request()` from `belte/server` rather than a handler parameter, so
|
|
22
|
+
the signature stays a single parsed-`args` bag.
|
|
19
23
|
*/
|
|
20
24
|
export type RemoteHandler<Args, Return> = (
|
|
21
25
|
args: Args,
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Whether the client advertised zstd in Accept-Encoding. Both static-asset
|
|
3
|
+
servers (the `/_app/` chunk server and the public/ server) gate their
|
|
4
|
+
pre-compressed responses on this, so the check lives in one place.
|
|
5
|
+
*/
|
|
6
|
+
export function acceptsZstd(req: Request): boolean {
|
|
7
|
+
return (req.headers.get('accept-encoding') ?? '').toLowerCase().includes('zstd')
|
|
8
|
+
}
|
|
@@ -41,8 +41,10 @@ export function buildOpenApiSpec(info: {
|
|
|
41
41
|
const url = entry.remote.url
|
|
42
42
|
const method = entry.remote.method
|
|
43
43
|
const jsonSchema = jsonSchemaForSchema(entry.schema, entry.jsonSchema)
|
|
44
|
+
const description = jsonSchema.description as string | undefined
|
|
44
45
|
const operation: Record<string, unknown> = {
|
|
45
46
|
operationId: commandNameForUrl(url),
|
|
47
|
+
...(description ? { description } : {}),
|
|
46
48
|
responses: { '200': { description: 'OK' } },
|
|
47
49
|
}
|
|
48
50
|
if (BODY_METHODS.has(method)) {
|