@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.
Files changed (98) hide show
  1. package/bin/belte.ts +25 -12
  2. package/package.json +2 -1
  3. package/src/appEntry.ts +124 -0
  4. package/src/belteResolverPlugin.ts +217 -194
  5. package/src/build.ts +6 -67
  6. package/src/buildCli.ts +36 -63
  7. package/src/buildDisconnected.ts +127 -0
  8. package/src/bundleApp.ts +123 -0
  9. package/src/bundleDisconnectedEntry.ts +17 -0
  10. package/src/cliEntry.ts +3 -9
  11. package/src/compile.ts +4 -15
  12. package/src/controlServerWorker.ts +261 -0
  13. package/src/dedupeSveltePlugin.ts +66 -0
  14. package/src/discoveryEntry.ts +12 -11
  15. package/src/lib/browser/cache.ts +3 -6
  16. package/src/lib/browser/page.svelte.ts +19 -21
  17. package/src/lib/browser/socketChannel.ts +11 -1
  18. package/src/lib/browser/types/Pages.ts +1 -1
  19. package/src/lib/bundle/BundleMenu.ts +11 -0
  20. package/src/lib/bundle/BundleMenuItem.ts +24 -0
  21. package/src/lib/bundle/BundleWindow.ts +20 -0
  22. package/src/lib/bundle/bindConnectedFlag.ts +29 -0
  23. package/src/lib/bundle/bindRequestNavigate.ts +31 -0
  24. package/src/lib/bundle/buildWebviewLib.ts +111 -0
  25. package/src/lib/bundle/disconnected.css +9 -0
  26. package/src/lib/bundle/disconnected.svelte +192 -0
  27. package/src/lib/bundle/ensureWebviewLib.ts +20 -0
  28. package/src/lib/bundle/exitWithParent.ts +28 -0
  29. package/src/lib/bundle/findFreePort.ts +14 -0
  30. package/src/lib/bundle/infoPlist.ts +46 -0
  31. package/src/lib/bundle/installMacMenu.ts +39 -0
  32. package/src/lib/bundle/listenLocalControlServer.ts +19 -0
  33. package/src/lib/bundle/native/belteMenu.mm +298 -0
  34. package/src/lib/bundle/native/webview.h +4557 -0
  35. package/src/lib/bundle/onMenu.ts +26 -0
  36. package/src/lib/bundle/openWebview.ts +81 -0
  37. package/src/lib/bundle/pngToIcns.ts +47 -0
  38. package/src/lib/bundle/probeBelteServer.ts +34 -0
  39. package/src/lib/bundle/resolveServerBinary.ts +12 -0
  40. package/src/lib/bundle/resolveWebviewLib.ts +51 -0
  41. package/src/lib/bundle/serverBinaryFilename.ts +8 -0
  42. package/src/lib/bundle/stableLocalPort.ts +19 -0
  43. package/src/lib/bundle/waitForServer.ts +23 -0
  44. package/src/lib/bundle/webviewBuildRevision.ts +9 -0
  45. package/src/lib/bundle/webviewCachePath.ts +23 -0
  46. package/src/lib/bundle/webviewLibName.ts +11 -0
  47. package/src/lib/bundle/webviewVersion.ts +7 -0
  48. package/src/lib/cli/createClient.ts +34 -36
  49. package/src/lib/cli/printHelp.ts +45 -2
  50. package/src/lib/cli/runCli.ts +12 -3
  51. package/src/lib/mcp/createMcpResourceServer.ts +1 -1
  52. package/src/lib/mcp/dispatchMcpRequest.ts +53 -73
  53. package/src/lib/server/AppModule.ts +2 -2
  54. package/src/lib/server/cli/handleCliDownload.ts +4 -5
  55. package/src/lib/server/cli/handleCliInstall.ts +17 -0
  56. package/src/lib/server/error.ts +23 -9
  57. package/src/lib/server/json.ts +5 -5
  58. package/src/lib/server/jsonl.ts +10 -5
  59. package/src/lib/server/prompts/definePrompt.ts +6 -6
  60. package/src/lib/server/prompts/renderPromptTemplate.ts +16 -0
  61. package/src/lib/server/prompts/types/Prompt.ts +8 -9
  62. package/src/lib/server/prompts/types/PromptOptions.ts +7 -12
  63. package/src/lib/server/prompts/types/PromptRegistryEntry.ts +3 -5
  64. package/src/lib/server/prompts/types/PromptRoutes.ts +4 -4
  65. package/src/lib/server/redirect.ts +13 -8
  66. package/src/lib/server/rpc/defineVerb.ts +4 -3
  67. package/src/lib/server/rpc/findVerbByCommandName.ts +18 -0
  68. package/src/lib/server/rpc/types/RemoteFunction.ts +1 -1
  69. package/src/lib/server/rpc/types/RemoteHandler.ts +4 -0
  70. package/src/lib/server/runtime/acceptsZstd.ts +8 -0
  71. package/src/lib/server/runtime/buildOpenApiSpec.ts +2 -0
  72. package/src/lib/server/runtime/cacheControlForAsset.ts +7 -2
  73. package/src/lib/server/runtime/createAssetHeaderCache.ts +35 -0
  74. package/src/lib/server/runtime/createPublicAssetServer.ts +6 -20
  75. package/src/lib/server/runtime/createServer.ts +50 -58
  76. package/src/lib/server/runtime/registryManifests.ts +33 -15
  77. package/src/lib/server/runtime/types/RequestStore.ts +2 -3
  78. package/src/lib/server/runtime/withResponseDefaults.ts +24 -0
  79. package/src/lib/server/sockets/createSocketDispatcher.ts +10 -7
  80. package/src/lib/server/sse.ts +10 -5
  81. package/src/lib/shared/cacheControlValues.ts +10 -2
  82. package/src/lib/shared/canonicalJson.ts +1 -5
  83. package/src/lib/shared/createCacheStore.ts +29 -20
  84. package/src/lib/shared/exitOnBuildFailure.ts +17 -0
  85. package/src/lib/shared/fileStem.ts +9 -0
  86. package/src/lib/shared/jsonSchemaForPromptArguments.ts +29 -0
  87. package/src/lib/shared/keyForRemoteCall.ts +7 -5
  88. package/src/lib/shared/parsePromptMarkdown.ts +34 -0
  89. package/src/lib/shared/promptNameForFile.ts +5 -5
  90. package/src/lib/shared/subscribableFromResponse.ts +104 -215
  91. package/src/lib/shared/types/PromptArgument.ts +12 -0
  92. package/src/lib/shared/writeRoutesDts.ts +5 -7
  93. package/src/serverBuildPlugins.ts +25 -0
  94. package/src/serverEntry.ts +4 -0
  95. package/template/package.json +2 -1
  96. package/src/lib/server/prompt.ts +0 -30
  97. package/src/lib/server/prompts/types/PromptMessage.ts +0 -10
  98. 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. Sorted key/value pairs are walked once
