@briancray/belte 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/belte.ts +25 -12
- package/package.json +2 -1
- package/src/appEntry.ts +124 -0
- package/src/belteResolverPlugin.ts +217 -194
- 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/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/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 +2 -1
- 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
|
@@ -3,16 +3,18 @@ import { canonicalJson } from './canonicalJson.ts'
|
|
|
3
3
|
|
|
4
4
|
/*
|
|
5
5
|
Derives a cache key from a verb-defined remote function and its args. The
|
|
6
|
-
prefix is `${method} ${url}` where `url` is the route template. GET/DELETE
|
|
6
|
+
prefix is `${method} ${url}` where `url` is the route template. GET/DELETE/HEAD
|
|
7
7
|
serialise args onto the URL as `?key=value` with keys sorted so the order
|
|
8
8
|
the caller assembled the object doesn't change the key; POST/PUT/PATCH join
|
|
9
|
-
args after a space as canonical JSON.
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
args after a space as canonical JSON. The verb split mirrors buildRpcRequest
|
|
10
|
+
exactly so the key and the synthesized Request can't disagree. Sorted
|
|
11
|
+
key/value pairs are walked once and concatenated directly so the hot
|
|
12
|
+
GET-cache path doesn't allocate per intermediate (entries / filtered /
|
|
13
|
+
URLSearchParams).
|
|
12
14
|
*/
|
|
13
15
|
export function keyForRemoteCall(method: HttpVerb, url: string, args: unknown): string {
|
|
14
16
|
const prefix = `${method} ${url}`
|
|
15
|
-
if (method === 'GET' || method === 'DELETE') {
|
|
17
|
+
if (method === 'GET' || method === 'DELETE' || method === 'HEAD') {
|
|
16
18
|
if (args && typeof args === 'object' && !Array.isArray(args)) {
|
|
17
19
|
const record = args as Record<string, unknown>
|
|
18
20
|
const keys = Object.keys(record).sort()
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { PromptArgument } from './types/PromptArgument.ts'
|
|
2
|
+
|
|
3
|
+
export type ParsedPromptMarkdown = {
|
|
4
|
+
description: string | undefined
|
|
5
|
+
arguments: PromptArgument[]
|
|
6
|
+
body: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
// Leading YAML frontmatter block fenced by `---` lines (CRLF tolerant).
|
|
10
|
+
const FRONTMATTER = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/
|
|
11
|
+
|
|
12
|
+
/*
|
|
13
|
+
Splits a `src/mcp/prompts/**.md` file into its frontmatter metadata and
|
|
14
|
+
template body. The frontmatter (optional) carries `description` and an
|
|
15
|
+
`arguments` list; everything after the closing `---` is the prompt body,
|
|
16
|
+
interpolated at render time via `{{name}}` placeholders. A file with no
|
|
17
|
+
frontmatter is all body. Parsed with Bun.YAML — the resolver plugin runs
|
|
18
|
+
under Bun, so the native parser is always available at build time.
|
|
19
|
+
*/
|
|
20
|
+
export function parsePromptMarkdown(source: string): ParsedPromptMarkdown {
|
|
21
|
+
const match = FRONTMATTER.exec(source)
|
|
22
|
+
if (!match) {
|
|
23
|
+
return { description: undefined, arguments: [], body: source.trim() }
|
|
24
|
+
}
|
|
25
|
+
const frontmatter = (Bun.YAML.parse(match[1]) ?? {}) as {
|
|
26
|
+
description?: string
|
|
27
|
+
arguments?: PromptArgument[]
|
|
28
|
+
}
|
|
29
|
+
return {
|
|
30
|
+
description: frontmatter.description,
|
|
31
|
+
arguments: Array.isArray(frontmatter.arguments) ? frontmatter.arguments : [],
|
|
32
|
+
body: source.slice(match[0].length).trim(),
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/*
|
|
2
|
-
Translates a prompt file path under `src/
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
2
|
+
Translates a prompt file path under `src/mcp/prompts/` into the prompt's
|
|
3
|
+
MCP name. Strips `.md` and joins nested folder segments with `-` (e.g.
|
|
4
|
+
`code/review.md` → `code-review`) so two prompts with the same stem in
|
|
5
|
+
different folders don't collide and the name stays a single valid MCP
|
|
6
6
|
prompt identifier.
|
|
7
7
|
*/
|
|
8
8
|
export function promptNameForFile(relativePath: string): string {
|
|
9
|
-
return relativePath.replace(/\.
|
|
9
|
+
return relativePath.replace(/\.md$/, '').replaceAll('/', '-')
|
|
10
10
|
}
|
|
@@ -24,7 +24,7 @@ mirroring the plain `fn(args)` decode path.
|
|
|
24
24
|
*/
|
|
25
25
|
function streamResponse<T>(response: Response): AsyncIterable<T> {
|
|
26
26
|
if (!response.ok) {
|
|
27
|
-
return errorIterable(new HttpError(response))
|
|
27
|
+
return errorIterable<T>(new HttpError(response))
|
|
28
28
|
}
|
|
29
29
|
const contentType = (response.headers.get('content-type') ?? '').toLowerCase()
|
|
30
30
|
if (contentType.startsWith('text/event-stream')) {
|
|
@@ -36,25 +36,9 @@ function streamResponse<T>(response: Response): AsyncIterable<T> {
|
|
|
36
36
|
return oneShot<T>(response)
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
let done = false
|
|
43
|
-
return {
|
|
44
|
-
async next() {
|
|
45
|
-
if (done) {
|
|
46
|
-
return { value: undefined, done: true }
|
|
47
|
-
}
|
|
48
|
-
done = true
|
|
49
|
-
throw error
|
|
50
|
-
},
|
|
51
|
-
async return() {
|
|
52
|
-
done = true
|
|
53
|
-
return { value: undefined, done: true }
|
|
54
|
-
},
|
|
55
|
-
}
|
|
56
|
-
},
|
|
57
|
-
}
|
|
39
|
+
/* Surfaces a non-2xx response (or any pre-stream failure) as a thrown error on the first pull. */
|
|
40
|
+
async function* errorIterable<T>(error: Error): AsyncGenerator<T> {
|
|
41
|
+
throw error
|
|
58
42
|
}
|
|
59
43
|
|
|
60
44
|
/*
|
|
@@ -64,120 +48,84 @@ completes. Makes `fn.stream(args)` symmetrical across streaming and
|
|
|
64
48
|
non-streaming handlers — callers can pick the iteration shape without
|
|
65
49
|
worrying about which body the handler returned.
|
|
66
50
|
*/
|
|
67
|
-
function oneShot<T>(response: Response):
|
|
68
|
-
|
|
69
|
-
[Symbol.asyncIterator](): AsyncIterator<T, void, undefined> {
|
|
70
|
-
let yielded = false
|
|
71
|
-
return {
|
|
72
|
-
async next() {
|
|
73
|
-
if (yielded) {
|
|
74
|
-
return { value: undefined, done: true }
|
|
75
|
-
}
|
|
76
|
-
yielded = true
|
|
77
|
-
const value = (await decodeResponse(response)) as T
|
|
78
|
-
return { value, done: false }
|
|
79
|
-
},
|
|
80
|
-
async return() {
|
|
81
|
-
yielded = true
|
|
82
|
-
return { value: undefined, done: true }
|
|
83
|
-
},
|
|
84
|
-
}
|
|
85
|
-
},
|
|
86
|
-
}
|
|
51
|
+
async function* oneShot<T>(response: Response): AsyncGenerator<T> {
|
|
52
|
+
yield (await decodeResponse(response)) as T
|
|
87
53
|
}
|
|
88
54
|
|
|
89
55
|
/*
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
56
|
+
Reads a streaming text Response and yields raw frame strings split on
|
|
57
|
+
`delimiter` (`\n\n` for SSE events, `\n` for JSON lines). Owns the whole
|
|
58
|
+
buffering lifecycle: incremental decode, amortised-O(n) compaction, a
|
|
59
|
+
final flush of the trailing partial frame, and reader cancellation when
|
|
60
|
+
the consumer stops iterating (the generator's `finally` runs on
|
|
61
|
+
`return()`). The SSE and jsonl parsers layer their per-frame parsing on
|
|
62
|
+
top of this single machine so the two can't drift.
|
|
96
63
|
*/
|
|
97
|
-
function
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
while (pending.length === 0 && !done) {
|
|
112
|
-
const { value, done: streamDone } = await reader.read()
|
|
113
|
-
if (streamDone) {
|
|
114
|
-
done = true
|
|
115
|
-
if (bufferStart < buffer.length) {
|
|
116
|
-
const frame = parseFrame(buffer.slice(bufferStart))
|
|
117
|
-
if (frame) {
|
|
118
|
-
pending.push(frame)
|
|
119
|
-
}
|
|
120
|
-
buffer = ''
|
|
121
|
-
bufferStart = 0
|
|
122
|
-
}
|
|
123
|
-
return
|
|
124
|
-
}
|
|
125
|
-
/*
|
|
126
|
-
Compact only when the unread region is small relative to
|
|
127
|
-
the consumed prefix — keeps amortised work O(n) instead
|
|
128
|
-
of quadratic slicing per frame boundary.
|
|
129
|
-
*/
|
|
130
|
-
if (bufferStart > buffer.length / 2) {
|
|
131
|
-
buffer = buffer.slice(bufferStart) + value
|
|
132
|
-
bufferStart = 0
|
|
133
|
-
} else {
|
|
134
|
-
buffer += value
|
|
135
|
-
}
|
|
136
|
-
let boundary = buffer.indexOf('\n\n', bufferStart)
|
|
137
|
-
while (boundary !== -1) {
|
|
138
|
-
const raw = buffer.slice(bufferStart, boundary)
|
|
139
|
-
bufferStart = boundary + 2
|
|
140
|
-
const frame = parseFrame(raw)
|
|
141
|
-
if (frame) {
|
|
142
|
-
pending.push(frame)
|
|
143
|
-
}
|
|
144
|
-
boundary = buffer.indexOf('\n\n', bufferStart)
|
|
145
|
-
}
|
|
64
|
+
async function* frameReader(response: Response, delimiter: string): AsyncGenerator<string> {
|
|
65
|
+
const body = response.body
|
|
66
|
+
if (!body) {
|
|
67
|
+
return
|
|
68
|
+
}
|
|
69
|
+
const reader = body.pipeThrough(new TextDecoderStream()).getReader()
|
|
70
|
+
let buffer = ''
|
|
71
|
+
let bufferStart = 0
|
|
72
|
+
try {
|
|
73
|
+
while (true) {
|
|
74
|
+
const { value, done } = await reader.read()
|
|
75
|
+
if (done) {
|
|
76
|
+
if (bufferStart < buffer.length) {
|
|
77
|
+
yield buffer.slice(bufferStart)
|
|
146
78
|
}
|
|
79
|
+
return
|
|
80
|
+
}
|
|
81
|
+
/*
|
|
82
|
+
Compact only when the unread region is small relative to the
|
|
83
|
+
consumed prefix — keeps amortised work O(n) instead of
|
|
84
|
+
quadratic slicing per frame boundary.
|
|
85
|
+
*/
|
|
86
|
+
if (bufferStart > buffer.length / 2) {
|
|
87
|
+
buffer = buffer.slice(bufferStart) + value
|
|
88
|
+
bufferStart = 0
|
|
89
|
+
} else {
|
|
90
|
+
buffer += value
|
|
147
91
|
}
|
|
92
|
+
let boundary = buffer.indexOf(delimiter, bufferStart)
|
|
93
|
+
while (boundary !== -1) {
|
|
94
|
+
yield buffer.slice(bufferStart, boundary)
|
|
95
|
+
bufferStart = boundary + delimiter.length
|
|
96
|
+
boundary = buffer.indexOf(delimiter, bufferStart)
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
} finally {
|
|
100
|
+
await reader.cancel().catch(() => undefined)
|
|
101
|
+
}
|
|
102
|
+
}
|
|
148
103
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
await pullFrames()
|
|
172
|
-
}
|
|
173
|
-
},
|
|
174
|
-
async return() {
|
|
175
|
-
done = true
|
|
176
|
-
await reader.cancel().catch(() => undefined)
|
|
177
|
-
return { value: undefined, done: true }
|
|
178
|
-
},
|
|
104
|
+
/*
|
|
105
|
+
SSE parser: yields the JSON-parsed `data` payload of each `event:`/`data:`
|
|
106
|
+
frame. The `sse()` respond helper emits an `event: error\ndata:
|
|
107
|
+
{"message":...}` frame when the source generator throws, which we surface
|
|
108
|
+
as a thrown Error so consumer loops can react to mid-stream failure
|
|
109
|
+
rather than silently stopping.
|
|
110
|
+
*/
|
|
111
|
+
async function* parseSse<T>(response: Response): AsyncGenerator<T> {
|
|
112
|
+
for await (const raw of frameReader(response, '\n\n')) {
|
|
113
|
+
const frame = parseFrame(raw)
|
|
114
|
+
if (!frame) {
|
|
115
|
+
continue
|
|
116
|
+
}
|
|
117
|
+
if (frame.event === 'error') {
|
|
118
|
+
try {
|
|
119
|
+
const decoded = JSON.parse(frame.data) as { message?: string }
|
|
120
|
+
throw new Error(decoded?.message ?? 'sse stream error')
|
|
121
|
+
} catch (err) {
|
|
122
|
+
if (err instanceof SyntaxError) {
|
|
123
|
+
throw new Error(frame.data || 'sse stream error')
|
|
124
|
+
}
|
|
125
|
+
throw err
|
|
179
126
|
}
|
|
180
|
-
}
|
|
127
|
+
}
|
|
128
|
+
yield JSON.parse(frame.data) as T
|
|
181
129
|
}
|
|
182
130
|
}
|
|
183
131
|
|
|
@@ -205,96 +153,22 @@ function parseFrame(raw: string): { event: string; data: string } | undefined {
|
|
|
205
153
|
}
|
|
206
154
|
|
|
207
155
|
/*
|
|
208
|
-
JSONL/NDJSON parser:
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
156
|
+
JSONL/NDJSON parser: parses each non-empty line as JSON and yields the
|
|
157
|
+
value. The `jsonl()` respond helper emits a trailing
|
|
158
|
+
`{"$error":"<message>"}` line when the source generator throws — that's
|
|
159
|
+
surfaced here as a thrown Error so consumer loops can react to mid-stream
|
|
160
|
+
failure.
|
|
213
161
|
*/
|
|
214
|
-
function parseJsonLines<T>(response: Response):
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
const pending: string[] = []
|
|
225
|
-
let done = false
|
|
226
|
-
|
|
227
|
-
async function pullLines(): Promise<void> {
|
|
228
|
-
while (pending.length === 0 && !done) {
|
|
229
|
-
const { value, done: streamDone } = await reader.read()
|
|
230
|
-
if (streamDone) {
|
|
231
|
-
done = true
|
|
232
|
-
if (bufferStart < buffer.length) {
|
|
233
|
-
pending.push(buffer.slice(bufferStart))
|
|
234
|
-
buffer = ''
|
|
235
|
-
bufferStart = 0
|
|
236
|
-
}
|
|
237
|
-
return
|
|
238
|
-
}
|
|
239
|
-
if (bufferStart > buffer.length / 2) {
|
|
240
|
-
buffer = buffer.slice(bufferStart) + value
|
|
241
|
-
bufferStart = 0
|
|
242
|
-
} else {
|
|
243
|
-
buffer += value
|
|
244
|
-
}
|
|
245
|
-
let newline = buffer.indexOf('\n', bufferStart)
|
|
246
|
-
while (newline !== -1) {
|
|
247
|
-
const line = buffer.slice(bufferStart, newline)
|
|
248
|
-
bufferStart = newline + 1
|
|
249
|
-
if (line.length > 0) {
|
|
250
|
-
pending.push(line)
|
|
251
|
-
}
|
|
252
|
-
newline = buffer.indexOf('\n', bufferStart)
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
return {
|
|
258
|
-
async next() {
|
|
259
|
-
while (true) {
|
|
260
|
-
if (pending.length > 0) {
|
|
261
|
-
const line = pending.shift() as string
|
|
262
|
-
const parsed = JSON.parse(line) as Record<string, unknown> & {
|
|
263
|
-
$error?: string
|
|
264
|
-
}
|
|
265
|
-
if (
|
|
266
|
-
parsed &&
|
|
267
|
-
typeof parsed === 'object' &&
|
|
268
|
-
typeof parsed.$error === 'string'
|
|
269
|
-
) {
|
|
270
|
-
throw new Error(parsed.$error)
|
|
271
|
-
}
|
|
272
|
-
return { value: parsed as T, done: false }
|
|
273
|
-
}
|
|
274
|
-
if (done) {
|
|
275
|
-
return { value: undefined, done: true }
|
|
276
|
-
}
|
|
277
|
-
await pullLines()
|
|
278
|
-
}
|
|
279
|
-
},
|
|
280
|
-
async return() {
|
|
281
|
-
done = true
|
|
282
|
-
await reader.cancel().catch(() => undefined)
|
|
283
|
-
return { value: undefined, done: true }
|
|
284
|
-
},
|
|
285
|
-
}
|
|
286
|
-
},
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
function emptyIterator<T>(): AsyncIterator<T, void, undefined> {
|
|
291
|
-
return {
|
|
292
|
-
async next() {
|
|
293
|
-
return { value: undefined, done: true }
|
|
294
|
-
},
|
|
295
|
-
async return() {
|
|
296
|
-
return { value: undefined, done: true }
|
|
297
|
-
},
|
|
162
|
+
async function* parseJsonLines<T>(response: Response): AsyncGenerator<T> {
|
|
163
|
+
for await (const raw of frameReader(response, '\n')) {
|
|
164
|
+
if (raw.length === 0) {
|
|
165
|
+
continue
|
|
166
|
+
}
|
|
167
|
+
const parsed = JSON.parse(raw) as Record<string, unknown> & { $error?: string }
|
|
168
|
+
if (parsed && typeof parsed === 'object' && typeof parsed.$error === 'string') {
|
|
169
|
+
throw new Error(parsed.$error)
|
|
170
|
+
}
|
|
171
|
+
yield parsed as T
|
|
298
172
|
}
|
|
299
173
|
}
|
|
300
174
|
|
|
@@ -315,15 +189,30 @@ export function subscribableFromResponse<T>(
|
|
|
315
189
|
name,
|
|
316
190
|
[Symbol.asyncIterator]() {
|
|
317
191
|
let inner: AsyncIterator<T, void, undefined> | undefined
|
|
192
|
+
let cancelled = false
|
|
318
193
|
return {
|
|
319
194
|
async next() {
|
|
195
|
+
if (cancelled) {
|
|
196
|
+
return { value: undefined, done: true }
|
|
197
|
+
}
|
|
320
198
|
if (!inner) {
|
|
321
199
|
const response = await fetchResponse()
|
|
322
200
|
inner = streamResponse<T>(response)[Symbol.asyncIterator]()
|
|
201
|
+
/*
|
|
202
|
+
If return() landed while we were awaiting the
|
|
203
|
+
fetch, `inner` was still undefined then so its
|
|
204
|
+
reader was never cancelled — release the body now
|
|
205
|
+
rather than leaving the HTTP stream open.
|
|
206
|
+
*/
|
|
207
|
+
if (cancelled) {
|
|
208
|
+
await inner.return?.(undefined)
|
|
209
|
+
return { value: undefined, done: true }
|
|
210
|
+
}
|
|
323
211
|
}
|
|
324
212
|
return inner.next()
|
|
325
213
|
},
|
|
326
214
|
async return() {
|
|
215
|
+
cancelled = true
|
|
327
216
|
await inner?.return?.(undefined)
|
|
328
217
|
return { value: undefined, done: true }
|
|
329
218
|
},
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/*
|
|
2
|
+
A single declared argument of a markdown prompt, parsed from the file's
|
|
3
|
+
YAML frontmatter `arguments:` list. `name` is the placeholder the body
|
|
4
|
+
interpolates via `{{name}}`; `description` + `required` feed the argument
|
|
5
|
+
list MCP advertises in `prompts/list`. Build-time only — markdown prompts
|
|
6
|
+
carry no runtime schema object, so this drives the generated JSON Schema.
|
|
7
|
+
*/
|
|
8
|
+
export type PromptArgument = {
|
|
9
|
+
name: string
|
|
10
|
+
description?: string
|
|
11
|
+
required?: boolean
|
|
12
|
+
}
|
|
@@ -9,13 +9,11 @@ when dispatching, so the page component sees `params.rest`, not
|
|
|
9
9
|
`params['*']`.
|
|
10
10
|
*/
|
|
11
11
|
function paramsForRoute(routeUrl: string): Record<string, 'string'> {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
}
|
|
18
|
-
return params
|
|
12
|
+
return Object.fromEntries(
|
|
13
|
+
parseRouteSegments(routeUrl)
|
|
14
|
+
.filter((segment) => segment.kind === 'param')
|
|
15
|
+
.map((segment) => [segment.name, 'string'] as const),
|
|
16
|
+
)
|
|
19
17
|
}
|
|
20
18
|
|
|
21
19
|
function renderParamsShape(shape: Record<string, 'string'>): string {
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { BunPlugin } from 'bun'
|
|
2
|
+
import { belteResolverPlugin } from './belteResolverPlugin.ts'
|
|
3
|
+
import type { SvelteConfig } from './lib/server/runtime/types/SvelteConfig.ts'
|
|
4
|
+
import { sveltePlugin } from './sveltePlugin.ts'
|
|
5
|
+
|
|
6
|
+
/*
|
|
7
|
+
The server-target Bun.build plugin pair shared by compile / buildCli /
|
|
8
|
+
bundleApp: the svelte loader (server generate) plus belte's virtual-module
|
|
9
|
+
resolver. `embedAssets` flips on the zstd asset embed used by the standalone
|
|
10
|
+
server binary; the CLI + launcher builds leave it off.
|
|
11
|
+
*/
|
|
12
|
+
export function serverBuildPlugins({
|
|
13
|
+
cwd,
|
|
14
|
+
svelteConfig,
|
|
15
|
+
embedAssets = false,
|
|
16
|
+
}: {
|
|
17
|
+
cwd: string
|
|
18
|
+
svelteConfig?: SvelteConfig
|
|
19
|
+
embedAssets?: boolean
|
|
20
|
+
}): BunPlugin[] {
|
|
21
|
+
return [
|
|
22
|
+
sveltePlugin({ generate: 'server', svelteConfig }),
|
|
23
|
+
belteResolverPlugin({ cwd, embedAssets, target: 'server' }),
|
|
24
|
+
]
|
|
25
|
+
}
|
package/src/serverEntry.ts
CHANGED
|
@@ -24,10 +24,14 @@ import { rpc } from './_virtual/rpc.ts'
|
|
|
24
24
|
import { shell } from './_virtual/shell.ts'
|
|
25
25
|
// @ts-expect-error virtual module resolved by belteResolverPlugin
|
|
26
26
|
import { sockets } from './_virtual/sockets.ts'
|
|
27
|
+
import { exitWithParent } from './lib/bundle/exitWithParent.ts'
|
|
27
28
|
import { createServer } from './lib/server/runtime/createServer.ts'
|
|
28
29
|
import { requestContext } from './lib/server/runtime/requestContext.ts'
|
|
29
30
|
import { setCacheStoreResolver } from './lib/shared/setCacheStoreResolver.ts'
|
|
30
31
|
|
|
32
|
+
// In a bundle, tie this server's life to the launcher's (no-op standalone).
|
|
33
|
+
exitWithParent()
|
|
34
|
+
|
|
31
35
|
setCacheStoreResolver(() => requestContext.getStore()?.cache)
|
|
32
36
|
|
|
33
37
|
await createServer({
|
package/template/package.json
CHANGED
package/src/lib/server/prompt.ts
DELETED
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import type { Prompt } from './prompts/types/Prompt.ts'
|
|
2
|
-
import type { PromptOptions } from './prompts/types/PromptOptions.ts'
|
|
3
|
-
import type { StandardSchemaV1 } from './rpc/types/StandardSchemaV1.ts'
|
|
4
|
-
|
|
5
|
-
/*
|
|
6
|
-
Declares an MCP prompt inside a file under `src/server/prompts/`. Each
|
|
7
|
-
file contains exactly one export, named after the file (e.g.
|
|
8
|
-
`summarize.ts` → `export const summarize = prompt(...)`). The bundler
|
|
9
|
-
reads the export name from the filename and the prompt name from the file
|
|
10
|
-
path under `src/server/prompts/`, then rewrites this call to bind the name
|
|
11
|
-
into definePrompt.
|
|
12
|
-
|
|
13
|
-
`render(args)` returns the messages MCP hands back for `prompts/get`:
|
|
14
|
-
either a bare string (one user message) or an explicit message array.
|
|
15
|
-
When `schema` is set, `Args` infers from `InferOutput<Schema>`, incoming
|
|
16
|
-
arguments validate against it, and MCP advertises the argument list in
|
|
17
|
-
`prompts/list`.
|
|
18
|
-
|
|
19
|
-
This function exists only for the type signature; calling it directly
|
|
20
|
-
means the bundler plugin didn't process the file, which throws.
|
|
21
|
-
*/
|
|
22
|
-
export function prompt<Schema extends StandardSchemaV1>(
|
|
23
|
-
opts: PromptOptions<StandardSchemaV1.InferOutput<Schema>> & { schema: Schema },
|
|
24
|
-
): Prompt<StandardSchemaV1.InferOutput<Schema>>
|
|
25
|
-
export function prompt<Args = Record<string, string>>(opts: PromptOptions<Args>): Prompt<Args>
|
|
26
|
-
export function prompt(_opts: PromptOptions): Prompt {
|
|
27
|
-
throw new Error(
|
|
28
|
-
'[belte] `prompt(...)` was called outside a prompts module — the prompt helper is only valid as the value of `export const <filename> = ...` inside a file under src/server/prompts/',
|
|
29
|
-
)
|
|
30
|
-
}
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
A single message in an MCP prompt's rendered output. `prompt({ render })`
|
|
3
|
-
returns either a bare string (sugar for one `user` message) or an array
|
|
4
|
-
of these. The dispatcher maps each into the MCP `prompts/get` wire shape
|
|
5
|
-
({ role, content: { type: 'text', text } }).
|
|
6
|
-
*/
|
|
7
|
-
export type PromptMessage = {
|
|
8
|
-
role: 'user' | 'assistant'
|
|
9
|
-
text: string
|
|
10
|
-
}
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import { findExportCallSite } from './findExportCallSite.ts'
|
|
2
|
-
import { stripImport } from './stripImport.ts'
|
|
3
|
-
|
|
4
|
-
const SINGLE_EXPORT_ERROR =
|
|
5
|
-
'[belte] prompts module contains more than one `prompt(...)` export — each file must declare exactly one prompt'
|
|
6
|
-
|
|
7
|
-
export type PreparedPromptModule = {
|
|
8
|
-
exportName: string
|
|
9
|
-
rewriteForServer: (name: string) => string
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
/*
|
|
13
|
-
Scans a `src/server/prompts/**` module once and returns its declared
|
|
14
|
-
export name plus a closure that, given the prompt name, emits the
|
|
15
|
-
server-side rewrite (`__belteDefinePrompt__("<name>", opts)` spliced into
|
|
16
|
-
the original source). Mirrors prepareSocketModule — a single tokenizer
|
|
17
|
-
pass so a `prompt` mention inside a string or comment is left alone.
|
|
18
|
-
*/
|
|
19
|
-
export function preparePromptModule(source: string): PreparedPromptModule | undefined {
|
|
20
|
-
const stripped = stripImport(source, 'belte/server/prompt')
|
|
21
|
-
const site = findExportCallSite(stripped, (ident) => ident === 'prompt', SINGLE_EXPORT_ERROR)
|
|
22
|
-
if (!site) {
|
|
23
|
-
return undefined
|
|
24
|
-
}
|
|
25
|
-
return {
|
|
26
|
-
exportName: site.exportName,
|
|
27
|
-
rewriteForServer(name: string): string {
|
|
28
|
-
const inner = stripped.slice(site.parenStart + 1, site.parenEnd).trim()
|
|
29
|
-
const binding =
|
|
30
|
-
inner.length === 0
|
|
31
|
-
? `__belteDefinePrompt__(${JSON.stringify(name)})`
|
|
32
|
-
: `__belteDefinePrompt__(${JSON.stringify(name)}, ${stripped.slice(site.parenStart + 1, site.parenEnd)})`
|
|
33
|
-
return stripped.slice(0, site.callStart) + binding + stripped.slice(site.parenEnd + 1)
|
|
34
|
-
},
|
|
35
|
-
}
|
|
36
|
-
}
|