10
- and concatenated directly so the hot GET-cache path doesn't allocate per
11
- intermediate (entries / filtered / URLSearchParams).
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/server/prompts/` into the
3
- prompt's MCP name. Strips `.ts` and joins nested folder segments with `-`
4
- (e.g. `code/review.ts` → `code-review`) so two prompts with the same stem
5
- in different folders don't collide and the name stays a single valid MCP
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(/\.ts$/, '').replaceAll('/', '-')
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
- function errorIterable<T>(error: Error): AsyncIterable<T> {
40
- return {
41
- [Symbol.asyncIterator](): AsyncIterator<T, void, undefined> {
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): AsyncIterable<T> {
68
- return {
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
- SSE parser: reads the response body as text frames separated by blank
91
- lines, splits each frame into `event:` / `data:` lines, and yields the
92
- JSON-parsed data payload. The `sse()` respond helper emits an
93
- `event: error\ndata: {"message":...}` frame when the source generator
94
- throws, which we surface as a thrown Error so consumer loops can
95
- surface mid-stream failure rather than silently stopping.
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 parseSse<T>(response: Response): AsyncIterable<T> {
98
- return {
99
- [Symbol.asyncIterator](): AsyncIterator<T, void, undefined> {
100
- const body = response.body
101
- if (!body) {
102
- return emptyIterator<T>()
103
- }
104
- const reader = body.pipeThrough(new TextDecoderStream()).getReader()
105
- let buffer = ''
106
- let bufferStart = 0
107
- const pending: Array<{ event: string; data: string }> = []
108
- let done = false
109
-
110
- async function pullFrames(): Promise<void> {
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
- return {
150
- async next() {
151
- while (true) {
152
- if (pending.length > 0) {
153
- const next = pending.shift() as { event: string; data: string }
154
- if (next.event === 'error') {
155
- try {
156
- const decoded = JSON.parse(next.data) as { message?: string }
157
- throw new Error(decoded?.message ?? 'sse stream error')
158
- } catch (err) {
159
- if (err instanceof SyntaxError) {
160
- throw new Error(next.data || 'sse stream error')
161
- }
162
- throw err
163
- }
164
- }
165
- const value = JSON.parse(next.data) as T
166
- return { value, done: false }
167
- }
168
- if (done) {
169
- return { value: undefined, done: true }
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: reads the response body as text, splits on `\n`,
209
- parses each non-empty line as JSON, and yields the value. The `jsonl()`
210
- respond helper emits a trailing `{"$error":"<message>"}` line when the
211
- source generator throws — that's surfaced here as a thrown Error so
212
- consumer loops can react to mid-stream failure.
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): AsyncIterable<T> {
215
- return {
216
- [Symbol.asyncIterator](): AsyncIterator<T, void, undefined> {
217
- const body = response.body
218
- if (!body) {
219
- return emptyIterator<T>()
220
- }
221
- const reader = body.pipeThrough(new TextDecoderStream()).getReader()
222
- let buffer = ''
223
- let bufferStart = 0
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
- const params: Record<string, 'string'> = {}
13
- for (const segment of parseRouteSegments(routeUrl)) {
14
- if (segment.kind === 'param') {
15
- params[segment.name] = 'string'
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
+ }
@@ -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({
@@ -7,7 +7,8 @@
7
7
  "dev": "belte dev",
8
8
  "build": "belte build",
9
9
  "start": "belte start",
10
- "compile": "belte compile"
10
+ "compile": "belte compile",
11
+ "bundle": "belte bundle"
11
12
  },
12
13
  "dependencies": {
13
14
  "@briancray/belte": "^0.1.0",
@@ -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
- }